When you log in to a web app, the server needs to remember who you are. It does this by storing a small piece of data in your browser called a cookie — typically containing something like your user ID. Your browser sends this cookie back with every request, so the server knows it's you.
Most frameworks — including React Router — sign the cookie using a technique called HMAC. Signing means the server can detect if someone tampered with the cookie (changed their user ID to someone else's, for example). That's important.
But signing doesn't hide the data. The cookie value is just base64-encoded — anyone can decode it and read what's inside. It's like sending a sealed envelope made of clear plastic. Nobody can swap the letter, but everyone can read it.
Encryption scrambles the data so that only the server (which has the secret key) can read it. Now the envelope is opaque.
| Can detect tampering | Hides the data | |
|---|---|---|
| Signed (HMAC) | Yes | No |
| Encrypted (AES-GCM) | Yes | Yes |
Right now, our cookie only stores a user.public_id — an opaque, random identifier that doesn't reveal anything useful even if someone reads it. So HMAC signing alone is sufficient.
If you later store sensitive data in the cookie (preferences, tokens, PII), you can add AES-256-GCM encryption on top. The crypto.ts module in the codebase has a ready-to-use implementation for this.
You probably don't need it if your cookie only stores opaque IDs. Consider adding it when:
The same COOKIE_SECRET serves two purposes — React Router uses it directly for HMAC signing, and the encryption layer derives a separate key from it, so the two are cryptographically independent.
COOKIE_SECRET), at least 32 charactersThe entire process uses the Web Crypto API — a standard built into every JavaScript runtime (browsers, Node.js, Deno, Cloudflare Workers). No external libraries needed.
COOKIE_SECRET). PBKDF2 is designed for passwords, where you need slow hashing to resist brute force. Using PBKDF2 with 1 iteration is pointless; with many iterations it adds latency for no benefit.+, /, and = characters that can cause issues in cookie values. base64url replaces them with -, _, and drops padding.Yes — for most apps, it absolutely is. HMAC signing prevents anyone from forging or tampering with the cookie. That's the important part. The only thing signing doesn't do is hide the contents — but if all you're storing is an opaque random ID, there's nothing useful to hide. Signing is the industry standard, and it's what React Router (and most other frameworks) ships with out of the box. You're not cutting corners by relying on it.