Skip to content

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-deploy

This 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 deploy

This 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:migrate

To regenerate migrations after a schema change:

bash
cd apps/gateway && pnpm db:generate

schema.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/callback

Each 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.

TenkeyBridge is an independent product, not affiliated with, endorsed by, or sponsored by Intuit Inc. QuickBooks, QuickBooks Online, and QuickBooks Desktop are trademarks of Intuit Inc., used only to describe compatibility.

TenkeyBridge is an independent product, not affiliated with, endorsed by, or sponsored by Intuit Inc. QuickBooks, QuickBooks Online, and QuickBooks Desktop are trademarks of Intuit Inc., used only to describe compatibility.