Before you write a single callback for a multi-tenant Auth.js v5 app, settle two questions: is this a new build or an existing NextAuth codebase, and will sessions be JWTs or database rows? The answers change what you build, and the first one changed in the last year. Auth.js has been through a change of stewardship, its v5 line is still tagged beta on npm, and the maintainers now point new projects elsewhere. A GitHub discussion from February 2026 makes the point from the user side: after more than three years of beta releases, one participant argued, the label no longer tells you much.
The multi-tenant pattern itself is older than any library. On Callidus, the multi-tenant clinic SaaS I built solo, the case study documents JWT claims that carry tenantId and role, with data under tenants/{tenantId}/… in Firestore. That stack is React, Firebase and Stripe Connect, not Auth.js. Auth.js applies the same idea on Next.js: put a claim on the session and check it on the server instead of looking the tenant up on every request.
Is Auth.js Still the Right Choice for a New Multi-Tenant SaaS in 2026?
For a new codebase, probably not: the Auth.js team itself recommends Better Auth for new projects, while existing Auth.js and NextAuth apps keep receiving security and urgent fixes. The 26 September 2025 announcement says Auth.js is now maintained by the Better Auth team, that maintenance continues "for security and urgent issues", and that "if you're starting something new" they recommend Better Auth. The Auth.js migration guide now opens with the same news: the project is part of Better Auth.
Then, on 7 July 2026, Vercel announced it had acquired Better Auth. The post cites more than 4.7 million weekly npm downloads, says the library stays MIT licensed and keeps its name, and that founder Bereket Engida and the core team are joining Vercel. This suggests Better Auth is where the ecosystem's investment is going, though the post makes no promise about Auth.js.
For multi-tenancy specifically, Better Auth's organization plugin lists organizations, members, roles, invitations, teams and an active organization on the session. Auth.js has none of these as first-class features. You would build them yourself. One more correction to a common assumption: Better Auth's docs say it also works without a database, through stateless session management, so a database is not a hard requirement.
If you're migrating an existing NextAuth v4 codebase, none of that forces a rewrite this week. This is a greenfield argument, and the rest of this post shows the Auth.js patterns for teams who stay or who inherit a v5 app.
When staying on Auth.js is still reasonable
There are honest reasons to keep it. You may already run v5 in production, and a rewrite of a working auth layer is a project with its own risk and no new customer value. Your team may know the callback model well, or you may need a provider or adapter combination you have already verified. In those cases the practical step is to pin your versions, follow the security fixes, and keep your tenant logic in your own code so that a later move to another library only replaces the session plumbing. That last point is a design choice I'd make regardless: if the tenantId lookup and the enforcement check live in your modules and not inside callbacks, the library becomes a replaceable detail.
How Do You Attach tenantId and Role to an Auth.js v5 Session?
You copy the values from the user onto the token in a jwt() callback, then from the token onto the session in a session() callback. Auth.js's role-based access control guide shows exactly this shape for a role, and a tenantId follows the same path:
callbacks: {
jwt({ token, user }) {
if (user) {
token.role = user.role
token.tenantId = user.tenantId
}
return token
},
session({ session, token }) {
session.user.role = token.role
session.user.tenantId = token.tenantId
return session
},
}
The guide adds the catch that matters for B2B: with this strategy, "if you want to update the role, the user needs to be forced to sign in again." The tenantId line is my adaptation of the documented pattern. You also need to extend the session types, or TypeScript will reject session.user.tenantId.
What v5 changes around the session
The migration guide says one auth() function replaces getServerSession, getToken and the withAuth wrapper, and works in Server Components, route handlers and Edge functions. The next-auth/middleware import is deprecated: you export auth from auth.ts and use it in proxy.ts on Next.js 16, or middleware.ts on older versions. The minimum Next.js version is 14, and environment variables move from the NEXTAUTH_ prefix to AUTH_.
Should a B2B Tenant Model Use JWT or Database Sessions?
Use database sessions if removing a user must cut access immediately, and JWT sessions only if a delay until sign-in or token expiry is acceptable for your product. Auth.js's session strategy docs describe the trade-off plainly.
| JWT sessions | Database sessions | |
|---|---|---|
| Storage | Token in an HttpOnly cookie | Session ID in a cookie, data server-side |
| Per-request cost | No database lookup | A database round trip |
| Revocation | Not possible before expiry without a server-side blocklist | Modify or delete server-side at any time |
| Role change | User must sign in again | Applies when you update the row |
| Edge runtime | Fits | Many adapters are not yet Edge-compatible |
| Default | Used unless a database is configured | Used once you configure an adapter |
Consider a hypothetical clinic owner who removes a practitioner from the account. With JWT sessions and no blocklist, that practitioner's token stays valid until it expires, and nothing on your server can reach into it. For per-seat B2B pricing, where admins expect removal to take effect at once, that is usually the deciding row. For a read-heavy internal tool with little churn, the lower per-request cost can win.
The same page says Auth.js mitigates the JWT limitation with shorter expiration times and token rotation, and that database sessions can be modified at any time server-side, which makes features like "sign out everywhere" and concurrent-login limits easier. Shortening the token lifetime narrows the window but does not close it, so decide how long a removed user may stay signed in and write that number into your requirements. It is easier to defend a stated window than an accidental one.
Where Does Tenant Isolation Actually Live?
In your own code: Auth.js gives you a place to put a tenantId claim, and you write the lookup that decides which tenant a signed-in user belongs to and the check that enforces it. The multi-tenant SaaS pattern lives in that lookup and check, whichever library you choose.
Check the session where the data is read
The Auth.js docs on protecting routes are direct: the proxy should not be your only authorization layer, and the session should be verified as close to your data fetching as possible. So check session.user.tenantId inside every server action and route handler that reads tenant data, and treat the proxy as a convenience redirect. A habit that pays off is one helper, for example requireTenant(session), that every handler calls before it touches data, so there is one place to audit and change and not twenty inline checks scattered through the codebase. That is my recommendation, and it matches how the Callidus case study describes its own approach, where access helpers replaced inline role checks so that changes to the access matrix ripple through consistently. When a new route forgets the helper, a code review or a simple test that hits the route as another tenant will catch it.
Split the config when your adapter is not Edge-safe
The migration guide's answer for adapters that don't run on the Edge is two files: auth.config.ts with edge-compatible options, imported by proxy.ts, and auth.ts that adds the adapter for everything else. Do this the moment you add a database adapter.
The Supabase adapter and your own tables
The Supabase adapter page has you create a separate next_auth schema, expose it in the Supabase API settings, and supply a SUPABASE_SERVICE_ROLE_KEY. Its example RLS policy uses next_auth.uid() on Auth.js's own tables. My reading is that your app tables need their own policies against a tenant_id column you populate; the page does not say otherwise, and it does not cover tenants. Keep the service role key on the server only. If you're weighing this against Supabase's built-in auth, the Next.js and Supabase auth pattern covers that route, and running both in one app means two sessions to keep consistent, so pick one.
Organizations and SSO
Adding an SSO provider is configuration, and deciding which organization a new sign-in belongs to is application code. You will need your own organizations and memberships tables and an invite flow, because Auth.js ships none of them. For the wider design, my multi-tenant SaaS architecture guide covers the data side, and the SaaS MVP stack puts auth in context as one decision among several.
A setup order that catches mistakes early
- Choose JWT or database sessions first, using the table above, because switching later touches every route that reads the session.
- Stand up the adapter against a throwaway database and confirm its schema migrates.
- Write the
jwt()andsession()callbacks, and test with at least two seeded tenants so a scoping bug shows up in a test. - Split
auth.config.tsfromauth.tswhen the proxy enters the picture. - Gate every server action and route handler on
session.user.tenantId. - Demote a test user and time how long their access lasts, then decide whether that window fits your pricing tier.
Pick your session strategy this week, and tell me whether the removal scenario in the table changed your answer.



