Skip to content
Tech Stack26 July 2026 · 8 min read

Auth.js v5 for B2B Multi-Tenant SaaS in 2026

The tenantId/role callback pattern for Auth.js v5 multi-tenant SaaS still works, but the ecosystem just steered hard toward Better Auth. Here is what changed, and why it matters.

Auth.js v5 for B2B Multi-Tenant SaaS in 2026

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?

Four worn brass toggle switches on a wall panel, one mid-replaced by a sleek new switch with rerouted wiring hanging loose below

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?

A blank brass key clamped in a small vise mid-engraving, with metal shavings scattered across the worktable

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

Two weathered apartment doors side by side, one with a plain brass keyhole and the other fitted with an intercom buzzer panel

| | 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

  1. 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.
  2. Stand up the adapter and confirm the next_auth schema migrates cleanly against a throwaway database, kept separate from your app schema.
  3. Write the jwt() and session() callbacks to stamp tenantId and role, testing against at least two seeded tenants so a copy-paste bug in tenant scoping shows up immediately instead of in production.
  4. Split auth.config.ts from auth.ts the moment you touch middleware, forcing JWT there even if the rest of the app uses database sessions.
  5. 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.
  6. 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.

Free resource

Free SaaS MVP Scope Template

A Notion document with the full feature checklist, MVP vs. nice-to-have table, pre-build questions, and cost signals — so you walk into any developer call knowing exactly what to ask for.

Get the template →
DL

Dusko Licanin

Full-Stack Developer · Banja Luka, Bosnia

Full-stack developer shipping SaaS MVPs, web apps, and mobile apps using AI-augmented workflows — without agency coordination overhead. Live portfolio: BookBed, Callidus, Pizzeria Bestek.

Frequently Asked Questions

Does NextAuth support multi-tenant SaaS out of the box?

No, Auth.js has no built-in multi-tenant concept, and you build tenant isolation yourself on top of its session callbacks. The pattern is the same jwt()/session() combo [the RBAC guide documents](https://authjs.dev/guides/role-based-access-control), just storing a tenantId value instead of a role, plus your own lookup deciding which tenant a freshly-authenticated user belongs to. Nothing in the library itself enforces that a user assigned to tenant A can't be handed tenant B's data — that check lives entirely in your route handlers and server actions.

Does Auth.js have an organization or team feature like Clerk or WorkOS?

No, Auth.js has no first-class organization or team model, only the raw primitives you assemble one yourself from. You still need your own `organizations` and `memberships` tables, an invite-token flow for adding members, and a lookup that resolves a signed-in user to the tenant they're currently acting inside — Auth.js only gives you a callback to stamp the result of that lookup onto the session.

Should I use JWT or database sessions for a SaaS session?

Use database sessions if you need to revoke a user's access immediately, and JWT sessions only if instant revocation never matters for your pricing tier. Per-seat B2B pricing almost always needs the former, since admins expect a removed teammate to lose access the moment they're removed, not at their next login. A JWT-only setup trades that guarantee for lower per-request cost and Edge-runtime compatibility, which is a fine trade for a low-churn internal tool and a bad one for anything billed per seat.

Does the Auth.js Supabase adapter handle Row Level Security automatically?

No, the Supabase adapter only manages Auth.js's own user and session tables and has nothing to do with your app's Row Level Security policies. You still write and maintain your own [RLS policies](/glossary/row-level-security) against a `tenant_id` column you own, exactly as if the adapter weren't installed at all. The two systems don't talk to each other, and treating the adapter as a security boundary for your own tables is the mistake worth avoiding here.

Is Auth.js still actively maintained in 2026?

Yes, but only for security patches and urgent fixes, since the Better Auth team took over maintenance in 2025 and now steers new projects toward Better Auth instead. [Vercel's acquisition of Better Auth in July 2026](https://vercel.com/blog/vercel-acquires-better-auth) makes that direction more permanent, not less, so treat Auth.js as stable-for-migrations rather than a first choice for a brand-new codebase.