Sessions

Cookie-based sessions with pluggable stores and flash message support.

Overview

@tekir/session provides cookie-based HTTP sessions. A session ID is stored in a signed cookie; the actual data lives in a configurable store (memory, Redis, or a SQL table). The middleware reads the cookie, hydrates a Session object, attaches it to ctx.session, and automatically persists changes after the response.

bun add @tekir/session

Configuration

Create config/session.ts and set the driver field. The SessionProvider creates the appropriate store automatically.

The session cookie is hardened by default: it is sent with HttpOnly, SameSite=Lax, and Secure. Secure defaults to true in production (and when NODE_ENV is unset); set cookie.secure: false to opt out, for example when serving over plain HTTP during local development.

config/session.ts
import env from '#env'
import type { SessionConfig } from '@tekir/session'

export default {
  driver: 'memory',
  age: 7200,                    // session lifetime in seconds (default: 2h)
  cookieName: 'tekir_session',
  cookie: {
    httpOnly: true,
    secure: env.NODE_ENV === 'production',
    sameSite: 'lax' as const,
    path: '/'
  }
} satisfies SessionConfig

For production with Redis (requires bun add @tekir/redis):

config/session.ts
import env from '#env'
import type { SessionConfig } from '@tekir/session'

export default {
  driver: 'redis',
  prefix: 'sess:',              // key prefix in Redis (default: 'sess:')
  age: 86400,                   // 24 hours
  cookieName: 'tekir_session',
  cookie: {
    httpOnly: true,
    secure: env.NODE_ENV === 'production',
    sameSite: 'lax' as const
  }
} satisfies SessionConfig

// Requires @tekir/redis: the provider reads config('redis') for connection options

Or with a database table:

config/session.ts
import env from '#env'
import type { SessionConfig } from '@tekir/session'

export default {
  driver: 'database',
  table: 'sessions',            // table name, auto-created (default: 'sessions')
  age: 7200,
  cookieName: 'tekir_session',
  cookie: {
    httpOnly: true,
    secure: env.NODE_ENV === 'production',
    sameSite: 'lax' as const
  }
} satisfies SessionConfig

// Uses the registered 'db' service: add DatabaseProvider before SessionProvider

Register SessionProvider in your kernel:

start/kernel.ts
import type { TekirApp } from '@tekir/core'
import { SessionProvider } from '@tekir/session'

export default function({ app }: TekirApp) {
  app.registerAll([SessionProvider])
}

Access the session in any controller or middleware via ctx.session:

core/controllers/cart_controller.ts
import type { HttpContext } from '@tekir/core'

export async function index({ session, response }: HttpContext) {
  const items = session.get<string[]>('items', [])
  return response.ok({ items })
}

Session Class

ctx.session is an instance of the Session class. It exposes two namespaces: the main data bag and the flash bag (one-time messages). Both are serialized to the store together on every save.

put / get / has

The three fundamental operations. All methods are synchronous; IO only happens when the session is saved after the response.

// Store a value
ctx.session.put('userId', 42)
ctx.session.put('theme', 'dark')

// Retrieve a value (with optional default)
const userId = ctx.session.get<number>('userId')       // 42
const locale = ctx.session.get<string>('locale', 'en') // 'en' if not set

// Check existence
const loggedIn = ctx.session.has('userId')  // true

all / pull / forget / clear

// all(), snapshot of the entire session data (shallow copy)
const data = ctx.session.all()
// { userId: 42, theme: 'dark' }

// pull(): read and remove in one call
const theme = ctx.session.pull<string>('theme', 'light')
// 'dark': and 'theme' is now removed from the session

// forget(): remove a single key
ctx.session.forget('userId')

// clear(): wipe every key
ctx.session.clear()
  • all(): returns a shallow copy. Mutating it does not affect the session.
  • pull(key, default?): reads and removes in one step. Ideal for one-off tokens.
  • forget(key): deletes a single key.
  • clear(): removes every key (flash is unaffected).

increment / decrement

Convenience helpers for numeric counters. Both accept an optional step argument.

// increment(key, by?), add to a numeric counter (default by = 1)
ctx.session.put('views', 0)

ctx.session.increment('views')        // 1
ctx.session.increment('views')        // 2
ctx.session.increment('views', 5)     // 7

// decrement(key, by?): subtract from a numeric counter (default by = 1)
ctx.session.decrement('views')        // 6
ctx.session.decrement('views', 3)     // 3

Flash Messages

Flash data is written on the current request and available on the next request. Reading with getFlash() consumes and removes it, the standard pattern for post-redirect-get messages.

// flash(), store a value that survives only one additional request
ctx.session.flash('success', 'Profile updated successfully.')
ctx.session.flash('errors', { email: 'Already taken.' })

// In the next request:
const msg = ctx.session.getFlash<string>('success')
// 'Profile updated successfully.'
// Reading via getFlash() immediately removes the key.

// Check without consuming
const hasError = ctx.session.hasFlash('errors')  // true

// Get all flash data (without consuming)
const all = ctx.session.flashAll()

Regenerate & Destroy

Call regenerate() after login to swap the session ID and prevent fixation attacks. Call destroy() on logout to delete the session entirely.

// regenerate(), replace the session ID (use after login to prevent fixation)
const newId = await ctx.session.regenerate()
ctx.session.put('userId', user.id)

// destroy(): delete session data from the store and clear the in-memory state
await ctx.session.destroy()

Drivers

  • memory: in-process Map. Data lost on restart, not shared across workers. Default for development.
  • redis: JSON in Redis with EXPIRE. Shared across instances. Requires @tekir/redis: reads connection options from config/redis.ts.
  • database: SQL table with INSERT OR REPLACE. Table auto-created. Requires DatabaseProvider registered before SessionProvider.

Config Reference

All options accepted by config/session.ts:

import type { SessionConfig } from '@tekir/session'

const config: SessionConfig = {
  // driver: 'memory' | 'redis' | 'database' (default: 'memory')
  driver: 'memory',

  // age: session lifetime in seconds (default: 7200)
  age: 7200,

  // cookieName: name of the session cookie (default: 'tekir_session')
  cookieName: 'tekir_session',

  // cookie: cookie attributes (secure defaults: HttpOnly + SameSite + Secure)
  cookie: {
    httpOnly: true,    // default: true
    secure:   true,    // default: true in production / when NODE_ENV is unset;
                       // set false to allow plain-HTTP local development
    sameSite: 'lax',   // 'strict' | 'lax' | 'none' (default: 'lax')
    path:     '/',
    domain:   undefined
  },

  // Redis driver options
  prefix: 'sess:',     // key prefix (default: 'sess:')

  // Database driver options
  table: 'sessions'   // table name (default: 'sessions')
}