Overview

How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol.

Voltro's auth story ships as a plugin: @voltro/plugin-auth. It's server-side primitives + client-side React glue, no third-party redirect dance, no managed-service dependency.

Built-in (password): password sign-up / sign-in, cookie-based sessions (multi-key rotation + sliding-window auto-renewal), rehash-on-verify, magic-link + password-reset flows, passkeys (WebAuthn), CSRF, session enumeration + revocation, multi-tenant memberships + switch-tenant, the typed Subject, TOTP/MFA enrolment, and a SubjectProvider React context + passkey ceremony helper (from the browser-safe @voltro/plugin-auth/web subpath). The single authRoutesPlugin() mounts every route under /auth.

Pluggable identity: auth is a strategy protocol, not just the password flow. First-party plugins ship for WorkOS, Kinde, Clerk, Auth0, Supabase, and generic OIDC; the shared jwtBearerStrategy covers any other OIDC provider; API-key callers use apiKeyStrategy from @voltro/protocol/apikey; you can write your own. Strategies stack, so you can run password + an external IdP side by side.

The pieces

┌────────────────────────────────────────────────────────────────┐
│  Browser                                                       │
│    POST /auth/signin   ────────┐                               │
│    Cookie: voltro:session=...  │                               │
└───────────────────────────────┬─┘                              │
                                │  Set-Cookie (HttpOnly, Secure) │
                                ▼                                │
┌────────────────────────────────────────────────────────────────┐
│  api app                                                       │
│    handleSignIn → verifyPassword → issueSession                │
│    readSession  → HMAC verify    → Subject                     │
└────────────────────────────────────────────────────────────────┘

                                │  ctx.subject (typed)

┌───────────────────────────────────────────────────────────────┐
│  Every query / mutation / workflow                            │
│    ctx.subject.id       (string on a user; null on anonymous) │
│    ctx.subject.tenantId (string on a user; string|null anon)  │
└───────────────────────────────────────────────────────────────┘

What's in this section

Schema tables

Two tables ship from @voltro/plugin-auth/schema:

import { usersTable, sessionsTable } from '@voltro/plugin-auth/schema'

export const users = usersTable
export const sessions = sessionsTable

postgresUserStore reads + writes the users table only — sessions are stateless signed cookies, so the default flow never touches sessionsTable. It ships for apps that opt into DB-backed session enumeration / "sign out other devices"; the stateless default leaves it empty. Don't redeclare these yourself — extend via additional columns in a sibling table linked by userId, not by modifying these.

Why password is the default (and IdPs are strategies, not the base)

You can run Clerk / WorkOS / Kinde — they're first-party strategies. But the built-in password flow is the default, and external IdPs plug in underneath the framework's own Subject, because:

  • Cookie sovereignty. With the built-in flow your app owns the session — no redirect to a third-party SSO domain, no managed-service dependency for a feature every B2B SaaS needs.
  • Multi-tenant model is yours. Even when an external IdP authenticates the user, tenantId and the typed Subject stay the framework's, not the vendor's. The IdP's claims ride along under metadata.claims; they don't replace the model. See external IdPs.
  • Self-host friendly. Password auth needs nothing external. Reach for an IdP when you want SSO/SCIM/enterprise federation, not because the framework forces a service on you.

Strategies stack, so adopting an IdP is additive — keep password sessions working while new sign-ups flow through the IdP, no big-bang cutover.