Skip to main content
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

npm i remix

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:
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

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

Build docs developers (and LLMs) love