Deploy and operate
Operator runbook for Duckbill™ on Vercel. Product chrome and this page use Duckbill™. Governing specifications may still say Inbox Agent until MAIL-193. npm packages stay @inbox-agent/* until that rename.
Do not put secret values in git, docs, Jira, screenshots, or logs. ENABLE_SEND stays off.
Hosts
| Environment | Git branch | Site |
|---|---|---|
| Preview | preview | https://duckbill-dev.jasonkoch.io |
| Production | main | https://duckbill.jasonkoch.io |
Vercel project inbox-agent-docs is linked to PlatypusDBA/duckbill. Framework is Next.js. Root directory is apps/web. Install is cd ../.. && npm ci. Build is cd ../.. && npm run build -w @inbox-agent/web. Include files outside the root so docs/ and workspace packages are available.
Promotion is PR-only: feature branch → preview, then preview → main. Do not commit directly to preview or main. See CI and docs publishing.
Required configuration (names only)
Process start fails closed without these. Copy .env.example locally. Set the same names in Vercel for preview and production.
| Variable | Rule |
|---|---|
APP_ENV | preview or production on those hosts (local / test elsewhere) |
DATABASE_URL | postgres:// or postgresql:// |
TOKEN_ENCRYPTION_KEY | secret, at least 32 characters |
SESSION_SECRET | secret, at least 32 characters |
Dangerous flags stay false unless the value is exactly true (ADR 0003). MVP keeps:
ENABLE_SEND=false
ENABLE_DELETE=false
ENABLE_MICROSOFT_WRITES is the safe write-back gate (read / flag / archive). It does not authorize send. PAUSE_AUTOMATION and PAUSE_AI are kill switches and do not enable send or delete.
Optional host names
Apple routes return 503 until these are set (Sign in with Apple, ADR 0028):
APPLE_CLIENT_ID
APPLE_TEAM_ID
APPLE_KEY_ID
APPLE_PRIVATE_KEY
APPLE_REDIRECT_URI
Microsoft connect returns 503 until these are set (live Microsoft connect):
MICROSOFT_CLIENT_ID
MICROSOFT_CLIENT_SECRET
MICROSOFT_REDIRECT_URI
Gmail connect returns 503 until these are set (live Gmail connect):
GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET
GMAIL_REDIRECT_URI
Register the exact preview and production /connect/gmail/callback URLs in
Google Cloud. Gmail remains least-privilege: do not add full-mail,
send, or compose scopes.
iCloud connect needs no extra host variable. The owner enters an app-specific
password on /accounts. It is stored as imap_secret ciphertext with
TOKEN_ENCRYPTION_KEY. Apply migrations 0021_icloud_imap_secret.sql,
0022_icloud_move.sql, and 0023_icloud_mailbox_writes.sql before live iCloud
writes. A second iCloud address is a second row. Do not put that password in Vercel env.
Owner session is an ops escape, not a second product IdP:
OWNER_SESSION_ENABLED
OWNER_IDENTITY_SUBJECT
Never commit .p8 files, Entra or Google secrets, or connection strings.
Database
PostgreSQL is the shared Supabase Sandbox schema inbox_agent. See persistence and environments.
npm run db:migrate # requires DATABASE_URL
npm run migrate:ci # CI: DATABASE_URL or ephemeral PGlite
Migrations are versioned SQL under database/migrations/. Do not drop unrelated schemas. Opening /mail applies any pending files with the existing DATABASE_URL. That does not add or change environment variables.
Deploy sequence
- Merge the feature PR into
preview. Confirm Vercel builds@inbox-agent/web. - Check preview HTTP 200:
/,/docs,/docs/operations/deploy,/api/health/live. - Confirm
ENABLE_SENDis nottrueon the preview host. - Open a
preview→mainpromotion PR. Merge only after preview is healthy. - Check production HTTP 200 on the same paths.
- Run
GET /api/health/readyandGET /api/health. A provider outage must not mark the process unready.
Health and recovery
| Probe | Meaning |
|---|---|
GET /api/health/live | Process is alive |
GET /api/health/ready | Database and required configuration |
GET /api/health | Queues, DLQ, drift, recovery mode, per-account lag |
GET /api/providers/health | Per-account provider health |
/ops | Same redacted health document |
See operational health and backup and restore. After restore, stay in recovery mode until reconciliation and the restore checklist pass. assertMutationsAllowed fails closed while recovery is active.
npx tsx scripts/backup-restore.ts export --out /tmp/duckbill-app-state.json
npx tsx scripts/backup-restore.ts restore --in /tmp/duckbill-app-state.json
Store backup JSON as a secret artifact. Do not commit it.
Logging
Mail bodies, tokens, token ciphertext, and secrets must never appear in routine logs. Redaction is always on. See privacy and logging.
Rollback
- Revert the promotion PR (
preview→mainor feature →preview) with a new PR. Do not force-push protected branches. - If a bad migration landed, stop writes, keep recovery mode, and restore application-owned state from the last backup.
- Rotate any credential that may have been exposed. Do not paste the new value into docs or Jira.
