Receipts docs
v1.0.0
Live demo Get help
● Ship · Server

Deployment
service, HTTPS, backups.

Run Receipts as a service behind an HTTPS proxy, block countries if you need to, and back up the database. Plus your responsibilities as an operator.

01Run it as a service (systemd) Not tested here

Create /etc/systemd/system/receipts.service:

[Unit]
Description=Receipts signal marketplace
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=receipts
Group=receipts
WorkingDirectory=/opt/receipts
ExecStart=/usr/bin/npm start
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now receipts
sudo systemctl status receipts           # should say "active (running)"
journalctl -u receipts -f                # live logs; Ctrl+C to leave

The app stops cleanly on systemctl stop: it stops the background jobs, the bot and the console, then closes the database. WorkingDirectory matters: the app reads .env from there.

02HTTPS with Caddy Not tested here

The app listens on plain HTTP port 8787. Caddy sits in front, gets a free Let's Encrypt certificate and renews it. Install Caddy following caddyserver.com/docs/install, open ports 80 and 443 (sudo ufw allow 80,443/tcp), then replace /etc/caddy/Caddyfile with:

signals.example.com {
    encode gzip
    reverse_proxy 127.0.0.1:8787
}
sudo systemctl reload caddy

Set TRUST_PROXY=1 so the sign-in limits use the visitor's address, and keep port 8787 closed to the internet. nginx works as well: proxy to 127.0.0.1:8787, set X-Forwarded-For, and manage the certificate yourself (for example with certbot).

03Country blocking

Admin → Access → Blocked countries blocks visitors by two-letter code (for example US GB). Blocked visitors get HTTP 451. The app does not look up countries itself: it reads a request header your proxy or CDN adds.

  • Cloudflare (proxied): adds CF-IPCountry when IP Geolocation is on. That is the default header; nothing else is needed.
  • Caddy or nginx without Cloudflare: add a GeoIP module (for example a MaxMind GeoLite2 database) that sets a header such as X-Country, and enter that name in Admin → Access → Country header.
Limits

A request without the header is let through, and if your server is reachable without Cloudflare a visitor can send any value. Treat blocking as a reasonable measure you document, not a promise. /static/ and /webhooks/ are never blocked, so Stripe can always reach you.

04Backups

Everything (accounts, signals, seals, batches, payments, settings) is in data/receipts.db; the console's settings, team and audit are in data/admin.sqlite. Back up .env (with ADMIN_SECRET_KEY) separately and privately.

The databases run in WAL mode. Do not just copy the files while the app runs; use SQLite's online backup, which is safe while running Tested:

cd /opt/receipts
sqlite3 data/receipts.db ".backup backup.db"
sqlite3 data/admin.sqlite ".backup admin-backup.sqlite"

A nightly backup with cron (sudo crontab -u receipts -e):

15 3 * * * cd /opt/receipts && mkdir -p backups && sqlite3 data/receipts.db ".backup backups/receipts-$(date +\%F).db" && find backups -name 'receipts-*.db' -mtime +14 -delete

Copy backups off the server too. To restore: stop the app, replace the database file, delete any -wal and -shm files next to it, start the app. The owner can also download a snapshot from Operations → Data backup.

Back up

Losing the database loses the sealed records. The Bitcoin timestamps prove a batch root existed at a time, but the event bodies needed to show that proof live only in your database.