Every web application needs to track authenticated users across requests. The two dominant approaches are JWT (JSON Web Tokens) and server-side sessions with cookies. Developers argue fiercely about which is "better" — but the truth is that the right choice depends on your architecture. This guide covers both, including the security tradeoffs most tutorials skip.
How JWTs Work
A JWT is a self-contained token: a base64-encoded JSON header + payload + cryptographic signature, concatenated with dots. When a user signs in, the server issues a JWT containing claims like sub (user ID), exp (expiry), and any custom data (roles, org ID). The client stores this token and sends it with every request.
The key property: the server doesn't need to look anything up. It verifies the signature cryptographically and trusts the claims in the payload. This makes JWTs attractive for stateless, horizontally-scaled services.
// A decoded JWT payload
{
"sub": "user_01HXYZ",
"email": "alice@example.com",
"org_id": "org_acme",
"role": "admin",
"iat": 1717660800,
"exp": 1717747200 // 24 hours
}How Server-Side Sessions Work
With sessions, the server generates a random, opaque session ID and stores the actual session data in a database or cache (Redis, Postgres). The client receives only the session ID in a cookie. On every request, the server looks up the session ID to retrieve the user's data.
The critical difference: the server is always the source of truth. Revoking a session is instant — just delete the row from the database.
JWT vs Sessions: The Real Tradeoffs
| Concern | JWT | Server Sessions |
|---|---|---|
| Revocation | Hard — must wait for expiry or maintain a blocklist | Instant — delete the DB row |
| Scalability | Excellent — no DB lookup per request | Requires fast session store (Redis) |
| Payload size | Grows with claims (~200–500 bytes typical) | Always small (session ID only) |
| Offline/edge | Works without DB access | Requires session store access |
| Logout reliability | Unreliable if token is cached | 100% reliable |
| Multi-device revoke | Requires blocklist or short TTL | Trivial — delete all rows for user |
The Security Problem with JWTs Most People Miss
If your JWT has a 24-hour expiry and a user's account is compromised, you cannot invalidate their token. You must wait 24 hours or maintain a revocation list — which adds the same DB lookup you were trying to avoid.
This is why Vaultix-ID uses a hybrid approach: issue RS256 JWTs for fast client-side verification, but validate against a server-side session row on every protected API request. The JWT carries the claims; the database is the authority on whether the session is still valid.
// Vaultix-ID's approach — best of both worlds
// 1. Issue short-lived JWT (1 hour) with session_id claim
// 2. On /me requests: verify JWT signature + check vx_sessions table
// 3. Sign-out: delete the DB row → instant revocation
// 4. SDKs can verify JWT locally without a network call
const { userId, sessionId } = await auth();
// JWT verified cryptographically AND DB session checkedWhere to Store JWTs
Storage location matters enormously for security:
- localStorage / sessionStorage — accessible to JavaScript. Any XSS vulnerability can steal your tokens. Never store JWTs here for auth.
- In-memory — secure against XSS but lost on page refresh, requiring silent re-authentication flows.
- httpOnly cookies — the right answer for most apps. Not accessible to JavaScript, sent automatically with every request, and can be scoped with
SameSiteandSecureflags.
Vaultix-ID stores the session token in an httpOnly, Secure, SameSite=None cookie named vaultix-session. This protects against XSS while supporting cross-domain embedding.
When to Use Each
Use JWTs when:
- You're building stateless microservices that need to verify identity without DB calls
- You have a mobile app that needs to work offline
- You're issuing short-lived access tokens (5–15 minutes) with refresh token rotation
- You need to pass user claims across multiple services (e.g., a service mesh)
Use server sessions when:
- Instant revocation is a hard requirement (financial apps, healthcare, admin panels)
- You need to show users their active sessions and let them log out remotely
- Your app is a traditional monolith and you have Redis or fast Postgres available
Conclusion
Neither approach is universally better. The best production systems — including Vaultix-ID — use short-lived JWTs for performance alongside server-side session validation for revocability. If in doubt, start with httpOnly cookie sessions (simpler, safer) and add JWT optimisations only when you have a concrete scaling problem.