Skip to Content
AssistantDeployment

Deployment

Where things live

ConcernWhere
ComputeDigitalOcean App Platform — app tunnelflight-assistant, UUID ed1d52aa-a497-4bbe-9693-054005ae5d7c, primary domain assistant.tunnelflight.com
DatabaseShared 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 controlE-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

KeyDev valueProd valueNotes
ANTHROPIC_API_KEYshared dev keyshared prod keyAnthropic Console
ASSISTANT_MODELclaude-sonnet-4-6claude-sonnet-4-6Tunable
ASSISTANT_JWT_SECRET32+ random chars32+ random charsDifferent per env
ASSISTANT_DB_USERassistant_appassistant_app
ASSISTANT_DB_PASSWORDfrom assistant_app userfrom assistant_app user
ASSISTANT_DB_NAMEtunnelflight_assistanttunnelflight_assistant
DB_HOSTlocal 127.0.0.1DO MySQL host
DB_PORT330625060DO managed convention, not 3306
DB_NAMEtunnelflightIBAMain schema name differs between envs
API_BASE_URLhttp://localhost:8080/apihttps://api.tunnelflight.com/apiWhere /api/auth/* proxies forward
ASSISTANT_STAFF_ROLE_IDS1,101,10Role allowlist — admin + IBA staff
ASSISTANT_CRONS_API_KEY32+ random chars32+ random charsAuth for /api/cron/run/*
S3_BUCKET / ASSISTANT_ARTIFACTS_BUCKETtunnelflight-assistant-artifactstunnelflight-assistant-artifacts
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_REGIONdev IAMprod IAM
SENTRY_DSNshared Sentry DSNshared Sentry DSNOne DSN; Sentry splits dev/prod by environment tag
RELEASE_VERSION0.0.1release SHA or tag
LOG_LEVELdebuginfo

Variables you should not put in Infisical

  • NODE_ENV — Node and Next set this themselves (development for next dev, production for next build and runtime). Putting it in a secret store is a footgun. If NODE_ENV=production leaks into the dev shell, next dev boots in production mode and you get phantom EvalError: Code generation from strings disallowed + CSS module-parse failures. The local scripts/dev-up.sh defensively unsets 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

  1. Infisical UI → Project IBA-Personal-Assistant → Secrets → switch the environment selector (top) to the target env
  2. Add the key/value, click save
  3. With auto-sync enabled, the Secret Sync fires within seconds — confirm via Integrations → Secret Syncs → do-assistant-sync (status should show Succeeded with a fresh Last Synced timestamp)
  4. Verify in DO via doctl apps spec get ed1d52aa-a497-4bbe-9693-054005ae5d7c | grep <KEY> (synced values appear at the app-level envs: block, not service-level)

Infisical → DO sync setup

The sync is split into two Infisical concepts:

ConceptWhatOne-time setup
App ConnectionCredential — the DO Personal Access Token Infisical uses to call the DO API. Named iba-assistantCreated via Infisical → Integrations → App Connections → DigitalOcean
Secret SyncThe actual mapping (which env → which DO app). Named do-assistant-syncCreated via Infisical → Integrations → Secret Syncs → DigitalOcean App Platform

Sync config:

  • Source: Infisical env prod, path /
  • Destination: DigitalOcean Connection iba-assistant, App tunnelflight-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):

FieldValue
NameInfisical sync — assistant (descriptive — visible in DO’s audit log)
Expiration90 days
Scopesapp:read + app:update only — do not grant full read/write

Rotation steps:

  1. DigitalOcean → API → Tokens → “Generate New Token” — fill in the spec above, copy the token (DO shows it once)
  2. Infisical → Integrations → App Connections → iba-assistant → Edit credentials → paste new token, save
  3. Trigger a manual sync from the do-assistant-sync Secret Sync detail page to confirm the new token works
  4. 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 main triggers 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 staff

Observability

  • Sentry: project tag component:assistant. Errors split by environment tag (development / production). beforeSend scrubs password, token, jwt, authorization, cookie, set-cookie keys.
  • 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/health returns 200 once the lifecycle’s connectors_initial_healthcheck step 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:

  1. Create the MySQL users (assistant_app writer + reader)
  2. Create the tunnelflight_assistant schema
  3. Run migrations 0001–0007 against the new env’s database
  4. Create the DO app component pointing at the right branch
  5. 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.

Last updated on