Required configuration, gateway webhooks, DigitalOcean App Platform (with its migration job), self-hosting and the quality gates.
Orochia — Deployment & Operations
Requirements
A missing required value makes the requests that need it answer 503 and logs the variable name — the platform never runs on a placeholder secret.
Gateway webhooks
DigitalOcean App Platform
One app per environment, one spec per app. Both build deploy/docker/Dockerfile and run a PRE_DEPLOY job
(node migrate.cjs) that applies packages/db/drizzle before every release.
Then enter the SECRET values in the app (Settings → Environment Variables) once and redeploy. They are
stored, encrypted, in the app — not in the spec, not in git — and every push to the app's branch redeploys with
them (deploy_on_push). A spec file is only for creating the app or changing its structure (services, job,
database, the list of variables): doctl apps update <APP_ID> --spec … replaces the whole spec, and the
SECRET entries have no value in the file — check them in the app afterwards.
The spec has no Redis: rate limits are kept in memory per instance (apps/web/lib/rate-limit.ts), which holds for
a single instance. A shared store comes back before scaling out.
Domain (orochia.com, DNS at Porkbun)
The specs declare the domains (orochia.com + www for production, dev.orochia.com for dev); DNS stays at
Porkbun. After doctl apps create, App Platform shows the target of each name (<app>.ondigitalocean.app):
Remove Porkbun's URL forwarding and any parking A/ALIAS record on the same names first. The TLS certificate is
issued by App Platform once the records resolve.
Owner account
The default user — the platform owner — comes from the environment of the release job:
On every release migrate.cjs applies the migrations, then creates or reactivates that account (ADMIN, age- and
2257-verified, not suspended). Nothing is deleted. Locally the seed does the same from .env, and
npm run db:owner [-- --email … --username … --name "…"] [--reset-password] applies it to any DATABASE_URL.
Bunny Stream security
Thumbnails and preview animations are on the same CDN, so they are signed too — one file per token
(?token=…&expires=…, 6-hour windows), which never opens the video's renditions.
Storage zone (avatars, thumbnails, 2257 documents)
BUNNY_STORAGE_ZONE, BUNNY_STORAGE_API_KEY (the zone's password), BUNNY_STORAGE_ENDPOINT (region host, default
storage.bunnycdn.com = Frankfurt) and BUNNY_PULL_ZONE_HOSTNAME (the pull zone connected to the storage zone). File
names are random UUIDs. 2257 documents are stored under private/ with no public URL and are read by operators only,
through GET /api/admin/documents?ref=…: add an edge rule on the storage pull zone that blocks /private/*.
The token is carried in the path (/bcdn_token=…&token_path=/<guid>/…) so renditions and segments, requested by
relative URL, are authorised too. New uploads are filed in the collection BUNNY_STREAM_COLLECTION_ID; who may
watch is decided by the app, never by Bunny collections.
E-mail (Resend, mg.orochia.com)
The sending domain mg.orochia.com is declared in Resend; its records live at Porkbun under mg: DKIM
TXT resend._domainkey.mg, and CNAME send.mg / CNAME rsend.mg (as Resend lists them). Once Resend shows the domain
verified, set RESEND_API_KEY (SECRET) and COMPLIANCE_ALERT_EMAIL in the app. Mailgun (MAILGUN_API_KEY +
MAILGUN_DOMAIN) remains supported when no Resend key is set.
Self-hosted (Docker Compose + Caddy)
Local development
Quality gates
CI runs the same gates plus the bundled migrator and the seed against PostgreSQL 16.