Appearance
Auth — Clerk + AdminUser
Vecnet's admin platform authenticates through Clerk (replaces the earlier NextAuth v5 + Credentials setup). Every protected route flows through one helper, requireSession(), which keeps the same contract as before so the ~50 route + page call sites didn't have to change.
Identity model
| Component | Role |
|---|---|
| Clerk | Sign-in (hosted), session management, MFA, password reset, account UI |
AdminUser (Prisma) | Vecnet's local mirror — holds role, name, email, audit-log foreign keys |
clerkId column | Bridge — AdminUser.clerkId (@unique) maps Clerk's user_… id to a local row |
AdminUser carries no password. Authoritative columns: id (cuid), clerkId, email, name, role (superadmin | finops | integrations), lastLoginAt, timestamps.
requireSession() — the only entry point
ts
import { auth } from "@clerk/nextjs/server";
import { prisma } from "@/lib/db";
import { redirect } from "next/navigation";
export async function requireSession(): Promise<Session> {
const { userId } = await auth();
if (!userId) redirect("/sign-in");
const admin = await prisma.adminUser.findUnique({ where: { clerkId: userId } });
if (!admin) redirect("/unauthorized");
return { user: { id: admin.id, email: admin.email, name: admin.name, role: admin.role } };
}Every server component and route handler awaits this. The returned user.id is the AdminUser cuid, not the Clerk user id — that's what audit FKs and seed scripts always referenced, and it stays stable across the cutover.
Sync — Clerk → AdminUser
The /api/clerk-webhooks route receives user.created, user.updated, user.deleted events from Clerk. Signatures are verified with svix (CLERK_WEBHOOK_SIGNING_SECRET).
Flow on user.created / user.updated:
- Read
email_addresses[0].email_address,first_name+last_name, andpublic_metadata.role. - If
roleis missing or not in the enum → skip. The Clerk user can sign in butrequireSession()will redirect them to/unauthorizeduntil a superadmin setspublicMetadata.rolein the Clerk dashboard. - Upsert
AdminUserbyclerkId. Existing row'sidis preserved — so foreign keys (settlement payout actor, adjustment author) continue to resolve.
On user.deleted we keep the AdminUser row. Future settlements that reference it remain valid. A superadmin can purge later via SQL if needed.
Dev bootstrap without ngrok
For local dev you don't need to expose the webhook. The seed script prints a banner pointing you at scripts/sim-admin-user.ts:
bash
# Create a Clerk user in the dashboard, copy the user_id, then:
npx tsx scripts/sim-admin-user.ts user_2abc123 superadmin yuyo@vecnet.local "Yuyo"This inserts the AdminUser row directly with the role you pass. Same effect as the webhook firing in production.
Sign-in / sign-out UI
| Page | What it is |
|---|---|
/sign-in | Clerk's hosted <SignIn /> component rendered inside the Vecnet shell |
/unauthorized | Friendly "your role isn't set yet" page |
| Sidebar sign-out | Clerk's <SignOutButton> wrapping the existing icon button |
The old /login server-action page, lib/auth.ts, lib/auth.config.ts, and app/api/auth/[...nextauth]/route.ts are gone.
Permission gates (unchanged)
lib/permissions.ts exports pure functions over AdminRole. They're imported by every server component and don't touch Prisma or Clerk — import-safe from client components.
Postgres-level RLS (future)
Today access control lives entirely in app code. The AdminUser table + roles are the model; Postgres has no row-level security policies yet. When Vecnet hosts merchant-facing surfaces (not yet — admin app only) in shared Postgres, we'll add RLS keyed off session.user.id. The Clerk integration was designed with this future in mind: the JWT issued by Clerk can be forwarded to Supabase / Postgres via auth.jwt() claims for RLS, but that wiring is out of scope for the admin build.
Environment
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_
CLERK_SECRET_KEY=sk_test_
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-inUser provisions all three keys from the Clerk dashboard (free tier suffices for the admin platform).