remix/cookie
The remix/cookie package provides utilities for creating, parsing, and serializing HTTP cookies. Cookies are the foundation of session management, user preferences, and other stateful behaviors in web applications.
Installation
The remix/cookie package is included with Remix. No additional installation is required.
import { createCookie } from 'remix/cookie'createCookie(name, options)
Creates a cookie configuration object that can be used to parse and serialize cookie values.
function createCookie(name: string, options?: CookieOptions): CookieParameters:
| Parameter | Type | Description |
|---|---|---|
name | string | The cookie name. This is the identifier that appears in the browser's developer tools and in the Cookie header. |
options | CookieOptions | Optional. Configuration for the cookie's behavior and security attributes. |
Returns: A Cookie object with parse and serialize methods.
Example:
import { createCookie } from 'remix/cookie'
let sessionCookie = createCookie('__session', {
httpOnly: true,
secure: true,
sameSite: 'Lax',
secrets: [process.env.SESSION_SECRET!],
maxAge: 60 * 60 * 24 * 7, // 1 week
})Cookie Options
| Option | Type | Default | Description |
|---|---|---|---|
domain | string | Current domain | The domain the cookie is valid for. Set to .example.com to share across subdomains. |
path | string | '/' | The URL path the cookie is valid for. |
expires | Date | -- | An absolute expiration date. Mutually exclusive with maxAge. |
maxAge | number | -- | Cookie lifetime in seconds from the time it is set. Preferred over expires. |
httpOnly | boolean | false | If true, the cookie is inaccessible to JavaScript in the browser (document.cookie). |
secure | boolean | false | If true, the cookie is only sent over HTTPS connections. |
sameSite | 'Strict' | 'Lax' | 'None' | 'Lax' | Controls when the cookie is sent with cross-site requests. |
partitioned | boolean | false | If true, the cookie uses the CHIPS (Cookies Having Independent Partitioned State) proposal for third-party cookie partitioning. |
secrets | string[] | [] | Array of secrets for HMAC-SHA256 signing. The first secret is used for signing; all are used for verification. |
encode | (value: string) => string | encodeURIComponent | Custom encoding function for the cookie value. |
decode | (value: string) => string | decodeURIComponent | Custom decoding function for the cookie value. |
domain
By default, a cookie is only sent to the exact domain that set it. The domain option allows sharing across subdomains:
createCookie('__session', {
domain: '.example.com',
// Valid for example.com, www.example.com, api.example.com, etc.
})path
By default, the cookie is sent for all paths ('/'). Restrict it to a specific path:
createCookie('admin_session', {
path: '/admin',
// Only sent for URLs starting with /admin
})expires and maxAge
maxAge sets a relative lifetime in seconds. expires sets an absolute Date. If neither is set, the cookie is a session cookie that is deleted when the browser closes.
// Relative (preferred)
createCookie('prefs', { maxAge: 60 * 60 * 24 * 365 }) // 1 year
// Absolute
createCookie('prefs', { expires: new Date('2027-01-01') })Prefer maxAge over expires
maxAge is simpler and avoids issues with clock skew between the server and client. It is the recommended approach.
httpOnly
When true, the cookie cannot be read by JavaScript in the browser. This protects against Cross-Site Scripting (XSS) attacks stealing the cookie.
createCookie('__session', { httpOnly: true })Always set httpOnly: true for session cookies. Only omit it for cookies that the browser's JavaScript needs to read (e.g. a theme preference used by client-side code).
secure
When true, the cookie is only sent over HTTPS connections. In development (HTTP), set it to false:
createCookie('__session', {
secure: process.env.NODE_ENV === 'production',
})sameSite
Controls when the cookie is sent with cross-site requests:
| Value | Behavior |
|---|---|
'Strict' | Never sent with cross-site requests. Most secure, but can break flows like "Sign in with Google" that redirect back to your site. |
'Lax' | Sent with top-level navigations (clicking a link) but not with embedded requests (images, iframes, fetch). Good default. |
'None' | Always sent, even cross-site. Requires secure: true. Only use if you need cross-site cookies (e.g. embedded widgets). |
partitioned
Enables CHIPS (Cookies Having Independent Partitioned State). When true, the cookie is partitioned by the top-level site, preventing cross-site tracking while still allowing the cookie to work in embedded contexts.
createCookie('widget_session', {
partitioned: true,
secure: true,
sameSite: 'None',
})encode and decode
Custom functions for encoding and decoding cookie values. The defaults are encodeURIComponent and decodeURIComponent. Override these if you need a different encoding scheme:
createCookie('data', {
encode: (value) => Buffer.from(value).toString('base64'),
decode: (value) => Buffer.from(value, 'base64').toString('utf-8'),
})Cookie Methods
cookie.parse(headerValue)
Parses a Cookie header string and extracts this cookie's value. If the cookie is signed, the signature is verified.
cookie.parse(headerValue: string): Promise<unknown>Parameters:
| Parameter | Type | Description |
|---|---|---|
headerValue | string | The value of the Cookie HTTP header from the incoming request. |
Returns: A Promise that resolves to the parsed cookie value, or null if the cookie is not present or the signature is invalid.
Example:
let cookieHeader = request.headers.get('Cookie') ?? ''
let value = await sessionCookie.parse(cookieHeader)
if (value) {
console.log('Session ID:', value)
}cookie.serialize(value)
Creates a Set-Cookie header string for this cookie with the given value. If secrets are configured, the value is signed.
cookie.serialize(value: unknown): Promise<string>Parameters:
| Parameter | Type | Description |
|---|---|---|
value | unknown | The value to store in the cookie. Must be JSON-serializable. |
Returns: A Promise that resolves to a complete Set-Cookie header string, including all cookie attributes.
Example:
let setCookie = await sessionCookie.serialize('abc123')
// "__session=abc123; HttpOnly; Secure; SameSite=Lax; Max-Age=604800"
return new Response('OK', {
headers: { 'Set-Cookie': setCookie },
})To clear a cookie, serialize it with null or an empty string and set maxAge to 0:
let setCookie = await sessionCookie.serialize('', { maxAge: 0 })Cookie Signing with HMAC-SHA256
Cookie signing uses HMAC-SHA256 to create a cryptographic signature appended to the cookie value. This allows the server to detect tampering --- if anyone modifies the cookie value, the signature will not match, and parse returns null.
Signing does not encrypt the cookie. The value is still readable. It only ensures integrity: the value has not been changed since the server set it.
How It Works
- When
serializeis called, the cookie value is signed using the first secret in thesecretsarray. - The signature is appended to the cookie value (e.g.
value.signature). - When
parseis called, each secret in thesecretsarray is tried to verify the signature. - If no secret produces a valid signature,
parsereturnsnull.
let cookie = createCookie('userId', {
secrets: ['my-secret-key'],
})
// Serialize: value is signed
let header = await cookie.serialize(42)
// "userId=42.HmacSha256Signature; ..."
// Parse: signature is verified
let value = await cookie.parse('userId=42.HmacSha256Signature')
// 42
// Parse with tampered value: signature check fails
let tampered = await cookie.parse('userId=1.HmacSha256Signature')
// nullSecret Rotation
The secrets option accepts an array. This supports secret rotation --- changing your signing key without invalidating existing cookies.
let sessionCookie = createCookie('__session', {
secrets: [
process.env.SESSION_SECRET_NEW!, // Current secret (used for signing)
process.env.SESSION_SECRET_OLD!, // Previous secret (still accepted for verification)
],
})How rotation works:
- The first secret in the array is used to sign new cookies.
- All secrets are tried when verifying incoming cookies.
- Cookies signed with the old secret continue to work.
- New cookies are signed with the new secret.
- After sufficient time has passed (longer than
maxAge), remove the old secret.
Rotation procedure:
# Step 1: Generate a new secret
node -e "console.log(crypto.randomUUID() + crypto.randomUUID())"
# Step 2: Add it as SESSION_SECRET_NEW, move the old one to SESSION_SECRET_OLD
# Step 3: Deploy with both secrets
# secrets: [process.env.SESSION_SECRET_NEW!, process.env.SESSION_SECRET_OLD!]
# Step 4: After maxAge has elapsed, remove SESSION_SECRET_OLD
# secrets: [process.env.SESSION_SECRET_NEW!]Use strong secrets
Generate secrets with at least 32 random characters. Never use predictable values like 'secret' or 'password'. Never hardcode secrets in source code --- use environment variables.
node -e "console.log(crypto.randomUUID() + crypto.randomUUID())"Examples
Session Cookie
The most common use case --- a signed, HTTP-only cookie for session management:
import { createCookie } from 'remix/cookie'
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
})Theme Preference Cookie
A cookie for storing a user's theme preference (readable by client-side JavaScript):
let themeCookie = createCookie('theme', {
httpOnly: false, // Accessible to client-side JS
secure: process.env.NODE_ENV === 'production',
sameSite: 'Lax',
maxAge: 60 * 60 * 24 * 365, // 1 year
})
// Set theme
let setCookie = await themeCookie.serialize('dark')
// Read theme
let theme = await themeCookie.parse(request.headers.get('Cookie') ?? '')
// 'dark'Cookie Consent Cookie
A cookie that records the user's consent to cookies:
let consentCookie = createCookie('cookie_consent', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'Strict',
maxAge: 60 * 60 * 24 * 365, // 1 year
})
// Record consent
let setCookie = await consentCookie.serialize({
analytics: true,
marketing: false,
timestamp: Date.now(),
})Short-Lived CSRF Token Cookie
A cookie that expires quickly, used alongside CSRF middleware:
let csrfCookie = createCookie('csrf', {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'Strict',
secrets: [process.env.CSRF_SECRET!],
maxAge: 60 * 60, // 1 hour
})Cross-Subdomain Cookie
A cookie shared across all subdomains:
let sharedCookie = createCookie('shared_session', {
domain: '.example.com',
httpOnly: true,
secure: true,
sameSite: 'Lax',
secrets: [process.env.SESSION_SECRET!],
maxAge: 60 * 60 * 24 * 7,
})Related
- Sessions & Cookies -- Conceptual guide to cookies and sessions
- remix/session -- Session management built on top of cookies
- remix/session-middleware -- Middleware that handles cookie-session integration
- Security Guide -- CSRF, XSS, and other security considerations