Appearance
Gateway ops — deploy, migrate, seed
Live
The hosted gateway at api.tenkeybridge.com went live 2026-07-09 (Fly.io sjc + Neon aws-us-west-2), proven end-to-end against a real QuickBooks Enterprise company file. This page documents the deploy flow that stood it up.
The gateway is a single Node/TypeScript process (Fly.io + Neon Postgres) that terminates the QBO-compatible REST API, brokers the agent-plane WebSocket, and runs the OAuth2 code-flow clone. This page covers standing one up: first deploy, DNS + TLS, database migrations, and seeding the first tenant.
One-time: create the Fly app
From the repo root:
bash
cd apps/gateway
fly launch --no-deployThis reads apps/gateway/fly.toml (app name tenkeybridge-gateway, region sjc — the nearest live region to the Neon database in aws-us-west-2; Fly deprecated den and sea in mid-2026 — min_machines_running = 1, auto_stop_machines = "off" — the agent's persistent WebSocket connection needs a machine that's always warm) and creates the Fly app without deploying yet.
Secrets
DATABASE_URL is never committed to fly.toml — it's set as a Fly secret so it isn't visible in the build config or flyctl config show:
bash
fly secrets set DATABASE_URL="postgres://<user>:<pass>@<neon-host>/<db>?sslmode=require"Use the pooled Neon connection string (PgBouncer, port 6543) — the gateway holds one long-lived pg.Pool per process, and Fly may run more than one machine.
Deploy
bash
fly deployThis builds apps/gateway/Dockerfile from the repo root (multi-stage, node:24-slim, pnpm install --prod — the gateway runs via tsx directly, no compile step) and rolls out the new image.
DNS + TLS
Point api.tenkeybridge.com at the Fly app with a CNAME in Vercel DNS:
api.tenkeybridge.com. CNAME tenkeybridge-gateway.fly.dev.Then request the certificate on the Fly side:
bash
fly certs add api.tenkeybridge.com
fly certs show api.tenkeybridge.com # poll until status is "Ready"Fly's edge proxy forwards the WebSocket upgrade for /agent over the same internal_port as the REST traffic — no separate listener or extra Fly config is needed for the agent plane.
Run migrations against Neon
Migrations are drizzle-kit generated SQL, checked into apps/gateway/drizzle/. Apply them with the migrate CLI, pointed at the real (non-pooled, for DDL) Neon URL:
bash
DATABASE_URL="postgres://<user>:<pass>@<neon-host>/<db>?sslmode=require" \
pnpm --filter @tenkeybridge/gateway db:migrateTo regenerate migrations after a schema change:
bash
cd apps/gateway && pnpm db:generateschema.ts (Drizzle table definitions) and applySchema in src/store/db.ts (hand-written idempotent DDL used by the test suite's PGlite instances) are the same schema expressed twice — keep them in agreement and re-run the gateway test suite after any change to either.
Seed the first tenant
Use the seed CLI (apps/gateway/src/seed.ts, exposed as pnpm gateway:seed from the repo root — there is no root-level pnpm seed, that name only exists as a package script inside apps/gateway) against the same DATABASE_URL:
bash
# 1. Create a realm (tenant) — prints realmId + a one-time admin key
DATABASE_URL=$NEON_URL pnpm --filter @tenkeybridge/gateway seed create-realm --name "Acme Co"
# 2. Issue an agent token for that realm — the edge agent's appsettings.json needs this
DATABASE_URL=$NEON_URL pnpm --filter @tenkeybridge/gateway seed issue-agent-token --realm $REALM_ID
# 3. Register an OAuth client for whatever app will call the REST API
DATABASE_URL=$NEON_URL pnpm --filter @tenkeybridge/gateway seed create-client \
--name "My App" --redirect https://myapp.example.com/oauth/callbackEach command prints its secret (adminKey, agentToken, client_secret) once — store it immediately, it is not retrievable later (only the hash is persisted).
create-realm also accepts --admin-key <key> to supply your own admin key; it must be at least 16 characters. Prefer omitting the flag so a strong key is generated for you.
Run the OAuth2 code-flow (authorize → admin-key consent → redirect with code → token exchange) against /oauth2/v1/authorize and /oauth2/v1/tokens to get an ACCESS_TOKEN scoped to that realm.
Milestone proof
With an edge agent connected for the realm and a valid access token, this is the end-to-end proof the gateway is live and bridging real QuickBooks data:
bash
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" \
https://api.tenkeybridge.com/v3/company/$REALM_ID/customer/$CUSTOMER_ID | jq .Staging
Staging E2E runs against a Neon branch database seeded with throwaway tenant data — not a second Fly app. Branch Neon's main database, point a local or preview gateway process at the branch's connection string via DATABASE_URL, run migrations and seed as above, and tear the branch down when done.