Redis Session Architecture

Deep dive into Redis session storage schema, sliding TTL expiration, active devices, and remote revocation.

Core Guides Nuxt 4 / TypeScript

Redis Storage Model

nuxt-bearer-auth uses Redis as the authoritative server-side session store.

Two key patterns are used in Redis:

1. session: (String with TTL): Stores the JSON-serialized session object. 2. user_sessions: (Set with TTL): Stores the list of active session UUIDs belonging to that user.
typescript
┌─────────────────────────────────────────────────────────────┐
│ Redis Server                                                │
│                                                             │
│  session:a4e98f01-2b81-49c6...  (String, TTL 604800s)       │
│  ├── userId: "usr_101"                                      │
│  ├── token: "eyJhbGciOiJIUzI1Ni..." (Bearer Token)          │
│  ├── refreshToken: "d84f2c91..."                            │
│  ├── profile: { name: "Enoch", email: "..." }               │
│  ├── createdAt: "2025-01-15T10:30:00.000Z"                  │
│  ├── expiresAt: 1737030600000  (epoch ms, application check)│
│  ├── lastActivity: "2025-01-15T14:22:10.000Z"               │
│  ├── userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X)"   │
│  └── ipAddress: "192.168.1.50"                              │
│                                                             │
│  user_sessions:usr_101  (Set, TTL 604800s)                  │
│  ├── "a4e98f01-2b81-49c6..."  (MacBook)                     │
│  └── "f72c1092-881a-41b9..."  (iPhone)                      │
└─────────────────────────────────────────────────────────────┘
Session data is stored as plain JSON in Redis. The session store itself is not application-level encrypted. See Redis Security Hardening for infrastructure-level isolation recommendations.

Session Data Structure

Each session in Redis adheres to the following TypeScript interface. The full session type is exported from the package root:

typescript
import type { BearerAuthSession, PublicSession } from 'nuxt-bearer-auth'

interface BearerAuthSession<User extends BearerAuthUser = BearerAuthUser> {
  userId: string
  token: string           // Bearer token — stored server-side, never sent to browser
  refreshToken?: string | null
  profile: User | null    // User profile object hydrated from backend
  createdAt: string       // ISO timestamp of session creation
  expiresAt: number       // Epoch milliseconds — application-level expiry guard
  lastActivity: string    // ISO timestamp — updated on every authenticated request
  userAgent?: string      // Request User-Agent header at session creation
  ipAddress?: string      // Client IP address at session creation
}

// Safe shape returned to the browser — omits token, refreshToken, userId, expiresAt
interface PublicSession {
  id: string
  createdAt: string
  lastActivity: string
  userAgent?: string
  ipAddress?: string
}
Important: The token and refreshToken fields are only ever stored in Redis on the Nuxt server. They are intentionally excluded from PublicSession and never included in any response to the browser.

Sliding Session Expiration

Sessions use a sliding expiration window. Each time an authenticated request is made, the session's lastActivity timestamp is updated and the Redis TTL is reset to the full maxAge value. This means:
  • A session with a 7-day maxAge does not necessarily expire exactly 7 days after login.
  • An active user whose requests are processed regularly will maintain a valid session indefinitely, as each request restarts the 7-day window.
  • A session that has had no activity for the full maxAge period (7 days by default) will expire.
  • typescript
    Day 0:    Login       → session created, TTL = 7 days
    Day 3:    Request     → TTL reset to 7 days from now (expires Day 10)
    Day 9:    Request     → TTL reset to 7 days from now (expires Day 16)
    Day 16+:  No request  → session expires after idle for 7 days
    
    The session JSON also carries an expiresAt field (epoch milliseconds) that serves as an application-level double-check independent of the Redis TTL, guarding against any TTL drift.

    User Sessions Index

    The user_sessions: Redis Set tracks all active session IDs for a given user. This enables multi-device session management — listing, inspecting, and remotely revoking individual sessions without affecting others.

    When a new session is created, its ID is added to this set. When a session is destroyed (logout, revocation, or TTL expiry cleanup), its ID is removed from the set.

    Listing Active Sessions

    You can build a "Manage Active Devices" UI by calling the built-in endpoint GET /api/auth/sessions. The response shape is { sessions: PublicSession[] }:
    vue
    <script setup lang="ts">
    import type { PublicSession } from 'nuxt-bearer-auth'
    
    const { data, refresh } = await useFetch<{ sessions: PublicSession[] }>('/api/auth/sessions')
    
    async function revokeSession(sessionId: string) {
      await $fetch(`/api/auth/sessions/${sessionId}`, { method: 'DELETE' })
      await refresh()
    }
    </script>
    
    <template>
      <div class="sessions-list">
        <h3>Active Sessions</h3>
        <div v-for="s in data?.sessions" :key="s.id" class="session-card">
          <div>
            <p class="font-bold">{{ s.userAgent || 'Unknown Device' }}</p>
            <p class="text-sm text-gray-400">Last active: {{ new Date(s.lastActivity).toLocaleString() }}</p>
            <p class="text-xs text-gray-500">IP: {{ s.ipAddress }}</p>
          </div>
          <button @click="revokeSession(s.id)" class="btn-danger">Revoke</button>
        </div>
      </div>
    </template>
    

    Revoking Remote Sessions

    When a session is revoked via DELETE /api/auth/sessions/:id: 1. The endpoint verifies the current user owns that session ID — a 403 is returned if there is a mismatch. 2. Redis executes DEL session:. 3. Redis removes from the user_sessions: set using SREM. 4. Any subsequent request from that device finds no session in Redis, the stale cookie is cleared, and a 401 Unauthenticated is returned.

    Revoke All Sessions

    To destroy every active session for a user (e.g. on a password change or security incident response), use the server-side utility destroyAllBearerAuthSessions in a custom Nitro handler:
    typescript
    // server/api/account/revoke-all-sessions.post.ts
    import { requireBearerAuthSession, destroyAllBearerAuthSessions } from '#imports'
    
    export default defineEventHandler(async (event) => {
      const session = requireBearerAuthSession(event)
    
      // Deletes every session key for this user and clears the user_sessions set
      await destroyAllBearerAuthSessions(session.userId)
    
      return { success: true, message: 'All sessions revoked' }
    })
    
    This performs a Redis pipeline that deletes all session: keys for the user and then deletes the user_sessions: set in a single atomic operation.