Business outcome. Product teams launch B2B SaaS without rebuilding the same plumbing every time. Auth, workspaces, role-based access, Stripe billing, and audit-grade tenant isolation are wired up end-to-end on day one, so engineering time goes into the actual product. The boring-but-load-bearing parts — dropped webhooks, last-admin guards, edge-runtime session validation — are already handled, with documented failure modes.
Built as the foundation B2B SaaS organizations launch on top of, not under. The interesting work isn't the features list; it's the failure modes that take SaaS products down in production — a dropped Stripe webhook, an admin who removes themselves, a session that has to validate at the edge before pg or bcrypt are reachable.
Pipeline
What's built
- Email/password auth with bcrypt and JWT sessions; edge-compatible middleware protects
/dashboard/*. - Multi-tenant by design. Every server query is scoped by an active-organization context derived from a signed cookie and re-validated as a real membership on every request. Org IDs are never accepted from the client.
- RBAC with
admin/memberand acan(role, permission)helper. - Member invites, role changes, removal with a transactional last-admin guard so a workspace can never become admin-less.
- Stripe-billed subscriptions per workspace. Checkout, Customer Portal, signature-verified webhooks, "Sync from Stripe" recovery for missed events. Stripe is the source of truth: duplicate-subscription guards and recovery paths derive state by asking Stripe, never the local DB.
- Edge-safe middleware split.
lib/auth.config.tsis the edge-safe shared config used by the proxy (wherepgandbcryptaren't available);lib/auth.tsextends it with the credentials provider. Same callbacks, same JWT, two execution environments. - Settings: display name, password change, workspace rename, danger-zone deletion that cancels the Stripe sub on the way out.
- EU-region deployable out of the box. Neon's eu-central regions and Vercel's fra1 functions cover the data-residency clauses most B2B buyers want; nothing in the hot path is pinned to a US region.
Tradeoffs
- Email/password + bcrypt, not magic links or passkeys. Boring, portable, deployable without a third-party identity provider. The Auth.js v5 wiring leaves a clean slot for OAuth or WebAuthn the first time a customer asks.
- Stripe is the source of truth, not the local DB. "Sync from Stripe" is one API call per recovery: cheap insurance against dropped webhooks and the only honest answer to "what does this customer actually have access to right now". The DB is a cache, treated as one.
- Edge-safe config split.
lib/auth.config.tsexists becausepgandbcryptcan't bundle on the edge runtime; duplicating one config file is the cost of running auth in middleware before the heavy modules load.