Auth.js v5 for B2B Multi-Tenant SaaS in 2026
I keep getting asked whether Auth.js v5 is still the safe default for a new B2B SaaS build, and my answer keeps changing shape depending on what week it is. Three years of beta tags will do that — one maintainer said as much in February 2026, pointing out that a library running in production everywhere is a strange thing to keep calling beta. On Callidus, the multi-tenant clinic SaaS I built solo, the tenant boundary lives in Firebase custom claims: tenantId and role baked directly into the JWT the client already carries. Auth.js solves the same shape of problem for teams on Next.js and Postgres, using the same idea: a claim on the token instead of a lookup on every request. The politics around the library, though, changed twice in the last year, and that matters more than any code sample below. This post walks through the actual v5 patterns first, because you'll need them whether you stay on Auth.js or migrate off it eventually.
What Does Auth.js v5 Actually Change for a Multi-Tenant Session?

It replaces four separate session-reading functions with one universal auth() call and moves your whole config to a root auth.ts file. Auth.js's own migration guide walks through dropping getServerSession, getToken, withAuth, and the old middleware import in favor of a single call that works almost everywhere server-side. Import paths change too: next-auth/middleware is gone, replaced in Next.js 16+ by a proxy file that wraps your exported auth function directly. None of that is exotic on its own. Doing it with a tenant_id sitting on every token is where it gets interesting.
How Do You Attach tenant_id and Role to the JWT?

Two callbacks named jwt() and session() copy a tenantId or role value from the user object onto the token, then onto the session object. The official RBAC guide's pattern is jwt({ token, user }) { if (user) token.role = user.role; return token }, followed by session({ session, token }) { session.user.role = token.role; return session }. Four lines. That's the whole pattern, and swapping role for tenantId gets you the entire multi-tenant session shape. The part the docs bury: on pure JWT sessions with no adapter, changing a user's role means forcing them to sign in again, because the token is the only place that role lives and nothing server-side can reach in and edit it mid-session.
JWT vs Database Sessions for a B2B Tenant Model

| | JWT sessions | Database sessions | |---|---|---| | Per-request cost | None — decoded from cookie | One DB round-trip | | Revocation | Not possible without a separate blocklist | Immediate — delete the row | | Works at the Edge | Yes | No, needs an adapter | | Role or tenant change takes effect | Next sign-in only | Next request | | Best fit | Read-heavy apps, low seat-churn | Per-seat pricing with admin-driven demotions |
That revocation row should decide it for most B2B tools, not the first one. Tuesday afternoon: a clinic owner emails asking why a practitioner they fired that morning can still open patient records from their phone. You've configured an SSO provider in ten minutes before — has your tenant-isolation logic ever been that fast to verify? A stateless JWT with no revocation path turns that into a support ticket instead of a five-minute fix.
Is Auth.js Still the Right Bet for a New SaaS in 2026?
For a brand-new codebase in 2026, probably not, and that verdict has nothing to do with the quality of the code itself. In September 2025, the Auth.js project announced it's now maintained by the Better Auth team — maintenance mode, security and urgent fixes only, no new features, with its own maintainers pointing new projects at Better Auth instead. Then on July 7, 2026, Vercel acquired Better Auth outright, with founder Bereket Engida and the whole core team joining Vercel to build agent-identity features on top of it. Auth.js isn't dying. It's just not where new investment goes anymore, and a library in that state is an odd foundation to pour a five-year SaaS onto.
Actually — that's too clean a verdict. If you're migrating an existing NextAuth v4 codebase, none of this changes your week. The callback patterns above still work, the package still ships security patches, and rewriting a working auth layer onto Better Auth mid-migration is its own multi-week project you didn't plan for. This is a greenfield-only argument.
There's a real technical reason greenfield teams get steered toward Better Auth, and it has nothing to do with branding. Better Auth requires a database connection from day one; there's no stateless-JWT-only mode the way Auth.js supports. That's a real constraint if you were hoping to skip a database until you had paying customers, but it also means the revocation problem in the table above mostly disappears by default, since sessions live server-side from the start.
The Edge Runtime Trap Nobody Warns You About
Middleware in Next.js runs on the Edge runtime, and the Edge runtime cannot open the TCP socket your database adapter needs. Ship a database-session config straight into your middleware file and you get an error about a missing Node tty module or a crash reading process.version, and neither one mentions Auth.js by name. The fix Auth.js documents is splitting config in two: an edge-safe auth.config.ts that forces the JWT strategy and holds no adapter, imported only by middleware, plus a full auth.ts with your adapter for everywhere else. You've probably already hit this if you tried the "just add the adapter" tutorial path first. Everyone does, once.
Wiring the Supabase Adapter Without Breaking Tenant Isolation
Install @supabase/supabase-js and @auth/supabase-adapter, then configure it with nothing more than your database URL and a secret. The adapter's own reference docs confirm it writes Auth.js's own user and session tables into a separate next_auth Postgres schema, not your app's public schema, which matters because those tables never collide with your own tenants or memberships tables. Row Level Security on your app schema stays a completely separate concern from anything the adapter manages, since the adapter never touches your schema at all. Your RLS policies still need to check a tenant_id column you own and populate yourself; the adapter has no opinion on it.
This pairs naturally with the Next.js + Supabase Auth server-side pattern if you're choosing between Supabase's own auth and bolting Auth.js on top of the same database — they solve overlapping problems, and you generally pick one, not both. Running both at once means two separate JWTs, two separate secrets, and two places a session can quietly expire out of sync with the other. Pick one for the whole app, even if it means migrating existing users off whichever one loses.
SSO Providers Are the Easy Part
Adding an SSO provider to Auth.js is a config object with a client ID and a secret. Deciding which organization a freshly-authenticated user belongs to, and refusing to let them read another tenant's data by mistake, is application code Auth.js never writes for you.
Organization Boundaries Live in Your Lookup, Not the Config File
You're the one building the mapping from an email domain or an invite token to a tenant_id, then stamping that onto the JWT in the callback above, then checking it on every server action and route handler that touches tenant data. The multi-tenant SaaS pattern lives in that lookup and that check, not in the auth library's config file, no matter which auth library you pick. Auth.js gives you a place to put the claim. It never tells you which tenant a user belongs to, and it was never going to.
A Realistic Setup Order for a New Auth.js v5 Multi-Tenant App
- Decide JWT or database sessions first, based on the revocation table above, before writing a single callback. Retrofitting the other strategy later touches every route that reads the session.
- Stand up the adapter and confirm the
next_authschema migrates cleanly against a throwaway database, kept separate from your app schema. - Write the
jwt()andsession()callbacks to stamptenantIdandrole, testing against at least two seeded tenants so a copy-paste bug in tenant scoping shows up immediately instead of in production. - Split
auth.config.tsfromauth.tsthe moment you touch middleware, forcing JWT there even if the rest of the app uses database sessions. - Gate every server action and route handler on
session.user.tenantId, and treat pages as the least important surface to check. An unguarded API route is the actual breach. - Load-test a role demotion end-to-end, confirming exactly how long a revoked user keeps working, and decide out loud whether that window is acceptable for your pricing tier.
Why This Whole Post Assumes You Already Have a Tenant Model
Because Auth.js configuration is the smallest part of shipping B2B SaaS auth, and treating it as the hard part is how teams end up with a beautifully configured callback and a tenant-isolation bug three weeks into production. The stack matters less than people assume here. The SaaS MVP stack I recommend treats auth as one decision among several, not the centerpiece, because it isn't the centerpiece.
Pick your session strategy this week if you haven't already, and tell me whether the revocation row in that table changed your answer.
