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: ThetokenandrefreshTokenfields are only ever stored in Redis on the Nuxt server. They are intentionally excluded fromPublicSessionand 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'slastActivity timestamp is updated and the Redis TTL is reset to the full maxAge value.
This means:
maxAge does not necessarily expire exactly 7 days after login.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
Theuser_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 endpointGET /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 viaDELETE /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 utilitydestroyAllBearerAuthSessions 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.