Every app that has users has an authentication problem: how does the server know that the person making a request is who they claim to be — without asking them to log in on every single request?
Session cookies solve this with a server-side lookup table. JWTs solve it differently — and in a way that scales better for modern mobile backends.
The Session Cookie Problem
Traditional session-based auth works like this: after login, the server generates a random session ID, stores it in a database, and sends it to the client as a cookie. On every request, the client sends the cookie back, the server looks it up in the database, and — if found — considers the user authenticated.
This works, but it has a cost: every authenticated request requires a database read. At scale, your auth database becomes a bottleneck. Worse, if you have multiple servers, they all need access to the same session store — adding infrastructure complexity.
How JWTs Work
A JWT (JSON Web Token) takes a different approach: instead of storing session state on the server, it encodes it in the token itself — and cryptographically signs it so the server can verify it without a database lookup.
A JWT has three parts, separated by dots:
- Header — the algorithm used for signing (e.g., HS256)
- Payload — the claims: user ID, expiry time, any other data you include
- Signature — the header + payload, signed with a secret key
When the server receives a request, it verifies the signature using the secret key. If the signature is valid and the token hasn't expired, the user is authenticated — no database lookup required.
The signature is what prevents tampering. If an attacker modifies the payload (say, changing their user ID to someone else's), the signature no longer matches — the server rejects it.
What We Put in the Payload
In SafaiPay's Go backend, our JWT payload contained:
user_id— the user's database IDphone— the verified phone numbertoken_version— an integer for revocation (explained below)exp— expiry timestamp (7 days from issuance)iat— issued-at timestamp
We kept the payload small — only what we needed on every request. Avoid putting sensitive data (passwords, full PII) in the payload; it's Base64-encoded, not encrypted, and can be decoded by anyone with the token.
The Revocation Problem
JWTs have a well-known weakness: they can't be revoked before expiry. Once issued, a token is valid until its exp claim passes. If a user logs out or you suspect their account is compromised, you can't "cancel" the token — the server will keep accepting it.
We solved this with a token_version field in PostgreSQL. Every user record has a token_version integer (starts at 1). Every JWT we issue includes the current version in its payload.
On each request, after verifying the signature, we check the token's version against the database value. If they match, the token is valid. To invalidate all of a user's tokens — on logout or security reset — we increment token_version in the database. Every existing token immediately fails the version check.
This does require one database read per request — the same cost as session-based auth. But it's a targeted read on a primary key, and it only happens when revocation is needed. Most apps gate this with a Redis cache: cache the version per user ID with a short TTL, and only hit PostgreSQL on a cache miss.
Token Refresh
Short-lived access tokens (15–60 minutes) minimise the damage if a token is stolen. But forcing users to log in every hour is terrible UX.
The standard pattern: issue a short-lived access token and a long-lived refresh token. When the access token expires, the client silently exchanges the refresh token for a new access token — no user interaction. If the refresh token is also expired (or revoked), the user is asked to log in again.
In SafaiPay, we used 7-day access tokens (acceptable for a mobile app where the main risk is device theft, handled by revocation) and skipped the refresh token complexity for the MVP. A production fintech app would use shorter access tokens with refresh.
JWTs don't eliminate the need for a database in auth — they reduce how often you hit it. That's the real benefit.
JWT auth is now the default for mobile backends. Understanding how the signature works, why revocation is hard, and how to solve it with a version field will take you further than any auth library tutorial.