Research-based overview. This article synthesizes RFC 7519 (the JWT specification), public documentation from Auth0, Supabase, and Clerk, and OWASP guidance on token storage. How we research.

One-sentence definition
A JSON Web Token (JWT) is a compact, URL-safe string defined by RFC 7519 that carries a set of claims — usually identity claims about a logged-in user — signed by an issuer so that any server holding the verification key can trust the contents without contacting a database.

Pronounced “jot” by the people who wrote the spec and “jay-double-you-tee” by everyone else, a JWT is the token format that quietly powers most modern SaaS authentication. When a user signs in to a Next.js app backed by Supabase, Clerk, or Auth.js, the artifact that gets handed back to the browser and replayed on every API call is almost always a JWT. Understanding what is inside one — and what should never be inside one — is the difference between auth that holds up and auth that quietly leaks.

The structure: header.payload.signature

A JWT looks like a long, mildly garbled string with two dots in it. Decoded, it is three pieces:

// A real-shaped JWT (truncated) eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTcyOH0.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk // Split on the dots: header = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 payload = eyJzdWIiOiJ1c2VyXzEyMyIsImV4cCI6MTcyOH0 signature = dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Each segment is base64url-encoded (the URL-safe variant of base64, which swaps +/ for -_ and drops padding). When you base64url-decode the first two segments, you get JSON:

// Header { "alg": "HS256", "typ": "JWT" } // Payload { "sub": "user_123", "exp": 1728000000 }

The header tells the verifier which algorithm was used to sign the token. The payload contains the claims — standardised fields like sub (subject, almost always the user ID), iat (issued at), exp (expires at), iss (issuer), and aud (audience), plus any custom claims the issuer wants to ship along. The signature is computed over base64url(header) + "." + base64url(payload) using the algorithm in the header and a secret or private key only the issuer holds.

Two important consequences fall out of that structure. First, the payload is not encrypted, only encoded. Anyone holding the token can read the claims by base64url-decoding the middle segment — tools like jwt.io will do it for you. Second, the signature is what makes the token trustworthy. If an attacker tampers with even one byte of the payload, the signature stops verifying, and any well-behaved server rejects the token.

Signed vs encrypted: JWS vs JWE

The umbrella term “JWT” covers two cousins from the JOSE (JavaScript Object Signing and Encryption) family of specs: JWS (JSON Web Signature, RFC 7515) and JWE (JSON Web Encryption, RFC 7516). The signed-only variant — JWS — is overwhelmingly the one you encounter in SaaS auth. The payload is readable by anyone, but the signature guarantees integrity.

Encrypted JWTs (JWEs) wrap an encrypted payload inside a different five-segment structure. They are useful when claims contain genuinely sensitive data, but they are rare in practice for one reason: well-designed JWTs do not contain sensitive data. They contain a user ID and a few flags. If you find yourself reaching for JWE, you have probably stuffed something into the payload that should have stayed in the database.

Signing algorithms you will see

The header's alg field identifies how the signature was made. Three families dominate:

Auth0's and Clerk's hosted issuers default to RS256. Supabase Auth historically used HS256 with a project-wide secret and has been migrating projects toward asymmetric signing. Whichever family you use, the rule is the same: never accept tokens whose alg is none. The infamous “alg: none” vulnerability from 2015 is still appearing in audit reports a decade later because some libraries trust the header instead of pinning the algorithm in the verifier.

Why SaaS uses JWTs: stateless auth

The whole point of a signed, self-contained token is that the server does not need to store session state. With a session-cookie design, every authenticated request triggers a database lookup — the cookie holds an opaque session ID, and the server reads the row to figure out which user this is. With a JWT, the user ID is already in the token. The server verifies the signature once (using a key it already has cached), reads sub out of the payload, and proceeds.

For solo SaaS founders, that buys three things:

Access tokens vs refresh tokens

Real-world JWT setups almost always issue two tokens at sign-in:

The split exists because long-lived JWTs are dangerous — once issued, you cannot easily revoke them — and short-lived JWTs without a refresh would log users out every fifteen minutes. The refresh token is typically opaque (not a JWT), stored in a secure HTTP-only cookie, and tied to a server-side record so it can be revoked. Auth0's, Clerk's, and Supabase Auth's docs all describe variations of this pattern.

Common JWT mistakes

JWT vs session cookies (the never-ending debate)

You will read a lot of takes online claiming JWTs are an anti-pattern and you should use session cookies instead. The pragmatic answer is more boring than either side admits: they are not actually opposites. Most modern auth providers issue a JWT inside an HTTP-only cookie, getting the integrity properties of a signed token and the storage properties of a cookie at the same time.

PropertyJWT (in localStorage)JWT (in cookie)Opaque session cookie
Stateless verificationYesYesNo (DB lookup per request)
XSS exposureHighLow (HTTP-only)Low (HTTP-only)
CSRF exposureNone (header-based)Yes (needs SameSite/CSRF token)Yes (needs SameSite/CSRF token)
Easy revocationHardHardEasy (delete server row)
Cross-domain APIsEasyHarder (CORS + cookie config)Harder

The practical guidance: use whichever format your auth provider issues by default, store it in an HTTP-only cookie if same-origin, and don't over-engineer. The interesting security questions are about token lifetime and refresh handling, not about the token format.

Tools that issue JWTs for solo SaaS

You almost certainly should not write your own JWT issuer. The hosted and open-source options that handle this for solo founders are mature and cheap or free at small scale:

How Supabase RLS uses the JWT

The JWT’s killer use case in the Supabase stack is that Postgres itself reads the token. When a request hits PostgREST with an Authorization: Bearer <jwt> header, the gateway verifies the signature, then sets a Postgres session variable containing the decoded claims. RLS policies on tables can then reference auth.uid() — a SQL function that pulls sub out of the JWT — to filter rows. The result: a single signed token, issued at sign-in, drives every authorisation decision from the API edge down to the row level. We walk through the wiring in what is RLS and the multi-tenant patterns in what is multi-tenancy. JWTs also show up alongside other integration primitives like webhooks, where they often sign the webhook payload to prove the sender.

The takeaway

A JWT is three base64url-encoded segments with a signature on the end. It carries a user’s identity in a way any service holding the verification key can trust without a database lookup. For solo SaaS, that property — stateless, cross-service identity — is what makes the token format worth using. The mistakes are well-known and avoidable: keep tokens out of localStorage, pin algorithms in your verifier, give every token an expiry, and never put PII in the payload. Get those four right and the format does its job quietly for the entire life of your product.

Get one SaaS build breakdown every week

The stack, prompts, pricing, and mistakes to avoid — for solo founders building with AI.