SSR Support
Nuxt Aegis provides full Server-Side Rendering (SSR) support with authenticated SSR enabled by default when Nuxt SSR is enabled. The module automatically detects your Nuxt SSR configuration and adjusts accordingly.
Default Behavior
When Nuxt SSR is enabled (ssr: true):
- Authenticated SSR is automatically enabled (
enableSSR: trueby default) - Pages can render with authenticated user data during SSR
useAsyncDataanduseFetchwork with authenticated endpoints on the server- Access tokens are never sent to the client in the HTML payload
When Nuxt SSR is disabled (ssr: false):
- Client-side rendering only
- Token refresh happens after app hydration on the client
- Standard SPA authentication flow
How Authenticated SSR Works
Server-Side Authentication (During SSR)
- Nitro plugin validates the httpOnly refresh cookie
- Generates a short-lived access token (5 minutes by default)
- Token is stored in
event.context.ssrAccessToken $apiinstance automatically includes this token in Authorization headersevent.context.useris populated for middleware and server routes
Client-Side Hydration (After SSR)
- Server-verified user state is available through hydration; access tokens are not included in that state
- Eligible startup resolution restores unknown sessions; already-resolved identity is left intact
- A client API 401 can trigger refresh to obtain an in-memory access token
- Successful client refresh applies the configured rotation policy (never rotated during SSR)
- A transient client refresh failure preserves server-verified identity without renewing a token
- Security
- SSR tokens are never sent to the client (stay in
event.context) - Refresh token is NOT rotated on server (avoids client/server conflicts)
- Client handles token rotation after hydration
- SSR tokens are never sent to the client (stay in
$api Plugin
The $api plugin provides automatic bearer token injection on both server and client. See the $api Plugin Reference for implementation details and the Composables API for usage examples.
Configuration
Authenticated SSR is enabled by default when Nuxt SSR is enabled. You can explicitly control this behavior:
export default defineNuxtConfig({
ssr: true, // Nuxt SSR must be enabled
nuxtAegis: {
// Option 1: Use default (automatic based on Nuxt SSR)
// enableSSR: undefined (defaults to true when ssr: true)
// Option 2: Explicitly enable authenticated SSR
enableSSR: true,
// Option 3: Disable authenticated SSR (client-only token refresh)
// enableSSR: false,
tokenRefresh: {
automaticRefresh: true, // Recommended
ssrTokenExpiry: '5m', // SSR token lifetime (default: '5m')
}
}
})Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
enableSSR | boolean | true when ssr: true | Enable authenticated SSR mode |
tokenRefresh.automaticRefresh | boolean | true | Required for SSR experience |
tokenRefresh.ssrTokenExpiry | string | '5m' | Token lifetime for SSR-generated access tokens |
Important: If you set enableSSR: true but have ssr: false in your Nuxt config, the module will log a warning and SSR authentication will not work.
Using Authenticated SSR
With authenticated SSR enabled (default), you can fetch authenticated data during SSR:
<template>
<div>
<div v-if="pending">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<div v-else>
<h1>Hello, {{ user?.name }}</h1>
<ul>
<li v-for="item in data" :key="item.id">
{{ item.title }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
const { user } = useAuth()
// This works on both server and client with authenticated SSR!
const { data, pending, error } = await useAsyncData(
'user-items',
() => $api('/api/user/items'),
{
server: true, // Enable server-side data fetching
}
)
</script>Benefits of Authenticated SSR
✅ True SSR - Pages render with user data on server
✅ Faster initial render - No loading states for authenticated data
✅ Secure - Access tokens never in HTML payload
✅ Works with all Nuxt patterns - useAsyncData, useFetch, $fetch
Performance Considerations
Authenticated SSR adds token validation and generation overhead (~10-50ms per request). The impact is logged in debug mode:
export default defineNuxtConfig({
nuxtAegis: {
logging: {
level: 'debug', // See SSR auth timing logs
}
}
})Best Practices
1. Server Routes with Authenticated SSR
With authenticated SSR enabled (default), server routes have access to both event.context.user and the $api instance with SSR tokens:
// server/routes/api/items.get.ts
export default defineEventHandler(async (event) => {
// Option 1: Use event.context.user (set by SSR plugin or auth middleware)
const user = event.context.user
if (!user) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
// Option 2: Use $api with SSR token (available during SSR)
const { $api } = useNuxtApp()
const items = await $api('/api/external/items') // Works during SSR!
return { user, items }
})Recommended: Use event.context.user from middleware for most cases. Only use $api when you need to call external authenticated endpoints during SSR.
2. Client-Side Data Fetching with $api
The $api plugin makes it seamless to fetch authenticated data on both server and client. It automatically uses the appropriate token for each context:
<template>
<div>
<h1>Your Dashboard</h1>
<div v-if="pending">Loading your data...</div>
<div v-else-if="error">{{ error.message }}</div>
<ul v-else>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<script setup>
const { $api } = useNuxtApp()
// $api automatically handles tokens for both SSR and client
// - On server: Uses event.context.ssrAccessToken (5 min lifetime)
// - On client: Uses in-memory access token (1 hour lifetime)
const { data: items, pending, error } = await useAsyncData('dashboard-items',
() => $api<Item[]>('/api/items'),
{
server: true, // Fetch during SSR with SSR token
}
)
</script>How it works:
- During SSR:
$apiuses the short-lived SSR token fromevent.context - On client:
$apiuses the long-lived access token from memory - Automatic refresh: A client API 401 triggers one refresh attempt; only success permits one replay
If you want to skip SSR and fetch only on the client:
const { data: items, pending, error } = await useAsyncData('dashboard-items',
() => $api<Item[]>('/api/items'),
{
server: false, // Skip server-side fetching
}
)Learn More About $api
See the $api Plugin Reference for detailed implementation information including:
- How automatic token refresh works
- SSR vs client token differences
- Error handling and retry logic
- Circular reference prevention
3. Protected Pages with Authenticated SSR
With authenticated SSR enabled (default), you can render protected content on the server:
<template>
<div>
<h1>Protected Page</h1>
<div v-if="!user">
<p>You must be logged in to view this page.</p>
<NuxtLink to="/auth/google">Log In</NuxtLink>
</div>
<div v-else>
<p>Welcome back, {{ user.name }}!</p>
<!-- Fetch and display user data during SSR -->
<div v-if="pending">Loading your data...</div>
<ul v-else>
<li v-for="item in items?.data" :key="item.id">
{{ item.title }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
const { user } = useAuth()
// Fetches on server with SSR token AND on client after hydration
const { data: items, pending } = await useAsyncData(
'protected-items',
() => $api('/api/protected/items'),
{
server: true, // Works with authenticated SSR!
}
)
</script>4. Public Routes
Public routes work seamlessly with SSR - no special handling needed:
<template>
<div>
<h1>Public Page</h1>
<p>This content is visible to everyone.</p>
<p v-if="user">Logged in as: {{ user.name }}</p>
</div>
</template>
<script setup>
const { user } = useAuth()
</script>5. Handling Loading and Resolution Failures
Loading and unresolved auth are separate. Without a verified identity, network/5xx or forbidden startup failures leave status unknown and expose an error, not guest state. Startup catches these failures so public content can render; protected-route middleware still propagates resolution failures and blocks navigation.
<template>
<div>
<p v-if="isLoading">Loading...</p>
<p v-else-if="user">Hello, {{ user.name }}!</p>
<p v-else-if="authStatus === 'unknown'">{{ error?.message || 'Session not checked' }}</p>
<p v-else>Please log in</p>
</div>
</template>
<script setup>
const { user, isLoading, authStatus, error } = useAuth()
</script>Retry unknown state with ensureResolved() and an established session with refresh(), handling rejection in either case. Refresh 401 clears auth; other refresh failures preserve existing identity. Preserved identity does not make an expired token valid or turn a failed API call into success. See client retry handling.
Security
Nuxt Aegis SSR implementation maintains strong security:
- ✅ No tokens in HTML: Access tokens are never exposed in server-rendered HTML
- ✅ httpOnly cookies: Refresh tokens remain in secure, httpOnly cookies
- ✅ In-memory tokens: Access tokens stored in memory only (cleared on refresh)
- ✅ HTTPS required: TLS encryption protects all token exchanges
- ✅ No XSS exposure: Tokens not accessible via JavaScript in HTML payload
Trade-offs
Advantages
- 🔒 Secure: No token exposure in HTML
- 🚀 Simple: Minimal configuration required
- Authenticated first render: Verified identity can be rendered on the server
- 🔄 Reactive: Authentication state updates automatically
Considerations
- Identity and tokens differ: Hydrated identity does not include a usable client access token
- Resolution failures: Unknown-with-error is distinct from loading and guest
- Client recovery: Explicit retry may be needed after transient failures; there is no background retry loop
Disabling SSR or Authenticated SSR
Disable Nuxt SSR Completely
If your application doesn't need SSR, you can disable it:
export default defineNuxtConfig({
ssr: false, // Disable SSR entirely (SPA mode)
nuxtAegis: {
// enableSSR is ignored when ssr: false
}
})Disable Only Authenticated SSR
Keep Nuxt SSR enabled but opt out of authenticated SSR (client-only token refresh):
export default defineNuxtConfig({
ssr: true, // Nuxt SSR enabled
nuxtAegis: {
enableSSR: false, // Disable authenticated SSR
}
})With this configuration:
- Pages still render on the server
- Authentication state is restored on the client after hydration
- No SSR access tokens are generated
useAsyncDatawithserver: truewill not have auth tokens during SSR