JWT vs Session Auth for SaaS: Which Should You Use in 2026?
JWT vs session authentication for SaaS — revocation, scaling, CSRF, and refresh tokens compared, with a clear recommendation and working code.
Every SaaS hits this fork early: when a user logs in, how do you remember them on the next request? The JWT vs session authentication debate has been argued to death, usually badly, with people treating it like a religious choice. It isn’t. They’re two different mechanisms with different tradeoffs, and the right answer depends on what you’re building. This post lays out the real differences — revocation, scaling, CSRF, refresh tokens — and gives a concrete 2026 recommendation for indie SaaS.
The fundamental difference
Session authentication is stateful. On login, your server creates a session record (in a database, Redis, or memory), generates a random session ID, and sends it to the browser as an HTTP-only cookie. On every request, the browser sends the cookie back, your server looks up the session, and knows who the user is. The source of truth lives on your server.
JWT (JSON Web Token) authentication is stateless. On login, your server signs a token containing the user’s claims (ID, role, expiry) with a secret. The client stores it and sends it on each request, usually in an Authorization: Bearer header. Your server verifies the signature and trusts the claims inside — no lookup required. The token is the proof.
That single difference — server lookup vs self-contained token — drives every tradeoff that follows.
Revocation: the JWT Achilles heel
This is the one that bites people in production. With sessions, logging a user out is trivial: delete the session row. Instant. Ban a user, force a logout after a password change, kill a stolen session — all immediate, because every request checks the server.
A JWT, once signed, is valid until it expires. You cannot un-sign it. If a token is stolen, or a user is banned, or they change their password, that token keeps working until expiry. The “solutions” all reintroduce server state:
- Short expiry + refresh tokens — keep access tokens to 15 minutes so a stolen one dies fast.
- A token blacklist — check a denylist on every request, which is… a database lookup, which is the stateful thing you used JWTs to avoid.
So “stateless JWT auth” usually becomes “mostly stateless with a stateful refresh/revocation layer.” Be honest with yourself about that before choosing it for revocation reasons.
Refresh tokens (the part everyone underestimates)
Doing JWTs properly means a two-token system:
- A short-lived access token (15–30 min) sent on every request.
- A long-lived refresh token (days/weeks) used only to mint new access tokens.
// On login: issue both
const accessToken = jwt.sign(
{ sub: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = crypto.randomUUID();
await db.refreshTokens.create({
token: refreshToken,
userId: user.id,
expiresAt: addDays(new Date(), 30),
});
Notice the refresh token is stored server-side — so you can revoke it, and revoking it cuts off the user once their current access token expires. You also want refresh token rotation: issue a new refresh token each time one is used and invalidate the old one, so a stolen refresh token has a short useful life and reuse is detectable. This is real work, and getting it wrong (e.g., leaving refresh tokens in localStorage, no rotation) creates a worse security posture than plain sessions.
Scaling: the JWT selling point
The classic JWT pitch is horizontal scaling. With sessions, every server needs access to the session store, so you run shared Redis or a sticky load balancer. With stateless JWTs, any server can verify a token with just the secret — no shared store, no lookup, easy to scale across regions and microservices.
This is real, but for most indie SaaS it’s a solution to a problem you don’t have yet. A single Redis instance handles enormous session volume. Unless you’re running many services that each need to authenticate independently, the scaling advantage is theoretical at your stage.
CSRF and where tokens live
Storage location is a security decision, not a detail:
- Session cookies (HTTP-only,
SameSite=Lax) are immune to XSS theft because JavaScript can’t read them, but they’re sent automatically, so you need CSRF protection (SameSiteplus a CSRF token for state-changing requests). - JWT in
localStorageis convenient and not auto-sent (no CSRF surface), but it’s readable by any JavaScript — one XSS bug and the token is exfiltrated. - JWT in an HTTP-only cookie gets you XSS protection but reintroduces CSRF concerns, and you’ve now coupled the token to the cookie the way a session would be.
There’s no free lunch. The most defensible setup for a browser-first app is HTTP-only cookies regardless of whether the value inside is a session ID or a JWT.
Don’t want to build any of this? Beag handles authentication for micro-SaaS — secure sessions, login, and password flows — alongside Stripe payments, so you skip the token-vs-session plumbing entirely and ship the product. If you’re weighing that kind of hosted membership layer, we compared Memberstack vs Outseta vs Beag head to head.
The 2026 recommendation
The honest consensus this year: there’s no universal winner, but there are clear defaults.
Use sessions (HTTP-only cookies, store in Redis or your database) when:
- You’re building a first-party web app — a dashboard, a server-rendered site, an SPA you control.
- You want instant logout and easy bans.
- Your user base is bounded (i.e., almost every indie SaaS).
For this, server sessions are the calm, correct default. Revocation is free, the security model is simple, and you avoid an entire class of token-handling bugs.
Use JWTs when:
- You’re building an API consumed by mobile apps or third-party clients.
- You have a microservices architecture where services authenticate requests independently.
- You’re prepared to implement short expiry, refresh token rotation, and a revocation strategy properly.
Use both — which many production teams do. Server sessions for the web app (instant logout, CSRF via SameSite), and short-lived JWTs plus server-stored refresh tokens for the mobile/API surface. Because the refresh token is a server record, you keep revocation even on the JWT side.
The trap to avoid
The worst outcome is choosing JWTs because they sound modern, storing them in localStorage, never implementing refresh or rotation, and discovering you can’t log users out. That’s strictly worse than the session approach you skipped. If you reach for JWTs, commit to the full lifecycle — or use sessions and move on to building features.
Wrapping up
Sessions are stateful, instantly revocable, and the right default for first-party web SaaS. JWTs are stateless and scale across services, but revocation and refresh tokens are non-negotiable work you take on. For most indie hackers, server sessions in Redis with HTTP-only cookies is the boring, correct choice — reach for JWTs only when an API or mobile client genuinely needs them.
Want to skip authentication entirely and get straight to your product? Beag adds auth and Stripe payments to your MVP in minutes →. If you’ve decided on tokens, our OAuth vs magic link guide covers how users actually log in.
Ready to Make Money From Your SaaS?
Turn your SaaS into cash with Beag.io. Get started now!
Start 7-day free trial →