Sessions & Cookies
HTTP is stateless --- each request is independent, with no built-in memory of previous requests. Cookies and sessions are how web applications remember things about a user across multiple requests.
This guide covers both topics in depth. For a step-by-step introduction, see Part 6 of the tutorial.
What Are Cookies?
A cookie is a small piece of data that a server sends to a browser. The browser stores it and automatically includes it in every subsequent request to that server. Cookies are the foundation of:
- Login/logout (remembering who you are)
- Shopping carts (remembering what you added)
- User preferences (language, theme)
- Analytics and tracking
Cookies are sent via HTTP headers:
# Server sends a cookie
Set-Cookie: theme=dark; HttpOnly; Secure; SameSite=Lax; Max-Age=604800
# Browser includes the cookie in future requests
Cookie: theme=darkCreating Cookies with createCookie()
Remix provides createCookie() to create cookie configurations:
import { createCookie } from 'remix/cookie'
let sessionCookie = createCookie('__session', {
httpOnly: true,
secure: true,
sameSite: 'Lax',
secrets: ['my-secret-key-change-in-production'],
maxAge: 60 * 60 * 24 * 7, // 1 week in seconds
})The first argument is the cookie name --- the identifier that appears in the browser's developer tools and in the Cookie header.
Cookie Options
| Option | Type | Description |
|---|---|---|
httpOnly | boolean | If true, JavaScript in the browser cannot read the cookie. Protects against XSS attacks. |
secure | boolean | If true, the cookie is only sent over HTTPS connections. |
sameSite | 'Strict' | 'Lax' | 'None' | Controls when the cookie is sent with cross-site requests. |
secrets | string[] | Array of secrets used to sign the cookie value. |
maxAge | number | How long the cookie lasts, in seconds. |
domain | string | The domain the cookie is valid for (e.g. .example.com). |
path | string | The URL path the cookie is valid for (default: '/'). |
httpOnly
When httpOnly is true, the cookie is invisible to JavaScript running in the browser (document.cookie cannot see it). This prevents Cross-Site Scripting (XSS) attacks from stealing session cookies.
Always set httpOnly for session cookies
Session cookies should always be httpOnly. There is no legitimate reason for client-side JavaScript to access the session cookie.
secure
When secure is true, the cookie is only sent over HTTPS connections. This prevents the cookie from being intercepted on unencrypted connections.
secure: process.env.NODE_ENV === 'production',Set it to false in development (where you use http://localhost) and true in production (where you use HTTPS).
sameSite
Controls when the cookie is sent with cross-site requests:
| Value | Behavior |
|---|---|
'Strict' | Cookie is never sent with cross-site requests. Most secure, but breaks "Login with Google"-style flows. |
'Lax' | Cookie is sent with top-level navigations (clicking a link) but not with embedded requests (images, iframes). Good default. |
'None' | Cookie is always sent, even cross-site. Requires secure: true. Only use this if you need cross-site cookies (e.g. embedded widgets). |
'Lax' is the recommended default. It protects against CSRF (Cross-Site Request Forgery) while still allowing normal navigation.
maxAge
How long the cookie persists, in seconds:
maxAge: 60 * 60 * 24 * 7, // 1 week
maxAge: 60 * 60 * 24 * 30, // 30 days
maxAge: 60 * 60, // 1 hourIf you omit maxAge, the cookie becomes a session cookie that is deleted when the browser closes.
domain
By default, a cookie is only sent to the exact domain that set it. The domain option allows sharing across subdomains:
domain: '.example.com',
// Cookie is valid for example.com, www.example.com, api.example.com, etc.path
By default, the cookie is sent for all paths ('/'). You can restrict it:
path: '/admin',
// Cookie is only sent for URLs starting with /adminCookie Signing and Secret Rotation
Cookie signing uses a secret key to create a cryptographic signature appended to the cookie value. This allows the server to detect if the cookie has been tampered with. It does not encrypt the value --- the data is still readable, but any modification invalidates the signature.
Why Sign Cookies?
Without signing, a user could modify their session cookie to pretend to be someone else. Signing prevents this:
- Server sets cookie:
userId=42|signature=abc123 - User tries to change it to:
userId=1|signature=abc123 - Server detects the signature does not match and rejects the cookie.
Secret Rotation
The secrets option accepts an array of strings. This supports secret rotation --- the process of changing your secret key without invalidating all existing sessions:
let sessionCookie = createCookie('__session', {
secrets: [
process.env.SESSION_SECRET_NEW!, // Current secret (used for signing)
process.env.SESSION_SECRET_OLD!, // Previous secret (accepted for verification)
],
})The first secret in the array is used to sign new cookies. All secrets are tried when verifying incoming cookies. This means:
- Deploy with new secret added to the front of the array.
- Old cookies (signed with the old secret) continue to work.
- New cookies are signed with the new secret.
- After enough time has passed (longer than
maxAge), remove the old secret.
Use strong secrets
Generate secrets with at least 32 random characters:
node -e "console.log(crypto.randomUUID() + crypto.randomUUID())"Never hardcode secrets in source code. Use environment variables.
What Are Sessions?
A session is server-side storage associated with a specific user, identified by a cookie. The cookie holds only a session ID (a short opaque string). The actual data (user ID, preferences, flash messages) lives on the server.
Browser Server
| |
| Cookie: sid=abc123 |
|------------------------------->|
| | Look up session "abc123"
| | session data: { userId: 42 }
| Response (personalized) |
|<-------------------------------|Session Storage Backends
Session data can be stored in different places depending on your needs:
Cookie Storage
Stores session data directly in the cookie (encrypted). No server-side storage needed.
import { createCookieSessionStorage } from 'remix/session/cookie-storage'
let sessionStorage = createCookieSessionStorage(sessionCookie)Pros: No external dependencies, works everywhere. Cons: Limited to ~4KB of data, data is sent with every request.
Filesystem Storage
Stores session data as files on disk:
import { createFsSessionStorage } from 'remix/session/fs-storage'
let sessionStorage = createFsSessionStorage('./sessions')Pros: Simple, no extra software needed. Cons: Does not work with multiple servers (each has its own files). Not suitable for serverless or container deployments.
Memory Storage
Stores session data in-memory (useful for testing):
import { createMemorySessionStorage } from 'remix/session/memory-storage'
let sessionStorage = createMemorySessionStorage()Pros: Fast, no dependencies. Cons: Data is lost when the server restarts. Not suitable for production.
Redis Storage
Stores session data in Redis (an in-memory data store):
import { createRedisSessionStorage } from 'remix/session-storage-redis'
let sessionStorage = createRedisSessionStorage({
url: process.env.REDIS_URL!,
})Pros: Fast, supports multiple servers, built-in expiration. Cons: Requires a Redis server.
Memcache Storage
Stores session data in Memcache:
import { createMemcacheSessionStorage } from 'remix/session-storage-memcache'
let sessionStorage = createMemcacheSessionStorage({
servers: [process.env.MEMCACHE_URL!],
})Pros: Fast, supports multiple servers. Cons: Requires a Memcache server.
Choosing a Storage Backend
| Backend | Development | Single Server | Multiple Servers |
|---|---|---|---|
| Cookie | Good | Good | Good |
| Filesystem | Good | Good | No |
| Memory | Testing only | No | No |
| Redis | Overkill | Good | Best |
| Memcache | Overkill | Good | Good |
The Session Middleware
The session() middleware connects the cookie and storage to your router:
import { session } from 'remix/session-middleware'
import { sessionCookie, sessionStorage } from './session.ts'
let router = createRouter({
middleware: [
session(sessionCookie, sessionStorage),
],
})On every request, the middleware:
- Reads the session cookie from the request.
- Loads the session data from storage using the session ID in the cookie.
- Makes the session available via
context.get(Session). - After the handler runs, saves any changes back to storage.
- Updates the
Set-Cookieheader in the response.
The Session API
Access the session in any handler:
import { Session } from 'remix/session'
handler({ get }) {
let session = get(Session)
// ...
}session.get(key)
Read a value from the session:
let userId = session.get('userId') // number | undefined
let theme = session.get('theme') // string | undefinedReturns undefined if the key does not exist.
session.set(key, value)
Write a value to the session:
session.set('userId', 42)
session.set('theme', 'dark')
session.set('cart', [{ bookId: 1, quantity: 2 }])Values can be any JSON-serializable data: strings, numbers, booleans, arrays, and objects.
session.has(key)
Check if a key exists in the session:
if (session.has('userId')) {
// User is logged in
}session.unset(key)
Remove a single key from the session:
session.unset('cart')session.flash(key, value)
Set a value that is only available for one subsequent request, then automatically removed:
// In the handler that processes an action
session.flash('message', 'Book created successfully!')
return createRedirectResponse('/books')// In the handler that renders the next page
let message = session.get('message') // "Book created successfully!"
// On the next request, session.get('message') returns undefinedFlash values are perfect for success/error notifications after redirects.
session.destroy()
Destroy the entire session. All data is deleted and the cookie is cleared:
session.destroy()
return createRedirectResponse('/login')Use this for logout.
session.regenerateId()
Generate a new session ID while preserving the session data:
session.regenerateId()This is a security best practice after authentication state changes (login, privilege escalation). It prevents session fixation attacks where an attacker obtains a session ID and waits for the victim to log in with it.
Regenerate after login
Always call session.regenerateId() after a successful login:
session.set('userId', user.id)
session.regenerateId()
return createRedirectResponse('/')Flash Messages Pattern
Flash messages are one of the most common session patterns. Here is a complete implementation:
Setting Flash Messages
// After creating a book
session.flash('flash', {
type: 'success',
message: 'Book created successfully!',
})
return createRedirectResponse('/books')
// After a validation error that requires a redirect
session.flash('flash', {
type: 'error',
message: 'Something went wrong. Please try again.',
})
return createRedirectResponse('/books/new')Reading and Displaying Flash Messages
// In the handler
handler({ get }) {
let session = get(Session)
let flash = session.get('flash')
let books = await db.findMany(booksTable)
return render(<BookList books={books} flash={flash} />)
}
// In the component
function FlashMessage() {
return ({ flash }: { flash?: { type: string; message: string } }) => {
if (!flash) return null
let bgColor = flash.type === 'success' ? '#d4edda' : '#f8d7da'
let textColor = flash.type === 'success' ? '#155724' : '#721c24'
return (
<div mix={css({
padding: '12px 16px',
marginBottom: '16px',
borderRadius: '4px',
backgroundColor: bgColor,
color: textColor,
})}>
{flash.message}
</div>
)
}
}Security Best Practices
Always Use httpOnly for Session Cookies
Prevents JavaScript from reading the cookie, protecting against XSS attacks.
Always Use secure in Production
Prevents the cookie from being sent over unencrypted HTTP connections.
Set sameSite to 'Lax' or 'Strict'
Prevents CSRF attacks by controlling when cookies are sent with cross-site requests.
Regenerate Session IDs After Login
Prevents session fixation attacks:
session.set('userId', user.id)
session.regenerateId()Use Strong, Random Secrets
Generate secrets with sufficient entropy (at least 32 characters). Never use predictable values like 'secret' or 'password123'.
Set Reasonable maxAge Values
Shorter session lifetimes reduce the window of opportunity for session hijacking. Balance security with user convenience:
- High security (banking): 15-30 minutes
- Normal applications: 1-7 days
- "Remember me": 30-90 days
Store Minimal Data in Sessions
Only store what you need (typically just a user ID). Load full user data from the database on each request. This way, if a user's permissions change, the change takes effect immediately.
Use Server-Side Session Storage
Prefer Redis or filesystem storage over cookie storage for sensitive applications. Server-side storage lets you:
- Invalidate sessions (e.g., "log out everywhere")
- Store more data (cookies are limited to ~4KB)
- Keep session data private (cookie storage, even when encrypted, reveals the data size)
Complete Session Setup Example
import { createCookie } from 'remix/cookie'
import { createFsSessionStorage } from 'remix/session/fs-storage'
export let sessionCookie = createCookie('__session', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'Lax',
secrets: [process.env.SESSION_SECRET!],
maxAge: 60 * 60 * 24 * 7, // 1 week
})
export let sessionStorage = createFsSessionStorage('./sessions')import { createRouter } from 'remix/fetch-router'
import { session } from 'remix/session-middleware'
import { sessionCookie, sessionStorage } from './session.ts'
let router = createRouter({
middleware: [
// ... other middleware ...
session(sessionCookie, sessionStorage),
],
})import { Session } from 'remix/session'
import { createRedirectResponse } from 'remix/response/redirect'
// Login handler
async function login({ get }) {
let data = get(FormData)
let session = get(Session)
// ... verify credentials ...
session.set('userId', user.id)
session.regenerateId()
session.flash('flash', {
type: 'success',
message: `Welcome back, ${user.name}!`,
})
return createRedirectResponse('/')
}
// Logout handler
function logout({ get }) {
let session = get(Session)
session.destroy()
return createRedirectResponse('/login')
}Related
- Tutorial: Sessions & Authentication --- Step-by-step auth setup.
- Middleware --- How session middleware integrates with the middleware chain.
- Forms & Mutations --- Flash messages with the Post/Redirect/Get pattern.
- Error Handling --- Handling authentication errors.