Deployment & CI¶
Three automated pipelines keep BlindProof running:
tests.yml— client + backend tests on every push and PR.ots-daily.yml— the daily OpenTimestamps maintenance cron.docs-drift.yml— the docs-drift check (see Keeping docs in sync).
Plus the backend itself, which is Docker-built and deployed to Fly.io, and this docs site, which is built and deployed by Cloudflare Pages on every push to main.
Backend on Fly.io¶
Configuration lives in backend/fly.toml. Summary:
- App:
blindproof - Region:
lhr(London — single-region for the POC) - VM:
shared-cpu-1x, 256 MB RAM - Volume:
data, 1 GB, mounted at/app/data - Storage: SQLite at
/app/data/db.sqlite3, blob store at/app/data/blobs/ - TLS: force HTTPS; Let's Encrypt cert via Fly
- Custom domain:
blindproof.tomd.org(CNAME + ACME challenge via Cloudflare, grey-clouded)
Deploying¶
Uses the repo's Dockerfile (Python 3.12-slim, uv for deps, non-root user) and entrypoint.sh (runs manage.py migrate --noinput at container start, then execs gunicorn on 0.0.0.0:8000).
Migrations run at container start, not as a Fly release command. Release machines don't mount volumes, so a release-command migration would silently operate on an ephemeral empty DB. Running inside entrypoint.sh means migrations apply to the real, mounted /app/data/db.sqlite3.
Secrets¶
Managed via fly secrets set. Production currently holds:
| Secret | Purpose |
|---|---|
DJANGO_SECRET_KEY |
Django session signing. |
BLINDPROOF_SIGNING_KEY |
Ed25519 private key for proof-bundle signatures. |
BLIND_OTS_MODE |
real. |
DJANGO_ALLOWED_HOSTS |
blindproof.fly.dev,blindproof.tomd.org. |
Everything else is baked into fly.toml as non-secret env.
Observability¶
fly logs --app blindproof is the main signal. A single VM means there's no aggregation concern to worry about.
The daily OTS cron¶
.github/workflows/ots-daily.yml runs at 02:00 UTC daily (plus workflow_dispatch for on-demand runs) and executes three management commands against the Fly machine via flyctl ssh:
sequenceDiagram
participant GA as GitHub Actions<br/>02:00 UTC daily
participant Fly as flyctl ssh
participant App as Fly machine
GA->>Fly: authenticate (FLY_API_TOKEN)
Fly->>App: manage.py upgrade_ots_receipts
App-->>Fly: yesterday's pending → confirmed
Fly->>App: manage.py aggregate_day --yesterday
App-->>Fly: new Merkle roots
Fly->>App: manage.py submit_ots_receipts
App-->>Fly: roots handed to calendars
Fly-->>GA: exit 0
Order matters:
upgrade_ots_receiptsfirst. Promotes yesterday's (and any still-pending earlier) receipts to Bitcoin-confirmed once the calendars have anchored them. This is what flipsverify.pyfrom PENDING to PASS.aggregate_day --yesterday. Builds Merkle roots for yesterday's HMAC commitments, one root per user per day.submit_ots_receipts. Hands new roots to the three default public calendars (alice.btc,bob.btc,finney).
Auth¶
A Fly deploy token scoped to the blindproof app only, stored as the FLY_API_TOKEN repo secret. Not a general-purpose Fly account token — the blast radius is limited to this one app.
Why GitHub Actions and not a Fly scheduled machine¶
Simpler ops: one cron in a YAML file, version-controlled alongside the code, manually triggerable via workflow_dispatch. The lag tolerance is hours (submit at 02:00, anchor 2–6 hours later, upgrade next day), so GitHub Actions' 5–15 min cron variance is a non-issue. Fly's scheduler would work too but adds a dedicated machine to the ops surface with nothing to offset it.
If sub-hour guarantees ever become a requirement, revisit.
This docs site¶
Built and deployed by Cloudflare Pages on every push to main. The Pages project pulls the repo via the Cloudflare Pages GitHub App, runs pip install zensical && zensical build --clean, and publishes the site/ directory. No workflow file — build config lives in the Pages project settings.
Published at https://docs.blindproof.tomd.org/.
Moved off GitHub Pages so the source repo can stay private without requiring GitHub Enterprise.
Keeping docs in sync¶
Because the docs site auto-deploys on every push to main, a stale page ships to production as fast as a stale API response. A drift check enforces the correspondence between code changes and doc updates.
The mapping is data at scripts/docs-drift-mapping.toml — a small TOML file listing areas, each with a set of code paths (fnmatch patterns) and the docs pages that should be updated alongside them. When adding a new public-surface area or a new doc page, extend the mapping in the same commit.
The checker is scripts/check_docs_drift.py — pure stdlib Python, ~130 lines. Given a diff range, it fails if any area has code changes without matching doc changes. Escape hatches: [skip-docs-check] on its own line in a commit message, or DOCS_DRIFT_SKIP=1 in the environment (CI still checks).
Local enforcement via prek, wired through .pre-commit-config.yaml as a pre-push hook. One-time setup per clone:
After that, git push runs the check automatically. A failure blocks the push with a readable report of what changed and what else needed to change.
CI backstop at .github/workflows/docs-drift.yml — runs the same prek hook on every push to main (and on PRs, reserved for future use). Computes the diff range from github.event.before/after, passes it to the checker via the DOCS_DRIFT_RANGE env var. A failure leaves a red status check on the commit, so direct-to-main pushes that skipped the local hook (git push --no-verify, or a clone where prek install hadn't been run) still surface the drift post-hoc.
Production housekeeping¶
Demo/test accounts in the production SQLite. [email protected] carries seeded plausible snapshots for dashboard visuals; the "snapshots" are metadata-only (their ciphertext blobs are random bytes — they decrypt to nothing useful). Remove via fly ssh console --app blindproof + python manage.py shell when no longer wanted.
SQLite backups. Fly volumes are not automatically backed up. For the POC the data is low-stakes (demo accounts); for V1, wire up periodic snapshots to B2.
Fly machine restarts are routine and non-disruptive — migrations re-run on start and are idempotent, and gunicorn rebinds cleanly.
Rollback¶
Not automated. If a deploy breaks:
Releases are image-tagged, so rolling back is a single command once the target tag is known. Migrations are additive — rollback does not un-migrate. If a schema change needs undoing, write a forward migration that restores the previous shape.
See also¶
- Backend internals — what the management commands actually do.
- Testing — the
tests.ymlworkflow. - Proof bundle format — what the signing key is protecting.