Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 047 / 100

Environment variables in Nuxt for Vue & Nuxt developers

Runtime config done right.

Expona Admin6 min read

Introduction

Managing environment variables across development, staging, and production environments is one of those foundational tasks that feels simple until it isn't. In Nuxt, environment variables in Nuxt for Vue & Nuxt developers go beyond just reading process.env—there's a proper way to handle configuration that keeps secrets secure, makes values accessible where they're needed, and prevents the kind of deployment disasters that happen at 3 AM.

We've all shipped a feature that worked locally but failed in production because an API endpoint was hardcoded or a secret leaked into the client bundle. Nuxt's runtimeConfig system solves this by giving us a structured, opinionated approach to managing configuration across server and client boundaries.

Understanding runtimeConfig in nuxt.config

The foundation of Nuxt's environment variable system is runtimeConfig, defined in your nuxt.config.ts file. This object lets you declare configuration values and how they should be exposed to your application.

Here's a practical example:

typescript
export default defineNuxtConfig({
  runtimeConfig: {
    // Private keys that are only available server-side
    apiSecret: '',
    databaseUrl: '',
    stripeSecretKey: '',

    // Public keys that are exposed to the client
    public: {
      apiBase: '',
      env: '',
      version: ''
    }
  }
})

The distinction matters. Keys at the root level of runtimeConfig are private—they're only available on the server and never bundled with client code. Keys under runtimeConfig.public are exposed to the browser and should contain only non-sensitive values.

This structure prevents accidental exposure of secrets while still making public configuration available everywhere you need it. When we say "environment variables in Nuxt for Vue & Nuxt developers," this runtime config pattern is the backbone we're building on.

Public keys for client-side values

Public configuration values are essential for client-side code that needs to know about API endpoints, feature flags, or environment names. A common scenario: your Vue components need to call an API, and the endpoint differs between environments.

Define these under runtimeConfig.public:

typescript
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      apiBase: 'https://api.example.com',
      analyticsId: '',
      featureFlags: {
        newCheckout: false,
        betaUI: false
      }
    }
  }
})

Access them in your Vue components using useRuntimeConfig():

typescript
<script setup lang="ts">
const config = useRuntimeConfig()
const apiBase = config.public.apiBase
const { featureFlags } = config.public

const fetchUser = async (id: string) => {
  const response = await $fetch(`${apiBase}/users/${id}`)
  return response
}

if (featureFlags.newCheckout) {
  // Render new checkout flow
}
</script>

This approach works in server routes, middleware, and components. The key is that these values are baked into the bundle at build time (they're not secrets), so every client has access to them.

A real-world example from an e-commerce project: you're integrating Google Analytics, Segment, or a similar service. The public tracking ID needs to be in client-side code. Put it in runtimeConfig.public, not in a hardcoded string in your component.

Override with NUXT_ environment variables

The magic happens when you pair runtimeConfig with environment variables. Nuxt automatically maps environment variables prefixed with NUXT_ to your runtime config.

For private keys, use the format NUXT_ in uppercase with underscores:

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    apiSecret: '',
    databaseUrl: '',
    stripe: {
      secretKey: ''
    }
  }
})

// In your shell environment or .env file:
// NUXT_API_SECRET=your-secret-here
// NUXT_DATABASE_URL=postgres://...
// NUXT_STRIPE_SECRET_KEY=sk_live_...

For public keys, nest them under the public prefix:

typescript
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      apiBase: '',
      env: ''
    }
  }
})

// In your environment:
// NUXT_PUBLIC_API_BASE=https://api.prod.example.com
// NUXT_PUBLIC_ENV=production

Nuxt reads these at build time (for static values) and startup time (for runtime overrides). This means you can build a single Docker image for all environments and inject different values via environment variables—the foundation of proper CI/CD.

A practical deployment workflow: your CI/CD pipeline builds once, then deploys to staging with NUXT_PUBLIC_API_BASE=https://api-staging.example.com and to production with NUXT_PUBLIC_API_BASE=https://api.example.com. No rebuilds, no environment-specific artifacts.

Never expose secrets in public config

This is where mistakes happen. We've seen production incidents where an API key, database password, or OAuth secret ended up in client-side code. It only takes once to understand why this matters.

The rule is simple: anything under runtimeConfig.public is exposed to the browser. Treat it as public information.

Bad example:

typescript
// ❌ DO NOT DO THIS
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      stripeSecretKey: '',  // Secret exposed to client
      databasePassword: '',  // Database password in browser
      jwtSecret: ''          // Authentication secret leaked
    }
  }
})

Correct pattern:

typescript
// ✅ CORRECT
export default defineNuxtConfig({
  runtimeConfig: {
    // Server-only (never exposed)
    stripe: {
      secretKey: ''
    },
    database: {
      password: ''
    },
    auth: {
      jwtSecret: ''
    },

    // Client-accessible (public data only)
    public: {
      stripe: {
        publicKey: ''  // Stripe publishable key is meant to be public
      },
      apiBase: ''
    }
  }
})

If you need a secret value in client-side code, the right approach is a server route or API endpoint that validates the request and returns the necessary data. For example, don't expose your Stripe secret key—create a server endpoint that initializes Stripe and returns a client token.

Common mistakes

Mixing up NUXT_ prefixes: Forgetting that public keys need NUXT_PUBLIC_ prefix. If your config has runtimeConfig.public.apiBase, the env var is NUXT_PUBLIC_API_BASE, not NUXT_API_BASE.

Checking environment variables at import time: Relying on process.env directly instead of useRuntimeConfig(). The runtime config system is designed to handle values that change between deployments; direct env var access can behave unpredictably in different contexts.

Forgetting about nested keys: If you have runtimeConfig.services.email.apiKey, the env var is NUXT_SERVICES_EMAIL_API_KEY. Nested structures translate to underscore-separated uppercase names.

Building with secrets in CI/CD: Embedding environment variables at build time when they should be injected at runtime. This prevents the same build artifact from running in multiple environments. Use environment variables at startup, not at build time, for secrets that differ per environment.

Exposing secrets by accident in config files: Committing .env files with real secrets to version control. Always use .env.example as a template and add .env* to .gitignore.

Key takeaways

  • Define your configuration structure in runtimeConfig with private keys at the root and public values under runtimeConfig.public
  • Override runtime config values using environment variables with the NUXT_ prefix (or NUXT_PUBLIC_ for client-side values)
  • Access configuration in your app via useRuntimeConfig(), not by reading process.env directly
  • Treat anything in runtimeConfig.public as visible to everyone—never put secrets there
  • Build once, deploy many: inject environment-specific values at runtime, not at build time, to support multiple environments with the same artifact
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles

Environment variables in Nuxt | Vue & Nuxt | Expona