What is JWT?
A JWT (JSON Web Token) is a compact, URL-safe string that represents a set of claims, usually about who a user is and when that information expires. It’s basically a signed, self-contained payload that a server can verify without having to look anything up in a database or session store.
Why use JWT?
The main reason people reach for JWTs is that they make authentication stateless. Traditional session-based systems depend on a centralized store (Redis, Memcached, the database) to keep track of every active user. That means every request has to hit that store to confirm who the user is. It works, but it’s not always ideal, especially when your app isn’t just a single monolithic Rails app anymore.
Stateless Authentication
With JWT, all the information the server needs to identify the user is inside the token itself. Once it’s signed, any server in your cluster can verify it independently using the same secret or public key. No shared session store, no sticky load balancers, no shared state.
Works across services and clients
If your app has multiple frontends (say, a React SPA, an iOS app, and a partner integration), or multiple backend services (Rails API, background job processors, microservices), JWTs make life easier. Everyone can verify the same token using the same key. There’s no need to coordinate session IDs or cookies across domains.
Scales horizontally
Because JWTs don’t require a central lookup, they’re ideal for distributed systems. You can spin up new Rails servers or container instances without worrying about where sessions live. They’re just tokens.
Portable and language-agnostic
JWTs are JSON, which means anything can parse and verify them; Ruby, Node, Go, Python, Rust, you name it. That makes them great for APIs that might outgrow Rails one day or need to integrate with external systems.
Self-contained
Each token can carry extra claims: user ID, role, scope, or even limited permissions (like “read-only” access). You can use that to simplify authorization logic in APIs that don’t always want to hit the database for every request.
When not to use JWT
JWTs are great for APIs that need to be stateless, shared across services, or accessed from multiple frontends. If you’re building a typical Rails app, or any web app that lives mostly under one domain, you might not actually need them.
Rails already gives you a perfectly solid authentication mechanism using signed session cookies. They’re automatically encrypted, protected against tampering, and include CSRF protection by default. You don’t have to manage token rotation, expiration, or revocation yourself. Rails just handles it.
Even in API-only projects, JWTs can be overkill if:
- The API is only consumed by your own frontend on the same domain.
- You don’t need to share authentication across multiple services or languages.
- You want easy session invalidation. Resetting a central session is a lot simpler than maintaining a token blocklist.
- You prefer server-side control over active sessions (e.g., logging out a user everywhere immediately).
Structure of a JWT
A JWT is just a string, three parts separated by dots, that encodes and signs a small bundle of data.
A simple JWT looks something like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOjEsImV4cCI6MTcwMDAwMDAwMH0.
1CjK8cLB6hE_3cWfPzSeEX3N2sBQcCPZZ5mAqWZ3k7E
Each section is a Base64URL-encoded JSON that breaks down into the following:
Header
This describes how the token was signed and what type of token it is. It’s usually something tiny like the following:
{
"alg": "HS256",
"typ": "JWT"
}
In this example, HS256 means HMAC using SHA-256, but other common values include RS256 (RSA public/private key pair).
Payload
The payload holds the actual claims, which is another way of saying “statements about the user or the session.” These are just key-value pairs, something like:
{
"sub": 1,
"name": "Carlos",
"iat": 1700000000,
"exp": 1700000900
}
Some of these keys are standardized such as:
sub- the subject (often the user id)iat- issued at (timestamp)exp- expiration timeiss- who issued the tokenaud- intended audience
You can also roll your own, like roles or scopes. Something similar to the following:
{
"role": "admin",
"scp": ["read:users"]
}
For a full break down of claims (including registered claims, public claims, and private claims) see RFC 7519.
Signature
The signature is what keeps everything honest. It’s a hash of the header and payload, generated with your secret (or private key). When a request comes in with a token, the server recomputes this signature using its secret key. If it matches, you can trust the data hasn’t been tampered with.
Coming Up
In the next article, I’ll go over how to generate a JWT manually, without having to rely on a package (because it really solidifies understanding), as well as how to use it with a package like Ruby’s jwt gem. Finally, we’ll wire it up in a Rails API and talk about expiration, rotation, and revocation.