
To implement authentication in 2026, do not build it yourself. Pick a managed provider that matches your product shape (Clerk for B2C polish, Auth0 for enterprise compliance, WorkOS for B2B SSO, Better Auth if you must own the data), then ship the standard pattern: an httpOnly cookie session, middleware-checked routes, role and permission split, passkey-first login, and MFA enforced on admin paths.
That is the whole post in 80 words. Everything below is the playbook for getting it right.
Custom auth almost always loses. The math has shifted hard in the last three years: provider quality is the highest it has ever been, free tiers are generous, and a single missed CVE costs more than five years of Auth0 bills. If you are building a public product, you are paying a managed provider. The only question is which one.
The honest counterargument is that auth code "looks easy." A bcrypt.hash, a JWT signed with a secret, a cookie. Six hours and you ship. The reason every senior engineer who has done this twice now picks a provider is that the work is not the happy path. It is account recovery, email enumeration mitigation, MFA fallback, audit logs, suspicious-login detection, SCIM provisioning, regional data residency, breached-password screening, and the four-week scramble when SOC 2 asks for a session-revocation report. Providers ship that for $25/month.
The choice in 2026 is mostly settled. Match the provider to what you are building, not to a Twitter argument.
| Provider | Best for | Free tier | Standout strength |
|---|---|---|---|
| Clerk | B2C SaaS, Next.js, indie | 10K MAU free | Pre-built React components, fastest DX |
| Auth0 | Enterprise, regulated, legacy SAML | 25K MAU free | Compliance certifications, broadest IDP support |
| WorkOS | B2B SaaS adding SSO + SCIM | SSO + Directory Sync free to 1M MAU | Enterprise auth without the procurement loop |
| Better Auth | Teams that must own the database | Open source | TypeScript-native, framework-agnostic |
| Supabase Auth | Apps already on Supabase | Free with project | RLS-native, no extra vendor |
Three quick decision rules. If you are a B2C SaaS or a Next.js side project, default to Clerk. We have a longer breakdown in our Clerk review and a head-to-head with Auth0 and NextAuth that gets into the gotchas. If your buyer is an IT admin who will ask for SAML and SCIM, you want WorkOS bolted on or Auth0 from day one (we wrote up the Auth0 vs Cognito tradeoff for AWS shops). If you cannot send user data to a third party for regulatory reasons, Better Auth (the production rename of Lucia v3) is the cleanest open-source path.
The cost question is the part everyone gets wrong, so we did the math in the cost to add user authentication. Spoiler: under 50K MAU, every option is rounding-error money compared to the engineering hours custom auth burns.
Whatever provider you pick, the integration shape is the same. Five components, in order.
The 2026 default is server-issued sessions stored in an httpOnly, Secure, SameSite=Lax cookie. The provider handles the OAuth handshake (PKCE for SPAs, authorization code for server apps), then sets a session cookie scoped to your domain. JWTs still exist inside the system, but they live server-side or inside the cookie, never in localStorage.
The reason is simple: a token in localStorage is readable by every script on the page, including the analytics tag your marketing team added last week. One XSS bug and your entire user base is compromised. An httpOnly cookie cannot be read by JavaScript at all. Every modern provider defaults to this. Do not override it.
The exception is non-cookie clients (mobile native, CLI). For those, store the refresh token in the OS keychain (iOS Keychain, Android Keystore, keytar on desktop), not in app preferences.
Auth lives at the edge of your request pipeline, not inside route handlers. In Next.js, that is middleware.ts. In Express, it is an app.use. In Hono, it is a c.use. The middleware reads the session cookie, validates it with the provider, attaches the user to the request, and either passes through or redirects.
A working Clerk middleware in Next.js 15:
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
const isProtectedRoute = createRouteMatcher(['/dashboard(.*)', '/api/private(.*)'])
const isAdminRoute = createRouteMatcher(['/admin(.*)'])
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) await auth.protect()
if (isAdminRoute(req)) {
await auth.protect((has) => has({ role: 'admin' }))
}
})
export const config = {
matcher: ['/((?!_next|.*\\..*).*)', '/(api|trpc)(.*)'],
}
Twelve lines. The same shape works for Auth0 (withMiddlewareAuthRequired), Better Auth (auth.api.getSession), or WorkOS (authkitMiddleware).
Roles answer "who is this person." Permissions answer "what can they do." Conflate them and you ship a isAdmin boolean that grows into a 14-branch if tree by month six.
The 2026 pattern is RBAC with permissions attached to roles, checked at the route or component boundary:
// route handler
if (!auth.has({ permission: 'org:billing:manage' })) {
return new Response('Forbidden', { status: 403 })
}
Clerk and Auth0 both ship this natively. For Better Auth, the Casbin plugin or a 50-line custom check works. The point is: never check user.role === 'admin' directly in business logic. That couples your code to your auth model and makes future role changes a refactor.
Offer passkeys (WebAuthn) as the primary login. Fall back to email + password. Require TOTP or WebAuthn as a second factor for any user with admin privileges or sensitive data access.
The trick is not making MFA universal at signup (which kills conversion) but using step-up authentication on the routes that need it. When a user hits /admin/billing, the provider re-prompts for MFA even if they are already logged in. Clerk, Auth0, and WorkOS all expose this as a one-line API.
Every provider gives you a session list per user. Surface it in your settings UI ("Active sessions" with browser, location, last active). Let users revoke individual sessions. This is the lowest-effort security feature with the highest user trust payoff, and it costs you a single React component.
These are the patterns we still see in code reviews, in roughly the order of how badly they will hurt you.
process.env.JWT_SECRET. The secret leaks once and every token in your system is forgeable. If you must hand-sign JWTs, use RS256 with key rotation and verify in microservices using the public key only./login or /forgot-password. A single attacker with a residential proxy pool will spray credential-stuffing attacks across your user base in hours. Cloudflare Turnstile, Upstash Ratelimit, or your provider's built-in throttle. Pick one. We cover the broader Postgres query optimization angle elsewhere, but rate-limit data should sit in Redis or the edge, not in your primary DB.Best practices have ROI curves. A few honest cases where you can skip a managed auth provider:
If your product has paying customers, has more than one role, or stores anything regulators care about, you are paying for managed auth. Period.
A working signup-login-session flow on Clerk or Auth0 takes a half-day for an engineer who has done it before. A production-ready setup with MFA, RBAC, audit logs, SSO for one enterprise customer, and password reset flows takes about a week.
Where teams burn time is the second week: the customization that providers technically support but that your engineer has to read three docs pages and a GitHub issue to figure out. SCIM provisioning. Custom OAuth scopes. Multi-tenant org switching. Session-revocation webhooks into your audit log.
This is the kind of work where every Cadence engineer is AI-native by default (vetted on Cursor, Claude Code, and Copilot fluency before they unlock bookings) and ships the integration in days instead of weeks. A senior engineer at $1,500/week is the right tier for the full production rollout. A mid engineer at $1,000/week handles the day-1 setup and the first two enterprise edge cases.
If you want a second opinion on whether your current auth stack is actually production-ready, audit your stack with Ship-or-Skip. It will tell you, in plain language, what to fix before your first SOC 2 audit asks for it.
role for identity, permission for capability checks./login, /signup, /forgot-password. Cloudflare Turnstile or Upstash Ratelimit.Want a sanity check before you go live? Cadence engineers ship managed-auth integrations every week. Book a senior at $1,500/week with a 48-hour free trial, and have a working production-grade auth stack inside a week. No recruiter loop, no contract, replace any week.
Almost never. The honest cases are an internal tool behind a VPN, a single-admin side project, or a throwaway prototype. If you have paying customers or store regulated data, pay for managed auth. The cost is a rounding error against a single breach.
No, but raw JWT-in-localStorage is. Use signed JWTs inside httpOnly cookies, or use opaque session tokens issued by your provider. If you do sign your own JWTs, use RS256 with key rotation, never HS256 with a shared secret in .env.
Use your provider's step-up authentication API. Trigger it on route entry for admin paths, not at login. Clerk's auth.protect() with a factor requirement, Auth0's acr_values parameter, or WorkOS's MFA challenge endpoint all do this in one line.
Yes for B2C, mostly yes for B2B. Offer passkeys first, password second, and TOTP as MFA fallback. The browser support is universal and the user-experience win (no password reset emails) pays for itself in a single quarter.
A working signup-login-session flow takes a half-day. A production-ready setup with MFA, RBAC, audit logs, and SSO for one enterprise customer takes about a week. Add another week if you need SCIM provisioning or multi-tenant org switching.