Skip to content

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

AccountWhy
VercelHosts the Next.js app
Supabase (or Neon / RDS)Production Postgres
ClerkAuth (free tier suffices for the admin platform)
Resend (later)Email — settlement notifications
Upstash or Railway Redis (optional)BullMQ workers

1. Provision Clerk

  1. Create a new Clerk application. Sign-in methods: Email + password only (the admin platform doesn't need OAuth).
  2. Copy the Publishable Key and Secret Key from the API Keys page.
  3. 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
  4. Copy the Signing Secret (whsec_…) shown after creating the webhook.
  5. Create the operator users in Clerk's dashboard. For each one, set Public metadata:
    json
    { "role": "superadmin" }
    Valid roles: superadmin, finops, integrations. Without this the user can sign in but lands on /unauthorized.

2. Provision Postgres

Supabase path:

  1. Create a new project. Pick a region close to Mexico City for latency.
  2. In Project Settings → Database, copy the Connection string (postgresql://…). Use the Session pooler URL for DATABASE_URL (long-lived) and the Direct connection URL for DIRECT_URL (Prisma migrations).
  3. 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:seed

db: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      # deploy

After the first deploy, go back to the Clerk dashboard and update the webhook URL to your real Vercel host.

6. Verify

  1. Visit https://<host>/sign-in → Clerk's sign-in form renders.
  2. Sign in with a user whose publicMetadata.role is set. Land on /.
  3. Visit /finances → Treasury KPIs render (likely 0s on a fresh DB).
  4. Visit /merchants → empty list, but the "New merchant" CTA works.
  5. Visit /sign-in while 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:

  1. Provision Upstash Redis or Railway Redis. Copy REDIS_URL.
  2. Set the env var on both Vercel (for the producer side) and the worker host.
  3. Deploy workers/*.ts to Railway/Fly as long-running services. Each worker imports its runOnce() 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:

  1. Get production API credentials for both PSPs.
  2. Replace the stub calls in lib/transactions/create.ts with real SDK calls (signature unchanged — they just need to return { id, status }).
  3. Flip USE_STUBS=false.

9. Common gotchas

  • Prisma client extensions + serverless edge runtime: the ledger immutability extension in lib/db-extensions.ts is Node.js-only. Vercel's default Node runtime is fine; explicitly set runtime: "nodejs" on the Clerk webhook route just to be safe.
  • BigInt JSON serialization: every API response goes through lib/money/serialize.ts:jsonSafe — never JSON.stringify a 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 to new 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.

Vecnet — Build Spec v0.2 · Obsidian Terminal