Appearance
Deploy guide
Runbook for spinning up vecnet-admin in production. Three moving parts: the Next.js app on Vercel, Postgres on Supabase (or any cloud Postgres), and Clerk for auth. Optional: Redis for background workers + a cron orchestrator.
This doc is the canonical order of operations. Skip the worker section if you only need on-demand admin access without scheduled settlement runs or snapshots.
Prereqs
| Account | Why |
|---|---|
| Vercel | Hosts the Next.js app |
| Supabase (or Neon / RDS) | Production Postgres |
| Clerk | Auth (free tier suffices for the admin platform) |
| Resend (later) | Email — settlement notifications |
| Upstash or Railway Redis (optional) | BullMQ workers |
1. Provision Clerk
- Create a new Clerk application. Sign-in methods: Email + password only (the admin platform doesn't need OAuth).
- Copy the Publishable Key and Secret Key from the API Keys page.
- Under Webhooks, add an endpoint:
- URL:
https://<your-vercel-host>/api/clerk-webhooks(placeholder for now; update after Vercel deploy) - Events:
user.created,user.updated,user.deleted
- URL:
- Copy the Signing Secret (
whsec_…) shown after creating the webhook. - Create the operator users in Clerk's dashboard. For each one, set Public metadata:jsonValid roles:
{ "role": "superadmin" }superadmin,finops,integrations. Without this the user can sign in but lands on/unauthorized.
2. Provision Postgres
Supabase path:
- Create a new project. Pick a region close to Mexico City for latency.
- In Project Settings → Database, copy the Connection string (
postgresql://…). Use the Session pooler URL forDATABASE_URL(long-lived) and the Direct connection URL forDIRECT_URL(Prisma migrations). - The Vecnet schema doesn't use Supabase RLS or Auth — only the Postgres layer. Disable RLS on tables if Supabase's default templates enable it.
3. Set Vercel env vars
In the Vercel project settings:
DATABASE_URL=postgresql://…pooler.supabase.com:6543/postgres
DIRECT_URL=postgresql://…direct.supabase.com:5432/postgres
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_…
CLERK_SECRET_KEY=sk_live_…
CLERK_WEBHOOK_SIGNING_SECRET=whsec_…
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/
NEXT_PUBLIC_CLERK_AFTER_SIGN_OUT_URL=/sign-in
USE_STUBS=false # true while Tonder/Menta integration is pending
# Optional — only if workers are also being deployed
REDIS_URL=redis://default:…@redis.upstash.io:6379
# Optional — when email is wired
RESEND_API_KEY=re_…4. Push the schema
From your local machine, once env vars are set:
bash
cd vecnet-admin
DATABASE_URL=<prod-url> DIRECT_URL=<prod-direct> npx prisma db push
DATABASE_URL=<prod-url> DIRECT_URL=<prod-direct> npx prisma generate
DATABASE_URL=<prod-url> DIRECT_URL=<prod-direct> npm run db:seeddb:seed only creates the house accounts now (no admin users — Clerk owns those). The seed banner prints the bootstrap instructions.
5. Deploy
bash
cd vecnet-admin
vercel link # link to your Vercel project
vercel --prod # deployAfter the first deploy, go back to the Clerk dashboard and update the webhook URL to your real Vercel host.
6. Verify
- Visit
https://<host>/sign-in→ Clerk's sign-in form renders. - Sign in with a user whose
publicMetadata.roleis set. Land on/. - Visit
/finances→ Treasury KPIs render (likely 0s on a fresh DB). - Visit
/merchants→ empty list, but the "New merchant" CTA works. - Visit
/sign-inwhile logged in → redirects to/.
7. Workers — separate runtime
Vercel's serverless functions can't run long-lived workers. Two options:
Option A: Vercel Cron + on-demand endpoints
Add a thin API route per worker that calls its runOnce() function, then schedule with Vercel Cron:
/api/cron/settlement-scheduler → hourly
/api/cron/rolling-release → daily
/api/cron/daily-snapshots → 30 23 * * *Gate each cron route by header (Authorization: Bearer ${CRON_SECRET}) and skip the BullMQ/Redis path entirely — the cron handler just calls composeSettlement, releaseDueReserves, or buildSnapshotsForDay directly.
This is the recommended path for the admin platform: no Redis, no background process, the Vercel Cron scheduler does the work.
Option B: Railway / Fly.io for workers
If you want true BullMQ scheduling with retries + dead-letter queues:
- Provision Upstash Redis or Railway Redis. Copy
REDIS_URL. - Set the env var on both Vercel (for the producer side) and the worker host.
- Deploy
workers/*.tsto Railway/Fly as long-running services. Each worker imports itsrunOnce()function via the BullMQ scheduler already wired in the file.
8. Tonder / Menta live integration
The orchestrator currently calls stubs (lib/stubs/tonder.ts, lib/stubs/menta.ts) when USE_STUBS=true. To go live:
- Get production API credentials for both PSPs.
- Replace the stub calls in
lib/transactions/create.tswith real SDK calls (signature unchanged — they just need to return{ id, status }). - Flip
USE_STUBS=false.
9. Common gotchas
- Prisma client extensions + serverless edge runtime: the ledger immutability extension in
lib/db-extensions.tsis Node.js-only. Vercel's default Node runtime is fine; explicitly setruntime: "nodejs"on the Clerk webhook route just to be safe. - BigInt JSON serialization: every API response goes through
lib/money/serialize.ts:jsonSafe— neverJSON.stringifya BigInt directly (it throws). - Mexico City timezone: permanently UTC-6 since 2022 (no DST). All cycle math uses
dayjs.tz("America/Mexico_City")— don't shortcut tonew Date()math. - First deploy errors on missing AdminUser: until you've signed in once and the webhook fired (or you ran
sim-admin-user.ts), every protected route bounces to/unauthorized. Expected.
10. Roll back
Vercel: redeploy the previous successful deployment from the dashboard. Prisma: prisma db push is destructive on column drops. Take a Supabase snapshot before every schema-changing deploy.