nuxt-safe-runtime-config
nuxt-safe-runtime-config

Validate Nuxt runtime config with Standard Schema at build time

Nuxt safe runtime config logo
Nuxt Safe Runtime Config

Validate Nuxt runtime config at build time using Zod, Valibot, ArkType, or any Standard Schema compatible library.


npm version npm downloads License Nuxt compatibility

🔗 Related Nuxt RFC: Enable Standard Schema Validation in Nuxt Config

Features

  • 🔒 Build-time validation with Zod, Valibot, ArkType, or any Standard Schema library
  • 🚀 Runtime validation (opt-in) validates config when the server starts
  • Auto-generated typesuseSafeRuntimeConfig() is fully typed without manual generics
  • 🛠 ESLint plugin warns when using useRuntimeConfig() instead of the type-safe composable
  • Zero runtime overhead by default — validation happens at build time only
  • 🔐 Shelve integration — fetch secrets from Shelve and merge into runtime config

Quick Setup

Install the module:

npx nuxi module add nuxt-safe-runtime-config

Compatibility

This module supports Nuxt 3 and Nuxt 4. It is also tested against Nuxt 4.2+ with future.compatibilityVersion: 5 and against the Nuxt 5 nightly channel (nuxt-nightly@5x).

Inline runtime schemas are converted to JSON Schema. Importable schema modules can instead execute the original Standard Schema at runtime, including transformations.

Usage

1. Define your schema

Use Zod, Valibot, ArkType, or any Standard Schema compatible library:

With Valibot
import { number, object, optional, string } from 'valibot'

const runtimeConfigSchema = object({
  public: object({
    apiBase: string(),
    appName: optional(string()),
  }),
  databaseUrl: string(),
  secretKey: string(),
  port: optional(number()),
})
With Zod
import { z } from 'zod'

const runtimeConfigSchema = z.object({
  public: z.object({
    apiBase: z.string(),
    appName: z.string().optional(),
  }),
  databaseUrl: z.string(),
  secretKey: z.string(),
  port: z.number().optional(),
})
With ArkType
import { type } from 'arktype'

const runtimeConfigSchema = type({
  'public': {
    'apiBase': 'string',
    'appName?': 'string'
  },
  'databaseUrl': 'string',
  'secretKey': 'string',
  'port?': 'number'
})

2. Configure in nuxt.config.ts

export default defineNuxtConfig({
  modules: ['nuxt-safe-runtime-config'],

  runtimeConfig: {
    databaseUrl: process.env.DATABASE_URL || 'postgresql://localhost:5432/mydb',
    secretKey: process.env.SECRET_KEY || 'default-secret-key',
    port: Number.parseInt(process.env.PORT || '3000'),
    public: {
      apiBase: process.env.PUBLIC_API_BASE || 'https://api.example.com',
      appName: 'My Nuxt App',
    },
  },

  safeRuntimeConfig: {
    $schema: runtimeConfigSchema,
  },
})

3. Use the type-safe composable

Access your validated config with full type safety — types are auto-generated from your schema:

<script setup lang="ts">
const config = useSafeRuntimeConfig()
// config.public.apiBase is typed as string
// config.secretKey is typed as string
</script>

You can also use the same composable in server code:

// server/utils/config.ts
export function getPrivateConfig() {
  const config = useSafeRuntimeConfig()
  return {
    secretKey: config.secretKey,
    apiBase: config.public.apiBase,
  }
}

If your editor runs type-checking outside Nuxt's auto-import context, import it explicitly:

import { useSafeRuntimeConfig } from '#imports'

Configuration Options

OptionTypeDefaultDescription
$schemaStandardSchemaV1 | stringSchema or path to its default-export module
validateAtBuildbooleantrueValidate during dev/build
validateAtRuntimebooleanfalseValidate when server starts
onError'throw' | 'warn' | 'ignore''throw'How to handle validation errors
jsonSchemaTargetstring'draft-2020-12'JSON Schema version for runtime validation
shelveboolean | ShelveOptionsundefinedShelve secrets integration (see below)

Shelve Integration

Shelve is a secrets management service. This module fetches secrets from Shelve at build time and merges them into your runtime config before validation.

Configure Shelve

Configure Shelve directly in your Nuxt config:

export default defineNuxtConfig({
  safeRuntimeConfig: {
    $schema: runtimeConfigSchema,
    shelve: {
      project: 'my-app',
      slug: 'my-team',
    },
  },
})

The module resolves configuration from multiple sources (highest priority first):

ConfigSources
projectnuxt.configSHELVE_PROJECTpackage.json name
slugnuxt.config.slug/teamSHELVE_TEAMSHELVE_TEAM_SLUG
environmentnuxt.configSHELVE_ENV → dev mode auto
urlnuxt.configSHELVE_URLhttps://app.shelve.cloud
tokenSHELVE_TOKEN~/.shelve

Explicit Configuration

You can override any auto-detected value:

export default defineNuxtConfig({
  safeRuntimeConfig: {
    $schema: runtimeConfigSchema,
    shelve: {
      project: 'my-app',
      slug: 'my-team',
      environment: 'production',
      url: 'https://app.shelve.cloud', // Self-hosted Shelve
      fetchAtBuild: true, // Default: fetch at build time
      fetchAtRuntime: false, // Opt-in: fetch on server cold start
    },
  },
})

Variable Transformation

Shelve variables transform from SCREAMING_SNAKE_CASE to camelCase with smart grouping:

DATABASE_URL           → databaseUrl
GITHUB_CLIENT_ID       → github.clientId (grouped)
GITHUB_CLIENT_SECRET   → github.clientSecret (grouped)
PUBLIC_API_URL         → public.apiUrl

Variables with repeated prefixes (2+ keys) nest automatically. PUBLIC_* and NUXT_PUBLIC_* map to runtimeConfig.public.

Runtime Fetch (Opt-in)

For dynamic environments or secret rotation, enable runtime fetching:

export default defineNuxtConfig({
  safeRuntimeConfig: {
    shelve: {
      fetchAtBuild: true, // Bake secrets into build
      fetchAtRuntime: true, // Also refresh on cold start
    },
  },
})

The runtime plugin runs before validation, so freshly fetched secrets are validated against your schema.

Install Wizard UX

On module install, an interactive setup wizard can help bootstrap validation and Shelve config. The wizard now:

  • shows a preview of planned actions first (install deps, write ~/.shelve, edit nuxt.config)
  • asks for a final confirmation before applying any change
  • skips automatically in CI and non-interactive terminals (non-TTY)

Runtime Validation

By default, validation only runs at build time. Inline schemas can opt into JSON Schema validation when the server starts:

nuxt.config.ts
export default defineNuxtConfig({
  safeRuntimeConfig: {
    $schema: runtimeConfigSchema,
    validateAtRuntime: true,
    onError: 'throw',
  },
})

This path uses @cfworker/json-schema after Nitro applies environment overrides. JSON Schema validates the resulting shape, but it cannot execute Standard Schema transformations.

Preserve Runtime Transformations

Runtime environment overrides often arrive as strings. Move the schema into a default-exported module when the validated output must contain canonical booleans, numbers, or other transformed values:

runtime-config.schema.ts
import * as v from 'valibot'

const runtimeBoolean = v.pipe(
  v.union([v.boolean(), v.picklist(['true', 'false'])]),
  v.transform(value => value === true || value === 'true'),
  v.boolean(),
)

const runtimeNumber = v.union([
  v.number(),
  v.pipe(v.string(), v.decimal(), v.transform(Number), v.number()),
])

export default v.object({
  perfTrace: v.object({
    enabled: runtimeBoolean,
    token: v.string(),
  }),
  public: v.object({
    replaySampleRate: runtimeNumber,
  }),
})

Pass the project-relative module path instead of the inline schema object:

nuxt.config.ts
export default defineNuxtConfig({
  safeRuntimeConfig: {
    $schema: './runtime-config.schema',
    validateAtRuntime: true,
    onError: 'throw',
  },
})

Consumers receive the transformed output and do not need to repeat coercion:

server/plugins/perf-trace.ts
const config = useSafeRuntimeConfig()

if (config.perfTrace.enabled)
  startTracing()

The server executes the schema once after Nitro applies runtime overrides. Request handlers wait for asynchronous schemas, and transformed public values are serialized into the Nuxt client payload.

!IMPORTANT Use a string $schema path when runtime behavior depends on transformations. Inline schemas retain the JSON Schema validation path for backward compatibility and do not execute transformations.

Runtime validation catches:

  • Environment variables with wrong types (e.g., NUXT_PORT=abc when expecting a number)
  • Missing required environment variables in production
  • Invalid values that pass build-time checks but fail at runtime

Shelve runtime fetching is separate from validation ownership. If shelve.fetchAtRuntime is enabled, the Shelve runtime plugin runs before validation so fetched secrets are included in the validated config.

ESLint Integration

The module includes an ESLint plugin that warns when using useRuntimeConfig() instead of useSafeRuntimeConfig().

With @nuxt/eslint (Automatic)

If you use @nuxt/eslint, the rule is auto-registered. No configuration needed.

Manual Setup

Add to your eslint.config.mjs:

import { configs } from 'nuxt-safe-runtime-config/eslint'

export default [
  configs.recommended,
  // ... your other configs
]

Or configure manually:

import plugin from 'nuxt-safe-runtime-config/eslint'

export default [
  {
    plugins: { 'safe-runtime-config': plugin },
    rules: { 'safe-runtime-config/prefer-safe-runtime-config': 'warn' },
  },
]

The rule includes auto-fix support — run eslint --fix to automatically replace useRuntimeConfig() calls.

Type Safety

Types are generated from JSON Schema for inline schemas. Schema modules use their Standard Schema input type for build-only validation and switch to the output type when validateAtRuntime is enabled. Build-only consumers therefore account for untransformed overrides, while successful runtime validation exposes the transformed output type.

The composable returns a fully typed object in both app and server contexts (app.vue, server/api, server/utils) without manual generics:

const config = useSafeRuntimeConfig()
// config is fully typed based on your schema

Generated types are stored in .nuxt/types/safe-runtime-config.d.ts and automatically included in your project.

Error Messages

When validation fails, you see detailed error messages:

[safe-runtime-config] Validation failed!
  1. databaseUrl: Invalid type: Expected string but received undefined
  2. public.apiBase: Invalid type: Expected string but received undefined
  3. port: Invalid type: Expected number but received string

The module stops the build process until all validation errors are resolved.

Upcoming Major Release Notes

  • Shelve setup no longer documents shelve.json auto-enablement; supported sources are nuxt.config, env vars, and package.json fallback for project name.
  • The install wizard now previews actions and requires explicit confirmation before mutating files or writing credentials.
  • Runtime and wizard key-shaping now use the same env-key mapping rules to avoid schema/runtime drift.

Why This Module?

Nuxt's built-in schema validation is designed for module authors and broader configuration. This module focuses specifically on runtime config validation using Standard Schema, allowing you to:

  • Use your preferred validation library (Valibot, Zod, ArkType)
  • Catch configuration errors at build time
  • Optionally validate at runtime for environment variable issues
  • Get full type safety in your components

Contribution

Local development
# Install dependencies
pnpm install

# Generate type stubs
pnpm run dev:prepare

# Develop with the playground
pnpm run dev

# Build the playground
pnpm run dev:build

# Run ESLint
pnpm run lint

# Run Vitest
pnpm run test
pnpm run test:watch

# Release new version
pnpm run release