dges the gap between the browser context and the application state.
1. The HTML Injection Point
The configuration file must be loaded synchronously before the application bundle executes. This ensures window properties are available when the framework initializes.
Implementation:
Modify index.html to include a script tag referencing a static configuration file. The path must be accessible via the web server root.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Application</title>
<!-- Runtime Configuration Bridge -->
<!-- Must load before main bundle to prevent race conditions -->
<script src="/runtime-config.js"></script>
</head>
<body>
<div id="app-root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
2. TypeScript Contract and Global Declaration
To maintain type safety, define the shape of the runtime configuration and augment the global Window interface. This prevents any types and provides IDE autocompletion.
// src/types/runtime-config.ts
export interface RuntimeConfig {
apiGateway: string;
authProvider: string;
featureFlags: {
enableNewCheckout: boolean;
betaAnalytics: boolean;
};
environment: 'development' | 'staging' | 'production';
}
// Augment global Window interface
declare global {
interface Window {
__APP_RUNTIME_CONFIG__?: RuntimeConfig;
}
}
export {};
3. The Runtime Loader
Create a module that reads the configuration from window. This module should handle validation and provide a safe accessor.
// src/config/loader.ts
import type { RuntimeConfig } from '../types/runtime-config';
export function getRuntimeConfig(): RuntimeConfig {
const config = window.__APP_RUNTIME_CONFIG__;
if (!config) {
throw new Error(
'Runtime configuration is missing. Ensure /runtime-config.js is loaded.'
);
}
// Optional: Validate required fields at runtime
if (!config.apiGateway) {
throw new Error('Runtime config missing required field: apiGateway');
}
return config;
}
// Singleton pattern for consistent access
let cachedConfig: RuntimeConfig | null = null;
export function useConfig(): RuntimeConfig {
if (!cachedConfig) {
cachedConfig = getRuntimeConfig();
}
return cachedConfig;
}
4. Infrastructure Mounting
The configuration file is generated or mounted at deployment time. The Docker image remains environment-agnostic.
Docker Compose Example:
services:
frontend:
image: registry.internal/my-org/frontend:sha-abc123
ports:
- "8080:80"
volumes:
# Mount environment-specific config into the static assets directory
- ./config/${TARGET_ENV}/runtime-config.js:/usr/share/nginx/html/runtime-config.js:ro
Kubernetes ConfigMap Example:
apiVersion: v1
kind: ConfigMap
metadata:
name: frontend-runtime-config
data:
runtime-config.js: |
window.__APP_RUNTIME_CONFIG__ = {
apiGateway: "https://api.staging.example.com",
authProvider: "https://auth.staging.example.com",
featureFlags: { enableNewCheckout: true, betaAnalytics: false },
environment: "staging"
};
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
template:
spec:
containers:
- name: frontend
image: registry.internal/my-org/frontend:sha-abc123
volumeMounts:
- name: config-volume
mountPath: /usr/share/nginx/html/runtime-config.js
subPath: runtime-config.js
volumes:
- name: config-volume
configMap:
name: frontend-runtime-config
Architecture Rationale:
- Synchronous Loading: Using a
<script> tag ensures the configuration is parsed and attached to window before the module system loads the application code. This avoids asynchronous race conditions.
- Global Augmentation: TypeScript declaration merging ensures the configuration is typed without requiring runtime type checks in every component.
- Volume Mounting: Mounting the file allows the container runtime to inject configuration without modifying the image layers. This supports dynamic updates in orchestration platforms.
Pitfall Guide
Runtime injection introduces specific operational risks. Address these during implementation to ensure production stability.
-
Script Execution Order Race Condition
- Explanation: If the application bundle loads before the configuration script,
window.__APP_RUNTIME_CONFIG__ will be undefined, causing initialization failures.
- Fix: Always place the configuration script tag before the main application script in
index.html. Avoid using defer or async on the config script.
-
Aggressive Browser Caching
- Explanation: Browsers may cache
runtime-config.js aggressively. If the configuration changes in the infrastructure but the browser serves a cached version, the application runs with stale settings.
- Fix: Configure the web server to send
Cache-Control: no-cache, no-store, must-revalidate headers specifically for the runtime configuration file. The main bundle can remain cached with long TTLs due to content hashing.
-
Bundler Interference
- Explanation: Build tools like Vite or Webpack may attempt to process or optimize the runtime configuration file if it is included in the source tree, potentially mangling the variable names or structure.
- Fix: Ensure the runtime configuration file is served as a static asset, not processed by the bundler. In Vite, place it in the
public directory. In Webpack, configure CopyWebpackPlugin to include it without transformation.
-
Type Safety Gaps
- Explanation: Accessing
window properties directly can lead to any types, losing the benefits of TypeScript.
- Fix: Use global declaration merging (
declare global { interface Window ... }) as shown in the Core Solution. This enforces type checking at compile time.
-
Security Misconceptions
- Explanation: Some teams attempt to obfuscate configuration using
window.__proto__ or encrypted strings, believing this protects sensitive data.
- Fix: Recognize that all client-side code is public. Runtime configuration should only contain non-sensitive data (API URLs, feature flags, environment names). Secrets must never be injected into the frontend runtime. Use backend proxies for sensitive operations.
-
Missing Configuration Handling
- Explanation: If the configuration file fails to load due to a network error or misconfiguration, the application may crash silently or behave unpredictably.
- Fix: Implement a strict validation step in the loader. Throw a descriptive error if the configuration is missing or malformed. Consider an error boundary in the UI to display a user-friendly message if configuration fails.
-
CORS and Network Latency
- Explanation: If the configuration is fetched via
fetch() instead of a script tag, CORS policies and network latency can delay application startup.
- Fix: Stick to the synchronous script tag approach for configuration. This avoids network requests and CORS issues entirely. The script tag is served from the same origin as the application.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Multi-Environment SaaS | Runtime Injection | Promotes identical artifacts; reduces build matrix complexity. | Lowers CI compute costs; reduces storage overhead. |
| Static Marketing Site | Build-Time Embedding | Configuration rarely changes; build-time allows aggressive caching of all assets. | Minimal cost difference; simpler pipeline for static content. |
| High-Security Compliance | Runtime Injection + Backend Proxy | Ensures immutable artifacts; keeps secrets off the client entirely. | Higher infrastructure complexity; improved auditability. |
| Rapid Prototyping | Build-Time Embedding | Faster initial setup; no infrastructure changes required. | Low initial cost; technical debt accumulates as environments grow. |
| Multi-Tenant Architecture | Runtime Injection | Enables dynamic tenant configuration without rebuilding per tenant. | Enables scalable multi-tenancy; reduces deployment friction. |
Configuration Template
Nginx Configuration Snippet:
server {
listen 80;
server_name frontend.example.com;
root /usr/share/nginx/html;
index index.html;
# Runtime config: No caching to ensure fresh configuration
location = /runtime-config.js {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# Main bundle: Aggressive caching with content hashing
location ~* \.(js|css|png|jpg|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA Fallback
location / {
try_files $uri $uri/ /index.html;
}
}
Docker Compose with Environment Variables:
services:
frontend:
image: registry.internal/my-org/frontend:latest
environment:
- TARGET_ENV=staging
volumes:
- type: bind
source: ./config/${TARGET_ENV}/runtime-config.js
target: /usr/share/nginx/html/runtime-config.js
read_only: true
ports:
- "8080:80"
Quick Start Guide
- Create the Config File: Generate
runtime-config.js with the structure window.__APP_RUNTIME_CONFIG__ = { ... };.
- Update HTML: Add
<script src="/runtime-config.js"></script> to index.html before the main script.
- Mount in Docker: Add a volume mount in your
docker-compose.yml to map the config file into the container.
- Access in Code: Import
useConfig from your loader module and use it in your application initialization.
- Verify: Run the container and check the browser console to ensure
window.__APP_RUNTIME_CONFIG__ is populated before the app loads.