SSR & Hydration

How nuxt-bearer-auth handles Server-Side Rendering without auth flicker or token leakage.

Core Guides Nuxt 4 / TypeScript

How SSR Works in Nuxt Bearer Auth

In traditional client-side SPAs using localStorage, the server cannot know whether the visitor is logged in during SSR. This leads to:
  • Flash of unauthenticated content (layout flicker)
  • Inability to fetch private data during SSR
  • Broken SEO or layout shifts
  • nuxt-bearer-auth solves this completely using HTTP-only session cookies and Nitro server plugins.
    typescript
    1. Client requests page (e.g. /dashboard) with Session Cookie
                          │
                          ▼
    2. Nuxt Server Plugin (bearer-auth.server.ts)
       - Reads session cookie from H3Event
       - Queries Redis: GET session:<id>
       - Attaches { user: profile, status } to nuxtApp.payload.bearerAuth
                          │
                          ▼
    3. Nuxt Server-Side Renders HTML
       - Vue components see auth.isAuthenticated === true
       - useFetch() can query protected server routes during SSR
                          │
                          ▼
    4. HTML + Hydration Payload streamed to Browser
       - Client initializes state synchronously from payload
       - ZERO layout flicker, ZERO extra client network roundtrip
    

    The Server Plugin & Payload

    The package includes a dedicated server plugin (bearer-auth.server.ts) that runs once per SSR request:
    typescript
    // Inside nuxt-bearer-auth runtime server plugin
    export default defineNuxtPlugin(async (nuxtApp) => {
      if (false) return
    
      const event = nuxtApp.ssrContext?.event
      if (!event) return
    
      const auth = useBearerAuth()
    
      try {
        const session = await getBearerAuthSession(event)
        auth.setUser(session?.profile || null)
    
        nuxtApp.payload.bearerAuth = {
          user: session?.profile || null,
          status: session?.profile ? 'authenticated' : 'unauthenticated',
          abilities: session?.abilities || null,
        }
      } catch (error) {
        auth.clearAuthState()
      } finally {
        auth.setAuthReady(true)
        auth.setServerChecked()
      }
    })
    

    What the SSR Payload Exposes

    Understanding what is and is not included in the SSR payload is important for security and data handling.

    Included in nuxtApp.payload.bearerAuth (sent to browser):
  • user — the user profile object returned by your backend (e.g. { id, name, email, role, ... })
  • status — the authentication status string ("authenticated" or "unauthenticated")
  • abilities — the normalized ability strings from the Redis session (string[] | null); powers advisory client checks such as auth.can()
  • Stays server-side only (never sent to browser):
  • token — the bearer token
  • refreshToken — the refresh token
  • userId — the internal user identifier
  • ipAddress — the client IP at session creation
  • userAgent — the user agent at session creation
  • createdAt / expiresAt / lastActivity — session lifecycle metadata
  • Developer note: The user profile object (session.profile) is serialized into the Nuxt SSR payload and becomes visible in the browser's hydration data. Avoid including highly sensitive data (such as internal secrets, PII beyond what your UI requires, or credentials) inside the user profile returned from your backend. Design the /me endpoint to return only what the client genuinely needs.

    Zero Client Auth Flicker

    Because the server plugin runs before SSR rendering begins, Vue components and useFetch() calls within SSR have immediate access to the correct auth.user and auth.isAuthenticated values. The hydrated payload means the browser initializes with the same state synchronously — no loading spinner or unauthenticated flash on page load.

    Accessing Sessions in Server Routes

    When building custom Nitro API routes (server/api/...), you can read the authenticated session directly from the H3 event context:
    typescript
    // server/api/projects.get.ts
    import { requireBearerAuthSession, callAuthApi } from '#imports'
    
    export default defineEventHandler(async (event) => {
      // 1. Ensures user is logged in, or throws 401 Unauthenticated
      //    Reads from event.context.auth — no additional Redis round-trip
      const session = requireBearerAuthSession(event)
    
      // 2. Call external API using the user's server-held bearer token
      const projects = await callAuthApi('/v1/projects', {
        event,
        method: 'GET',
      })
    
      return {
        userId: session.userId,
        projects,
      }
    })
    
    requireBearerAuthSession(event) reads from event.context.auth, which is populated by the server middleware on every request. It does not make an additional Redis query. callAuthApi() automatically retrieves the bearer token from the Redis session and injects it as an Authorization: Bearer header on the outgoing request to your backend. The token is never passed to or through the browser.

    Server-Side Route Middleware

    Because the session is hydrated before route middleware executes on SSR, unauthenticated requests to protected pages are redirected on the server before any protected HTML is rendered. On client-side navigation, the same middleware logic runs in the browser using the already-hydrated auth state.