Skip to content

[Bug] Custom tokenStorage in app.config.ts is stripped during nuxt generate (static builds) #526

@SDIjeremy

Description

@SDIjeremy

Describe the bug

Custom tokenStorage defined in app.config.ts (as documented in Token Storage) is silently stripped during nuxt generate builds. The get and set functions cannot survive JSON serialization, so the entire tokenStorage object is dropped from the compiled app config. The module then falls back to cookie-based token storage, which doesn't work correctly in Capacitor WebViews.

This directly affects the primary use case for token mode — Capacitor/Ionic mobile apps — since these apps require nuxt generate to produce static files for the native WebView.

To Reproduce

  1. Create a Nuxt app with ssr: false and nitro: { preset: 'static' } (standard Capacitor setup)
  2. Configure sanctum in token mode with a custom tokenStorage in app.config.ts as per the docs:
// app.config.ts
import { Capacitor } from '@capacitor/core'

export default defineAppConfig({
  sanctum: {
    tokenStorage: Capacitor.isNativePlatform()
      ? {
          async get() {
            const { Preferences } = await import('@capacitor/preferences')
            const result = await Preferences.get({ key: 'sanctum.token' })
            return result.value ?? undefined
          },
          async set(_app: unknown, token?: string) {
            const { Preferences } = await import('@capacitor/preferences')
            if (token) {
              await Preferences.set({ key: 'sanctum.token', value: token })
            } else {
              await Preferences.remove({ key: 'sanctum.token' })
            }
          },
        }
      : undefined,
  },
})
  1. Run NATIVE_APP=true nuxt generate
  2. Inspect .nuxt/app.config.mjs — the compiled output is:
const inlineConfig = {
  "nuxt": {}
}
// No sanctum.tokenStorage — functions were stripped during serialization
export default /*@__PURE__*/ defuFn(inlineConfig)
  1. Grep the built JS bundles in .output/public/_nuxt/ for "Preferences" — zero matches. The entire Capacitor token storage branch is dead-code-eliminated because the serialized config resolved to undefined.

  2. Login succeeds (POST to login endpoint returns a token), but all subsequent requests are unauthenticated because the token was never stored.

Expected behavior

The custom tokenStorage functions defined in app.config.ts should be available at runtime in the built application, allowing token-based authentication to work in Capacitor/Ionic apps built with nuxt generate.

Actual behavior

nuxt generate evaluates app.config.ts at build time and serializes the result into inlineConfig. Since JavaScript functions are not JSON-serializable, the tokenStorage.get and tokenStorage.set methods are silently dropped. The compiled .nuxt/app.config.mjs contains no reference to the user's sanctum config.

At runtime, useSanctumAppConfig().tokenStorage is undefined, so the plugin falls back to cookieTokenStorage. In a Capacitor WebView loading static files, cookie-based token storage doesn't persist correctly, causing all authenticated requests to fail.

This only affects production builds (nuxt generate). In dev mode (nuxt dev), Vite's HMR imports app.config.ts as a live module — functions are preserved and everything works correctly.

Module information

  • Version: 2.1.3
  • Content of your nuxt.config.ts:
export default defineNuxtConfig({
  ssr: false,
  modules: ['nuxt-auth-sanctum'],

  sanctum: {
    mode: 'token',
    endpoints: {
      login: '/login/mobile',
      logout: '/logout/mobile',
    },
    redirect: {
      keepRequestedRoute: true,
      onLogin: '/',
      onLogout: '/login',
      onAuthOnly: '/login',
      onGuestOnly: '/',
    },
  },

  nitro: {
    preset: 'static',
    output: {
      dir: '.output',
      publicDir: '.output/public',
    },
  },
})

Logs

No runtime errors are logged. The module silently falls back to cookie storage:

[nuxt-auth-sanctum] Token storage is not defined, switch to default cookie storage

This is the only indication that the custom storage was not loaded.

Additional context

Root cause

Nuxt's app.config.ts is designed for serializable configuration values. During nuxt generate, the file is evaluated at build time and only JSON-serializable values are preserved in the inlineConfig object within .nuxt/app.config.mjs. Functions, class instances, and other non-serializable values are silently dropped.

The Token Storage documentation recommends placing tokenStorage (which contains get/set functions) in app.config.ts. This works in dev mode but not in production builds using nuxt generate — which is the standard build mode for the Capacitor/Ionic apps that token mode was designed for (Feature #33).

Workaround

Use a Nuxt client plugin instead of app.config.ts to register the tokenStorage at runtime via updateAppConfig():

// plugins/sanctum-storage.client.ts
export default defineNuxtPlugin(() => {
  updateAppConfig({
    sanctum: {
      tokenStorage: {
        async get() {
          const { Preferences } = await import('@capacitor/preferences')
          const result = await Preferences.get({ key: 'sanctum.token' })
          return result.value ?? undefined
        },
        async set(_app: unknown, token?: string) {
          const { Preferences } = await import('@capacitor/preferences')
          if (token) {
            await Preferences.set({ key: 'sanctum.token', value: token })
          } else {
            await Preferences.remove({ key: 'sanctum.token' })
          }
        },
      },
    },
  })
})

This works because plugins are compiled by Vite into the JS bundle (functions survive), and updateAppConfig() overwrites the cookie storage fallback set by the module's plugin. Since the initial identity fetch is deferred to the page:loading:start hook, the override is in place before any authenticated requests are made.

Suggestion

The documentation should recommend the plugin approach for Capacitor/Ionic apps that use nuxt generate, or the module could provide a built-in mechanism (e.g., a tokenStorage option accepted as a plugin registration) that doesn't rely on app.config.ts serialization.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdocumentationImprovements or additions to documentationinvestigateRequires additional investigation

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions