Session middleware for Remix using signed cookies. It loads session state from incoming requests, stores it in request context using Session, and persists updates automatically.
Features
- Session Lifecycle Handling - Reads and saves session state per request
- Context Integration - Exposes session APIs directly on request context
- Secure Cookie Support - Designed for signed session cookies
Installation
Usage
import { createRouter } from 'remix/fetch-router'
import { createCookie } from 'remix/cookie'
import { Session } from 'remix/session'
import { createCookieSessionStorage } from 'remix/session/cookie-storage'
import { session } from 'remix/session-middleware'
let sessionCookie = createCookie('__session', {
secrets: ['s3cr3t'], // session cookies must be signed!
httpOnly: true,
secure: true,
sameSite: 'lax',
})
let sessionStorage = createCookieSessionStorage()
let router = createRouter({
middleware: [session(sessionCookie, sessionStorage)],
})
router.get('/', (context) => {
let session = context.get(Session)
session.set('count', Number(session.get('count') ?? 0) + 1)
return new Response(`Count: ${session.get('count')}`)
})
The middleware:
- Reads the session from the cookie on incoming requests
- Makes it available as
context.get(Session)
- Automatically saves session changes and sets the cookie on responses
The session cookie must be signed for security. This prevents tampering with the session data on the client.
Login/Logout Flow
A basic login/logout flow could look like this:
import * as res from 'remix/fetch-router/response-helpers'
import { Session } from 'remix/session'
import { html } from 'remix/html-template'
router.get('/login', ({ get }) => {
let session = get(Session)
let error = session.get('error')
return res.html(html`
<html>
<body>
<h1>Login</h1>
${error ? html`<div class="error">${error}</div>` : null}
<form method="POST" action="/login">
<input type="text" name="username" placeholder="Username" />
<input type="password" name="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
</body>
</html>
`)
})
router.post('/login', ({ get }) => {
let session = get(Session)
let formData = get(FormData)
let username = formData.get('username')
let password = formData.get('password')
let user = authenticateUser(username, password)
if (!user) {
session.flash('error', 'Invalid username or password')
return res.redirect('/login')
}
// Regenerate session ID for security
session.regenerateId()
session.set('userId', user.id)
return res.redirect('/dashboard')
})
router.post('/logout', ({ get }) => {
let session = get(Session)
session.destroy()
return res.redirect('/')
})
Flash Messages
Flash messages are perfect for displaying one-time notifications after redirects:
router.post('/contact', async ({ get }) => {
let session = get(Session)
let formData = get(FormData)
try {
await sendContactEmail(formData)
session.flash('success', 'Message sent successfully!')
return res.redirect('/contact')
} catch (error) {
session.flash('error', 'Failed to send message')
return res.redirect('/contact')
}
})
router.get('/contact', ({ get }) => {
let session = get(Session)
let success = session.get('success')
let error = session.get('error')
return res.html(html`
<html>
<body>
${success ? html`<div class="success">${success}</div>` : null}
${error ? html`<div class="error">${error}</div>` : null}
<form method="POST">
<!-- form fields -->
</form>
</body>
</html>
`)
})
Protected Routes
Use session data to protect routes that require authentication:
function requireAuth(context: Context) {
let session = context.get(Session)
let userId = session.get('userId')
if (!userId) {
session.flash('error', 'Please log in to continue')
return res.redirect('/login')
}
return null // Allow request to proceed
}
router.get('/dashboard', (context) => {
let authResponse = requireAuth(context)
if (authResponse) return authResponse
let session = context.get(Session)
let userId = session.get('userId')
return res.html(html`
<html>
<body>
<h1>Dashboard</h1>
<p>Welcome, user ${userId}!</p>
</body>
</html>
`)
})
Storage Options
The middleware works with any session storage strategy:
Cookie Storage
import { createCookieSessionStorage } from 'remix/session/cookie-storage'
let sessionStorage = createCookieSessionStorage()
let router = createRouter({
middleware: [session(sessionCookie, sessionStorage)],
})
Filesystem Storage
import { createFsSessionStorage } from 'remix/session/fs-storage'
let sessionStorage = createFsSessionStorage('/tmp/sessions')
let router = createRouter({
middleware: [session(sessionCookie, sessionStorage)],
})
External Storage
import { createRedisSessionStorage } from '@remix-run/session-storage-redis'
let sessionStorage = createRedisSessionStorage({
url: 'redis://localhost:6379',
})
let router = createRouter({
middleware: [session(sessionCookie, sessionStorage)],
})
API Reference
session(cookie, storage)
Creates session middleware for fetch-router.
Parameters:
cookie: Cookie - A signed cookie instance for session identification
storage: SessionStorage - The session storage strategy to use
Returns: Middleware
Using Session in Routes
Access the session from route handlers using context.get(Session):
import { Session } from 'remix/session'
router.get('/profile', (context) => {
let session = context.get(Session)
// Read values
let userId = session.get('userId')
let theme = session.get('theme')
// Set values
session.set('lastVisit', Date.now())
// Flash messages
session.flash('notification', 'Profile updated')
// Check for values
if (session.has('userId')) {
// user is logged in
}
// Remove values
session.unset('tempData')
return Response.json({ userId, theme })
})
Security Best Practices
Always Sign Session Cookies
Session cookies must be signed to prevent tampering:
let sessionCookie = createCookie('__session', {
secrets: ['your-secret-key'], // Required!
httpOnly: true,
secure: true,
sameSite: 'lax',
})
Regenerate Session IDs on Login
Prevent session fixation attacks by regenerating the session ID after authentication:
router.post('/login', ({ get }) => {
let session = get(Session)
if (validCredentials) {
session.regenerateId() // Important!
session.set('userId', user.id)
return res.redirect('/dashboard')
}
})
Destroy Sessions on Logout
Properly clean up sessions when users log out:
router.post('/logout', ({ get }) => {
let session = get(Session)
session.destroy()
return res.redirect('/')
})
session - Session management and storage
cookie - Cookie parsing and serialization
fetch-router - Router for the web Fetch API