Source document · API/docs/OPERATIONS.md
Production operations runbook
Readiness
Set DATABASE_URL and run go run ./cmd/migrate before deployment. /ready is the
deployment gate; a production deployment must also verify PostgreSQL with
pg_isready and an authenticated migration check.
Staging and production startup require DATABASE_URL, AUTH_TOKEN_SECRET (at
least 32 bytes), an email provider, HTTPS S3-compatible storage credentials,
and LiveKit wss:// URL/API key/API secret. For MailAfrica, set
MAILAFRICA_API_KEY and MAILAFRICA_FROM_ADDRESS; the key must be the
server-side MAIL_ key from the MailAfrica dashboard. To send from your own
verified domain, also set MAILAFRICA_FROM_DOMAIN_ID to its numeric domain ID
and use an address registered for that domain. Without a domain ID, MailAfrica
requires a permitted address on its platform sender domain. The API sends
transactional verification, sign-in, and password-reset messages through
POST https://api.mailafrica.online/api/outbound/emails. MailAfrica currently
documents a charge of 5 TZS per recipient, debited from its wallet, and a
limit of two individual sends per second; keep the wallet funded. If no
MailAfrica key is configured, SMTP remains available and must offer STARTTLS.
They also require LISTING_SUBSCRIPTION_AMOUNT in currency minor units,
LISTING_SUBSCRIPTION_CURRENCY, and LISTING_SUBSCRIPTION_DAYS; these values
determine subscription payment amounts and renewal extensions on the server.
Manual WeChat and Alipay payment collection also requires
WECHAT_PAY_INSTRUCTIONS and ALIPAY_PAY_INSTRUCTIONS. Set these to the
verified merchant account/QR details and the steps customers must follow. The
API includes the method-specific text in payment responses; it never invents a
recipient. Keep the values current with the merchant accounts actually able to
receive payments.
Only development, test, staging, and production are accepted as
APP_ENV; unknown values fail startup so a typo cannot select in-memory
repositories. /ready is enabled automatically in staging and production and
returns 503 when PostgreSQL cannot be reached. Keep migration execution in the
release pipeline before starting the new application version.
Backup and restore
Use managed PostgreSQL point-in-time recovery where available. Before a
release, record the backup identifier. For a restore drill, restore into an
isolated database, run migrations, execute DATABASE_URL=... go test ./...,
and verify order/payment/community/reporting row counts before promotion.
Load and security validation
Run authenticated API load tests against a staging database only. Confirm rate-limit responses, request-size rejection, token expiry, role boundaries, SQL-injection payloads, private-media authorization, and webhook signature validation. Never run destructive restore or load tests against production.
Observability
Scrape /metrics and correlate request_id values from structured logs. Alert
on readiness failures, elevated 5xx responses, rate-limit spikes, database
connection exhaustion, and payment webhook backlog.
Outbox notifications
Order-created events are delivered to customer and provider in-app inboxes by a worker running in the API process. The outbox is at-least-once; notification IDs are deterministic so retrying a partially delivered event does not duplicate an inbox item. Failed events back off for up to 256 seconds and are dead-lettered after 10 attempts. Check for exhausted events with:
SELECT id, event_type, aggregate_id, attempts, last_error, dead_lettered_at
FROM outbox_events
WHERE dead_lettered_at IS NOT NULL
ORDER BY dead_lettered_at DESC;Correct the underlying data or delivery issue before manually clearing the
dead-letter marker and setting available_at = NOW() to retry an event.
Android push delivery
Configure FIREBASE_PROJECT_ID and FIREBASE_CREDENTIALS_FILE with a Firebase service account authorized for Firebase Cloud Messaging. The service account file must be mounted outside the repository and kept private. When configured, persisted order notifications are also sent to enabled Android device tokens. Without these values, notifications remain available in the authenticated in-app inbox.