Deployment
Where things live
| Concern | Where |
|---|---|
| Compute | DigitalOcean App Platform — app tunnelflight-assistant, UUID ed1d52aa-a497-4bbe-9693-054005ae5d7c, primary domain assistant.tunnelflight.com |
| Database | Shared prod MySQL cluster (00a12320-9654-43dd-8384-06d478014c20). Assistant-owned tables in tunnelflight_assistant; curated tools read from IBA via read-only assistant_app user |
| Secrets (canonical) | Infisical — project IBA-Personal-Assistant, env prod |
| Secrets (runtime) | DigitalOcean — synced from Infisical via the do-assistant-sync Secret Sync integration |
| Source control | E-DigitalGroup/tunnelflight on the main branch with deploy_on_push: true |
| Spec / runbook (historical) | assistant/specs/03c-operational-prep/deploy-walkthrough.md (in-repo) — the first-deploy bootstrap from Phase 3.6 |
Secrets: Infisical is canonical, DO is a mirror
Live environment variables are set in Infisical and synced into DO, not edited in the DO UI. The Infisical → DigitalOcean App Platform sync runs on every Infisical change.
- Don’t edit env vars directly in DO App Platform’s UI. Anything you set there is treated as a component-level override and silently wins over the synced app-level value — Infisical changes will appear to be ignored.
- All edits go through Infisical → Secrets → switch to the target environment → save. The sync fires automatically (status visible in Infisical → Integrations → Secret Syncs →
do-assistant-sync). - The sync has
Secret Deletion: Disabled— Infisical only adds/updates keys. It never deletes a DO env var that isn’t in Infisical. (This is the safe failure mode: a misconfigured sync can’t wipe DO’s env.)
Required environment variables
| Key | Dev value | Prod value | Notes |
|---|---|---|---|
ANTHROPIC_API_KEY | shared dev key | shared prod key | Anthropic Console |
ASSISTANT_MODEL | claude-sonnet-4-6 | claude-sonnet-4-6 | Tunable |
ASSISTANT_JWT_SECRET | 32+ random chars | 32+ random chars | Different per env |
ASSISTANT_DB_USER | assistant_app | assistant_app | |
ASSISTANT_DB_PASSWORD | from assistant_app user | from assistant_app user | |
ASSISTANT_DB_NAME | tunnelflight_assistant | tunnelflight_assistant | |
DB_HOST | local 127.0.0.1 | DO MySQL host | |
DB_PORT | 3306 | 25060 | DO managed convention, not 3306 |
DB_NAME | tunnelflight | IBA | Main schema name differs between envs |
API_BASE_URL | http://localhost:8080/api | https://api.tunnelflight.com/api | Where /api/auth/* proxies forward |
ASSISTANT_STAFF_ROLE_IDS | 1,10 | 1,10 | Role allowlist — admin + IBA staff |
ASSISTANT_CRONS_API_KEY | 32+ random chars | 32+ random chars | Auth for /api/cron/run/* |
S3_BUCKET / ASSISTANT_ARTIFACTS_BUCKET | tunnelflight-assistant-artifacts | tunnelflight-assistant-artifacts | |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_REGION | dev IAM | prod IAM | |
SENTRY_DSN | shared Sentry DSN | shared Sentry DSN | One DSN; Sentry splits dev/prod by environment tag |
RELEASE_VERSION | 0.0.1 | release SHA or tag | |
LOG_LEVEL | debug | info |
Variables you should not put in Infisical
NODE_ENV— Node and Next set this themselves (developmentfornext dev,productionfornext buildand runtime). Putting it in a secret store is a footgun. IfNODE_ENV=productionleaks into the dev shell,next devboots in production mode and you get phantomEvalError: Code generation from strings disallowed+ CSS module-parse failures. The localscripts/dev-up.shdefensivelyunsets it after sourcing.env, but it’s better not to have it in Infisical in the first place.NEXT_RUNTIME/NEXT_PHASE— set by Next.js per-runtime; don’t override.
Adding a secret to a new env
- Infisical UI → Project
IBA-Personal-Assistant→ Secrets → switch the environment selector (top) to the target env - Add the key/value, click save
- With auto-sync enabled, the Secret Sync fires within seconds — confirm via Integrations → Secret Syncs →
do-assistant-sync(status should showSucceededwith a freshLast Syncedtimestamp) - Verify in DO via
doctl apps spec get ed1d52aa-a497-4bbe-9693-054005ae5d7c | grep <KEY>(synced values appear at the app-levelenvs:block, not service-level)
Infisical → DO sync setup
The sync is split into two Infisical concepts:
| Concept | What | One-time setup |
|---|---|---|
| App Connection | Credential — the DO Personal Access Token Infisical uses to call the DO API. Named iba-assistant | Created via Infisical → Integrations → App Connections → DigitalOcean |
| Secret Sync | The actual mapping (which env → which DO app). Named do-assistant-sync | Created via Infisical → Integrations → Secret Syncs → DigitalOcean App Platform |
Sync config:
- Source: Infisical env
prod, path/ - Destination: DigitalOcean Connection
iba-assistant, Apptunnelflight-assistant - Initial sync behavior:
Overwrite Destination Secrets - Secret Deletion:
Disabled - Auto-sync:
Enabled
Rotating the DO API token
The DO Personal Access Token used by the App Connection has a 90-day expiration by design. When it expires, the sync silently stops pushing — secrets keep working in DO (the last synced values stick) but new Infisical changes don’t propagate. There is no alert. Calendar this rotation.
Token spec (recreate the same way each rotation):
| Field | Value |
|---|---|
| Name | Infisical sync — assistant (descriptive — visible in DO’s audit log) |
| Expiration | 90 days |
| Scopes | app:read + app:update only — do not grant full read/write |
Rotation steps:
- DigitalOcean → API → Tokens → “Generate New Token” — fill in the spec above, copy the token (DO shows it once)
- Infisical → Integrations → App Connections →
iba-assistant→ Edit credentials → paste new token, save - Trigger a manual sync from the
do-assistant-syncSecret Sync detail page to confirm the new token works - Revoke the old token in DigitalOcean once the sync succeeds
Deploys
The app spec has deploy_on_push: true against the main branch:
- Every merge to
maintriggers an auto-deploy. Plan PR merges accordingly — there’s no “merge to main, deploy later” window. If you need that gap, temporarily pause auto-deploy in the DO app settings before merging. - Manual deploy:
doctl apps create-deployment ed1d52aa-a497-4bbe-9693-054005ae5d7c [--force-rebuild] - Env-change deploys are auto-triggered when the Infisical sync pushes a new value to DO.
Build phase
Build uses the assistant’s Dockerfile (Next.js standalone output, Node 20 alpine). CI gates on assistant/.github/workflows/assistant-ci.yml — lint + typecheck + tests — run on every PR. The deploy build itself skips redundant type-check and lint to keep deploys fast (next.config.ts sets typescript.ignoreBuildErrors and eslint.ignoreDuringBuilds).
Smoke test after deploy
# Liveness
curl -sS -o /dev/null -w "%{http_code}\n" \
https://assistant.tunnelflight.com/api/health
# Expect: 200
# Auth wall is up
curl -sS -o /dev/null -w "%{http_code} → %{redirect_url}\n" \
https://assistant.tunnelflight.com/chat
# Expect: 307 → /login?reason=no_session
# Live login (manual, browser): assistant.tunnelflight.com → log in as admin or IBA staffObservability
- Sentry: project tag
component:assistant. Errors split byenvironmenttag (development/production).beforeSendscrubspassword,token,jwt,authorization,cookie,set-cookiekeys. - Logs: Winston structured JSON to stdout, captured by DO’s log stream. Tail with
doctl apps logs ed1d52aa-a497-4bbe-9693-054005ae5d7c --type run --follow. - Health:
/api/healthreturns 200 once the lifecycle’sconnectors_initial_healthcheckstep succeeds. Failing connectors fail the boot.
First-time deploy bootstrap
The first deploy of a brand-new assistant environment (e.g. a future staging) has a one-time bootstrap not covered by routine dev:up or migrations:
- Create the MySQL users (
assistant_appwriter + reader) - Create the
tunnelflight_assistantschema - Run migrations
0001–0007against the new env’s database - Create the DO app component pointing at the right branch
- Wire up the Infisical → DO Secret Sync for that environment
The full step-by-step is in assistant/specs/03c-operational-prep/deploy-walkthrough.md (in-repo). That doc is the canonical first-deploy runbook; this page covers steady-state operations after the first deploy succeeds.