Skip to content

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

ComponentRole
ClerkSign-in (hosted), session management, MFA, password reset, account UI
AdminUser (Prisma)Vecnet's local mirror — holds role, name, email, audit-log foreign keys
clerkId columnBridge — 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:

  1. Read email_addresses[0].email_address, first_name + last_name, and public_metadata.role.
  2. If role is missing or not in the enum → skip. The Clerk user can sign in but requireSession() will redirect them to /unauthorized until a superadmin sets publicMetadata.role in the Clerk dashboard.
  3. Upsert AdminUser by clerkId. Existing row's id is 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

PageWhat it is
/sign-inClerk's hosted <SignIn /> component rendered inside the Vecnet shell
/unauthorizedFriendly "your role isn't set yet" page
Sidebar sign-outClerk'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-in

User provisions all three keys from the Clerk dashboard (free tier suffices for the admin platform).

Vecnet — Build Spec v0.2 · Obsidian Terminal