onmax/nuxt-safe-runtime-config(opens in a new tab)
Language: TypeScript
Validate Nuxt runtime config using Zod, Valibot, ArkType... any Standard Schema library
- stars
- 33
- watchers
- 0
- forks
- 0
- open issues
- 1
- Last updated
- 10/29/2026
Language: TypeScript
Validate Nuxt runtime config using Zod, Valibot, ArkType... any Standard Schema library
Validate Nuxt runtime config at build time using Zod, Valibot, ArkType, or any Standard Schema compatible library.
🔗 Related Nuxt RFC: Enable Standard Schema Validation in Nuxt Config
useSafeRuntimeConfig() is fully typed without manual genericsuseRuntimeConfig() instead of the type-safe composableInstall the module:
npx nuxi module add nuxt-safe-runtime-config
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.
Use Zod, Valibot, ArkType, or any Standard Schema compatible library:
With Valibotimport { number, object, optional, string } from 'valibot' const runtimeConfigSchema = object({ public: object({ apiBase: string(), appName: optional(string()), }), databaseUrl: string(), secretKey: string(), port: optional(number()), })
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(), })
import { type } from 'arktype' const runtimeConfigSchema = type({ 'public': { 'apiBase': 'string', 'appName?': 'string' }, 'databaseUrl': 'string', 'secretKey': 'string', 'port?': 'number' })
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, }, })
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, } }
Pass the request event for the validated equivalent of useRuntimeConfig(event):
// server/api/token.get.ts export default defineEventHandler((event) => { const { secretKey } = useSafeRuntimeConfig(event) return sign(secretKey) })
On Nitro 3, which has no per-request runtime config, this returns the same config as useSafeRuntimeConfig().
If your editor runs type-checking outside Nuxt's auto-import context, import it explicitly:
import { useSafeRuntimeConfig } from '#imports'
| Option | Type | Default | Description |
|---|---|---|---|
$schema |
StandardSchemaV1 | string |
— | Schema or path to its default-export module |
validateAtBuild |
boolean |
!validateAtRuntime |
Validate during dev/build |
validateAtRuntime |
boolean |
false |
Coerce and validate when server starts |
onError |
'throw' | 'warn' | 'ignore' |
'throw' |
How to handle validation errors |
jsonSchemaTarget |
string |
'draft-2020-12' |
JSON Schema version for runtime validation |
shelve |
boolean | ShelveOptions |
undefined |
Shelve secrets integration (see below) |
A committed .env.example can declare runtime config and its generated types. Enable it explicitly:
export default defineNuxtConfig({ modules: ['nuxt-safe-runtime-config'], safeRuntimeConfig: { envSchema: '.env.example', }, })
Preparation and builds require the schema file, without deployment variables. The built server validates injected NUXT_* values at startup. Assignments are real defaults; @example values are documentation only.
See the environment schema guide, supported profile, and runnable deployment lab. The lab checks generated consumer types, runtime restarts, invalid deployments and secret leakage with pnpm test:lab:env-schema.
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 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):
| Config | Sources |
|---|---|
| project | nuxt.config → SHELVE_PROJECT → package.json name |
| slug | nuxt.config.slug/team → SHELVE_TEAM → SHELVE_TEAM_SLUG |
| environment | nuxt.config → SHELVE_ENV → dev mode auto |
| url | nuxt.config → SHELVE_URL → https://app.shelve.cloud |
| token | SHELVE_TOKEN → ~/.shelve |
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 }, }, })
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.
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.
On module install, an interactive setup wizard can help bootstrap validation and Shelve config. The wizard now:
~/.shelve, edit nuxt.config)By default, validation only runs at build time. Build validation checks the values written in nuxt.config.ts; runtime validation checks the values the deployed server runs with. Enabling validateAtRuntime turns build validation off unless you set validateAtBuild: true, so builds do not need placeholder secrets. This applies to both the Nuxt module and the standalone Nitro module.
Inline schemas can opt into JSON Schema validation when the server starts:
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.
Environment overrides arrive as strings (Nitro 3) or partially parsed (Nitro 2 turns 1 into a number). Before validating, the module converts each override to the type the schema expects:
boolean: 'true', 'false', '1', '0' (any case), 1, 0number: numeric strings such as '0.5'; integer: integral strings such as '3000'string: integers and booleans that Nitro 2 already parsed, such as 123'["email","github"]'Only values the schema would otherwise reject are converted, so fields that accept strings are left alone. A plain boolean() field is enough, and useSafeRuntimeConfig() returns real booleans and numbers on the server and in the client payload. The validated values are also written back into the per-request useRuntimeConfig(event) on Nitro 2 and the shared config on Nitro 3.
Move the schema into a default-exported module when the validated output needs parsing beyond these coercions:
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:
export default defineNuxtConfig({ safeRuntimeConfig: { $schema: './runtime-config.schema', validateAtRuntime: true, onError: 'throw', }, })
Consumers receive the transformed output and do not need to repeat coercion:
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.
Schema modules are coerced against their input JSON Schema when one can be derived. Schemas with transforms usually have no JSON Schema form and receive the raw values.
Runtime validation catches:
NUXT_PORT=abc when expecting a number)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.
The module includes an ESLint plugin that warns when using useRuntimeConfig() instead of useSafeRuntimeConfig().
If you use @nuxt/eslint, the rule is auto-registered. No configuration needed.
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. useRuntimeConfig(event) becomes the event-scoped useSafeRuntimeConfig(event).
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.
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.
shelve.json auto-enablement; supported sources are nuxt.config, env vars, and package.json fallback for project name.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:
# 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
Releases go through uppt. Every push to main updates a draft release/vX.Y.Z PR built from conventional commits. Merging it tags the version, creates the GitHub release, and publishes the package to npm through OIDC trusted publishing. If publishing fails after the tag exists, run gh workflow run release.yml --ref vX.Y.Z.
Use .env for values and a committed .env.example for types and defaults:
export default defineNuxtConfig({ modules: ['nuxt-safe-runtime-config'], safeRuntimeConfig: { envSchema: '.env.example' }, })
useSafeRuntimeConfig() provides the declared types after Runtime validation. Strings, booleans, numbers, enums, URLs, JSON arrays and object shapes are supported. The existing $schema API remains available as an alternative schema source. See the environment schema guide.