Manage Environment Variables in Frontend Builds
Frontend frameworks distinguish between build-time and runtime environment variables. Getting this wrong causes variables to be undefined in the browser or, worse, secrets to leak into client-side JavaScript bundles.
This guide covers how each major framework handles environment variables and what to account for when deploying to Railway.
Build-time vs runtime
Build-time variables are injected into the JavaScript bundle during npm run build. They become static strings in the output files. Changing a build-time variable requires a new build and deploy.
Runtime variables are read by server-side code at request time. They are never shipped to the browser. Changing a runtime variable takes effect on the next request without rebuilding.
On Railway, all service variables are available during both the build step and at runtime. The framework's prefix convention determines which variables are exposed to client-side code in the bundle.
Framework prefix conventions
| Framework | Client-side prefix | Server-only | How to access in client code |
|---|---|---|---|
| Vite (React, Vue, Solid) | VITE_ | No prefix | import.meta.env.VITE_API_URL |
| Next.js | NEXT_PUBLIC_ | No prefix | process.env.NEXT_PUBLIC_API_URL |
| Nuxt | NUXT_PUBLIC_ in runtimeConfig.public | runtimeConfig (no prefix) | useRuntimeConfig().public.apiUrl |
| SvelteKit | PUBLIC_ | No prefix | import { env } from '$env/static/public' |
| Astro | PUBLIC_ | No prefix | import.meta.env.PUBLIC_API_URL |
| Angular | N/A (uses environment.ts files) | N/A | environment.apiUrl |
| Gatsby | GATSBY_ | No prefix | process.env.GATSBY_API_URL |
| Remix | None (use loaders) | All env vars | Pass from loader to component |
Setting variables in Railway
- Go to your service in the Railway dashboard.
- Open the Variables tab.
- Add your variables with the correct prefix for your framework.
Variables set here are available during both the build and at runtime. See Variables for more details.
When a build-time variable changes, you must redeploy. Railway triggers a new deploy when you update a variable, which rebuilds the app with the new value baked in.
Reference variables across services
If your frontend needs to know your backend's public URL, use a reference variable instead of hardcoding it:
This keeps the value in sync when domains change.
Server-side variables in SSR frameworks
SSR frameworks (Next.js, Nuxt, SvelteKit, Remix, Astro in SSR mode) run server code that can access all environment variables, not just prefixed ones.
In Next.js, Server Components and Route Handlers can read process.env.DATABASE_URL directly. This value is never sent to the browser. Only variables with the NEXT_PUBLIC_ prefix are included in the client bundle.
In Nuxt, runtimeConfig values (without the public key) are server-only. In SvelteKit, $env/static/private and $env/dynamic/private are server-only.
Common mistakes
Variable is undefined in the browser
Symptoms: import.meta.env.API_URL returns undefined in the browser, but the variable is set in Railway.
Cause: The variable is missing the framework-specific prefix. Without the prefix, the build tool strips it from the client bundle as a security measure.
Fix: Rename the variable with the correct prefix (e.g., VITE_API_URL for Vite, NEXT_PUBLIC_API_URL for Next.js) and redeploy.
Secrets leaked into the client bundle
Symptoms: Database credentials, API secret keys, or other sensitive values are visible in the browser's JavaScript source files.
Cause: A secret was given a client-side prefix (e.g., NEXT_PUBLIC_DATABASE_URL).
Fix: Remove the public prefix from secret variables. Access them only in server-side code (API routes, server components, loaders). Rotate the compromised credentials immediately.
To audit your bundle for leaked values, search the build output:
Variable unchanged after updating in Railway
Symptoms: The old value still appears in the browser after updating the variable in the Railway dashboard.
Cause: Build-time variables are baked into the JavaScript bundle. If Railway's deploy completed but a CDN or browser cache serves the old bundle, the old value persists.
Fix: Clear your CDN cache if applicable. For browser caching, ensure your build tool uses content hashes in filenames (Vite does this by default).
Pattern: Runtime config injection for SPAs
For SPAs served as static files (no SSR server), all variables are build-time by default. If you need to change a variable without rebuilding, inject it at serve time.
This pattern serves a small script that reads from the server environment:
- Create a server endpoint or template that injects environment values:
- Include the script in your
index.htmlbefore your app bundle:
- Read from
window.__envin your app code instead ofimport.meta.env.
This approach is also useful when you want the same Docker image to work across multiple Railway environments without rebuilding. For more detail on this pattern, see Skipped Builds.
Next steps
- Variables - Configure service variables and reference variables.
- Configure SPA routing - Set up Caddy or Nginx fallback routing.
- Skipped Builds - How Railway handles build caching and when build-time variables cause issues.
- Deploying a Monorepo - Share variables between frontend and backend services.