Skip to documentation
Open dashboard
Docs/Operate and troubleshoot

Self-hosting and operations

Processes, configuration, migrations, deployment, recovery, and monitoring.

Download this chapter
On this page

This chapter is for operators of an authorized CherrySub source checkout. It describes the implemented processes and operational contracts. The public staging installation is not a template you can treat as a production high-availability guarantee.

Processes and boundaries

Process/componentRole
Core server (cmd/server)Receipt/store ingress, projections, runtime/admin APIs, webhook worker, reconciliation
Web control plane (cmd/web-control-plane)Hosted accounts, projects, dashboard queries, credential management, export worker
Web applicationNext.js dashboard, public site, and documentation
PostgreSQLImmutable ledger, mutable projections, memberships, sessions, durable jobs/outbox
Email/report gatewayFixed-template account email and private export object/download handling
Reverse proxyHTTPS termination, private origin routing, protected observability
Optional sandbox brokerDevelopment bridge for checked-in sandbox applications; not your production authentication system
CLIRuns on the operator/developer's computer; uses hosted APIs and local store sign-in

Core and web control plane share the intended database and compatible encryption-key configuration. Store/receipt verification happens before the transaction that appends and reduces evidence. Webhook network delivery does not hold a database transaction open.

Local development

From the backend checkout:

sh
cp .env.example .env
docker compose up --build

Review the example before changing it; it contains development defaults. Change POSTGRES_PORT and the host DATABASE_URL together if the host port conflicts, or APP_PORT for the application port. Container-to-container addresses are separate.

For host commands, load your intended development environment, then run make migrate, make dev, make test, and make lint. go run ./cmd/server exposes its serve/migrate/replay/reconcile commands. The web control plane is a separate process, typically on port 8082; the core defaults to 8080. With the intended development database/environment loaded, start the hosted process in another terminal:

sh
HTTP_ADDR=:8082 WEB_COOKIE_SECURE=false go run ./cmd/web-control-plane

The insecure-cookie override is for local HTTP development only. Hosted verification/invitation/reset flows still need the configured email gateway; running locally does not disable email verification or store verification.

In the web checkout, use Node 24, npm ci, and API_URL pointing at the hosted control-plane API origin that exposes /auth and /dashboard. Start with npm run dev. The browser also needs the intended publicly reachable runtime API for its SDK integration; these roles must not be confused.

Core configuration reference

VariablePurpose / default
DATABASE_URLRequired intended PostgreSQL connection
HTTP_ADDRListen address; core defaults to :8080
LOG_LEVEL, LOG_FORMATinfo and json defaults; text supported for development
RECEIPT_RATE_LIMIT_RPS, RECEIPT_RATE_LIMIT_BURSTPer-key, process-local receipt throttle; defaults 100/200
GOOGLE_PLAY_API_ENABLEDEnables environment-backed Play reads independently of RTDN
GOOGLE_APPLICATION_CREDENTIALSApplication Default Credentials file path for environment-backed Google access
GOOGLE_PUBSUB_AUDIENCE, GOOGLE_PUBSUB_SERVICE_ACCOUNTExact authenticated push audience and identity; configured together
APPLE_ISSUER_ID, APPLE_KEY_ID, APPLE_PRIVATE_KEY_B64Complete environment-backed Apple server-key tuple; all or none
STORE_CREDENTIAL_MASTER_KEYSComma-separated base64 32-byte encryption keys for managed app credentials
ADMIN_API_KEYSOperator root bearer-key ring enabling /admin/v1
WEBHOOK_MASTER_KEYSBase64 32-byte encryption-key ring for webhook signing secrets
WEBHOOK_WORKERSDelivery concurrency; default four in the core configuration
RECONCILIATION_INTERVAL0 disables scheduled recovery; otherwise 1 minute–24 hours
RECONCILIATION_LOOKBACKFirst-run lookback, default 24 hours; bounded 1 minute–30 days
RECONCILIATION_APPLE_ENVIRONMENTSproduction,sandbox by default; choose the intended environments

Managed store credentials can supply per-app reads without relying on one deployment-wide store key. The store encryption ring currently falls back to WEBHOOK_MASTER_KEYS when its dedicated variable is empty; configure dedicated compatible rings deliberately rather than assuming unrelated processes can decrypt each other's records.

Hosted web and email/report configuration

The web control plane needs DATABASE_URL, its HTTP_ADDR, and the relevant store/webhook master keys. WEB_SESSION_TTL defaults to seven days and accepts one minute through 30 days. Keep WEB_COOKIE_SECURE enabled behind production-shaped HTTPS. EXPORT_WORKERS defaults to one and is bounded from one through eight. The hosted process accepts SHUTDOWN_WINDOW from one second to one minute (default ten seconds); the core uses its ten-second configured shutdown default.

Configure EMAIL_GATEWAY_URL, EMAIL_GATEWAY_SECRET, and PUBLIC_WEB_URL together. The gateway secret must match its deployment; the public web origin controls allowed account-action destinations. Without delivery configuration, email-dependent flows/exports are unavailable rather than pretending mail was sent.

The checked-in email/report gateway is a Cloudflare Worker with an email-sending binding and a private R2 REPORTS binding. Its public action/report origins and allowed sender are explicit configuration. Provision the appropriate sender and private bucket, store the shared secret in the platform secret store, and run that package's npm ci / npm run check before the approved environment's deployment. Its checked-in production environment is not configured.

After a gateway rollout, check health, unauthenticated request rejection, one authorized test-account email, and a bounded test report upload/download. Export links expire after 24 hours; private bucket lifecycle cleanup is a backstop, not the primary link authorization mechanism.

Deploy the origin

Use the staging Compose/Caddy templates as a topology reference: expose HTTPS through the proxy, keep PostgreSQL and application ports private, run migrations deliberately, and wait for readiness before routing traffic. Immutable image tags, versioned configuration, and backups make rollback concrete.

Before a release, back up the database, record actual image references/configuration, retain required encryption keys, run formatting/tests/static analysis, validate Compose/proxy configuration, and check readiness on the new version. Production requires its own reviewed blue/green or equivalent rollout, compatible expand/contract migrations, origin protection, off-site recovery, and availability targets. Two processes on one host do not protect against host failure.

The production topology document records a preferred direction; it does not mean a multi-origin deployment is currently live. Never infer authorization to mutate another environment from the word staging in a command example.

Reconciliation and missed observations

Run a bounded recovery pass with the core server command:

sh
go run ./cmd/server reconcile --app-id APP_UUID

Scheduled recovery uses RECONCILIATION_INTERVAL. Successful streams retain cursors and intentionally overlap five minutes. Apple recovers signed notification history for selected environments. Google recovers void/refund evidence and bounded order-accounting enrichment. Each stream advances only after its full page set succeeds; one app/store failure does not stop every stream.

A PostgreSQL advisory lock prevents concurrent ownership of the same reconciliation run across replicas. Recovery does not bypass verification, invent another provider's history, or treat Google order-accounting data as an independent access decision.

Replay and projection repair

Start with read-only verification:

sh
go run ./cmd/server replay --verify --app-id APP_UUID

Replay rebuilds projections in an unlogged shadow schema, checks drift, and compares derived events without replacing live tables. It uses a global advisory lock and brief safe ledger-boundary locks; do not launch competing jobs into the fixed shadow schema.

Only after reviewing the report and establishing a recoverable backup should an operator deliberately use replay --apply. Apply performs the final compared projection replacement; it is not an event-history edit. Keep the original source ledger and mapping-version rules intact.

Health, metrics, and response

Core /livez reports process liveness. /readyz and /healthz check database readiness. The web control plane has its own /healthz. /metrics is for protected internal scraping and is blocked by the staging public proxy.

Monitor ingest acceptance/deduplication/rejection/failure, receipt throttling, webhook attempts/duration/disabled endpoints, reconciliation runs/records/duration, export job failures, and database health. Labels are bounded; avoid placing customer tokens or private store payloads in logs/metrics.

For an incident, preserve evidence, identify the failing dependency, stop uncontrolled retry amplification, and restore the last known-good routing/configuration when needed. Test database and key restoration rather than relying only on a successful backup command. Troubleshooting · Security.