Bridge Training
Auth

Overview

Authentication adapter over Clerk, with a vendor-free surface for apps, a simulated mode and server-side helpers.

@workspace/auth is the only package allowed to import Clerk. Apps mount a provider, read auth through hooks and types that never mention the vendor, and the API verifies tokens and webhooks through adapters that implement the training ports.

The package covers three runtimes:

RuntimeEntry pointsUsed by
Browser, Vite + React@workspace/auth, /clerk, /simulated, /config, /impersonation-handoff, /styles.cssapps/app
Browser + server, Next.js/nextjs, /server/nextjsapps/admin
Node/server, /server/testing, /testing/playwrightapps/api, e2e suites

Architecture

@workspace/auth
  types.ts                 AuthPort, AuthSession, AuthComponents, ImpersonationState
  hooks/                   Store-backed hooks (useAuthPort, useAuthSession, useImpersonation…)
  internal/store.ts        Zustand vanilla store: the single source of auth state
  internal/clerk/          Clerk provider, sync to the store, localization
  internal/simulated/      Vendor-free provider driven by the host
  internal/impersonation*  Start/stop actions and per-tab persistence
  impersonation-handoff.ts URL-fragment handoff from the admin to the app
  config.ts                parseAuthConfig (zod)
  avatar-file.ts           Avatar validation and AvatarUpdateError
  nextjs/                  Next.js client provider and hooks
  server/                  Node adapters: token verification, webhooks, admin API, session tokens
  testing/                 Playwright helpers and Clerk test-user pool

The store in the middle

Every client hook reads a Zustand vanilla store that holds userId, isLoaded, isSignedIn, the AuthSession, the active impersonation and the signOut function. Two providers write into it:

  • createClerkAuth mounts Clerk and syncs its hooks into the store during render, so consumers see fresh state on their first render and never get a flash redirect on reload.
  • createSimulatedAuth loads the store from a scripted state without any vendor code. The dev scenarios and component tests use it.

Both expose the same AuthComponents shape, so the host is written once:

import type { AuthComponents } from '@workspace/auth';

async function loadClerkAuth(): Promise<AuthComponents> {
  const { createClerkAuth } = await import('@workspace/auth/clerk');
  return createClerkAuth({
    publishableKey: getEnv().auth.publishableKey,
    useLanguage: useAppLanguage,
    signInUrl: ROUTES.auth.signIn,
    signUpUrl: ROUTES.auth.signUp,
    afterAuthUrl: '/',
  });
}

const auth = shell?.auth ?? (await loadClerkAuth());
const AuthProvider = auth.Provider;

<AuthProvider pending={<AppLoading />}>
  <App />
</AuthProvider>

The provider suspends its children until auth has loaded, keeping pending visible. The sign-in and sign-up routes render auth.SignIn and auth.SignUp.

Design principles

  • One vendor import site: Clerk types and hooks never leave the package. Apps type against AuthPort, AuthSession and AuthComponents.
  • Stable references: the store absorbs Clerk's referential instability. useAuthPort only re-renders on identity, loading or impersonation changes, never on a token refresh.
  • Effective identity: getUserId() returns the impersonated subject while an impersonation is active, getActorId() always returns the signed-in user. See Impersonation.
  • Roles live in the database: the server helpers read a role from Clerk public metadata only to seed an invited account. The accounts row stays the source of truth afterwards.
  • Vanilla config: the package takes a publishableKey, apps map their env to it with parseAuthConfig.

Quick start

Read auth state

import { useAuthPort, useAuthSession } from '@workspace/auth';

const auth = useAuthPort();
auth.isAuthenticated();   // boolean
auth.getUserId();         // effective user id, or null

const session = useAuthSession();
const token = await session?.getToken();   // bearer for the API

Verify a bearer on the API

import { verifyToken } from '@workspace/auth/server';

const payload = await verifyToken(token, { secretKey: env.CLERK_SECRET_KEY });
// { userId, sessionId, email }

Protect a Next.js app

import { createAuthProxy } from '@workspace/auth/server/nextjs';

export default createAuthProxy({
  publicRoutes: ['/sign-in(.*)', '/api(.*)'],
  signInUrl: '/sign-in',
});

Exports

PathDescription
@workspace/authStore-backed hooks, AuthPort and friends, avatar validation
@workspace/auth/clerkcreateClerkAuth, password hooks (loads Clerk)
@workspace/auth/simulatedcreateSimulatedAuth, useSimulatedAuth, SimulatedAuthState
@workspace/auth/typesThe shared types alone, no runtime
@workspace/auth/configauthConfigSchema, parseAuthConfig
@workspace/auth/impersonation-handoffBuild and parse the admin → app handoff URL
@workspace/auth/styles.cssTailwind @source directive for the package classes
@workspace/auth/nextjsNext.js client: AuthProvider, SignIn, session hooks
@workspace/auth/serverNode adapters: verifyToken, webhook verifier and handlers, admin service, HMAC session tokens
@workspace/auth/server/nextjsNext.js server: session reads, createAuthProxy, invitations, role sync
@workspace/auth/server/testingFaker factories for Clerk webhook payloads and signed requests
@workspace/auth/testing/playwrightClerk testing mode and the per-worker user pool

Package structure

avatar-file.ts
clerk.ts
config.ts
deferred.ts
impersonation-handoff.ts
index.ts
known-viewer.ts
nextjs.ts
session-signal.ts
simulated.ts
styles.css
types.ts
sign-in.tsx
sign-up.tsx
auth-profile.ts
index.ts
useAuthProfile.ts
useAuthService.ts
useAuthSession.ts
useAvatarUpdate.ts
useHasPassword.ts
useImpersonation.ts
useImpersonationActions.ts
usePasswordChange.ts
usePasswordReset.ts
auth-gate.tsx
identity-snapshot-of-user.ts
impersonation-actions.ts
impersonation-storage.ts
impersonation.ts
signup-source-metadata.ts
store.ts
auth-sync-provider.tsx
clerk-session.tsx
create-clerk-auth.tsx
localization.ts
mappers.ts
provider.tsx
deferred-auth-provider.tsx
wait-for-token.ts
context.ts
create-simulated-auth.tsx
session.ts
state.ts
use-simulated-auth.ts
hooks.ts
provider.tsx
clerk-accounts-adapter.ts
clerk-identity-provider.ts
index.ts
nextjs.ts
public-metadata-role.ts
session-token.ts
testing.ts
verify-token.ts
webhook-verifier.ts
playwright.ts
provisioning.ts