Skip to content

Deployment & CI

Three automated pipelines keep BlindProof running:

  1. tests.yml — client + backend tests on every push and PR.
  2. ots-daily.yml — the daily OpenTimestamps maintenance cron.
  3. 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

cd backend
fly deploy

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:

  1. upgrade_ots_receipts first. Promotes yesterday's (and any still-pending earlier) receipts to Bitcoin-confirmed once the calendars have anchored them. This is what flips verify.py from PENDING to PASS.
  2. aggregate_day --yesterday. Builds Merkle roots for yesterday's HMAC commitments, one root per user per day.
  3. 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:

uvx prek install --hook-type pre-push

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:

fly releases --app blindproof
fly deploy --image <previous-release-image>

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