# Runtime Configuration Pattern ## Overview MotoVaultPro uses a **K8s-aligned runtime configuration pattern** where sensitive values (like API keys) are loaded at container startup from mounted secrets, not at build time. This approach: - Mirrors Kubernetes deployment patterns - Allows configuration changes without rebuilding images - Keeps secrets out of build artifacts and environment variables - Enables easy secret rotation in production ## Architecture ### How It Works 1. **Build Time**: Container is built WITHOUT secrets (no API keys in image) 2. **Container Startup**: - `/app/load-config.sh` reads `/run/secrets/google-maps-api-key` - Generates `/usr/share/nginx/html/config.js` with runtime values - Starts nginx 3. **App Load Time**: - `index.html` loads `` - `window.CONFIG` is available before React initializes - React app reads configuration via `getConfig()` hook ### File Structure ``` frontend/ ├── scripts/ │ └── load-config.sh # Reads secrets, generates config.js ├── src/ │ └── core/config/ │ └── config.types.ts # TypeScript types and helpers ├── index.html # Loads config.js before app └── Dockerfile # Runs load-config.sh before nginx ``` ## Usage in Components ### Reading Configuration ```typescript import { getConfig, getGoogleMapsApiKey } from '@/core/config/config.types'; export function MyComponent() { // Get full config object with error handling const config = getConfig(); // Or get specific value with fallback const apiKey = getGoogleMapsApiKey(); return ; } ``` ### Conditional Features ```typescript import { isConfigLoaded } from '@/core/config/config.types'; export function FeatureGate() { if (!isConfigLoaded()) { return ; } return ; } ``` ## Adding New Runtime Configuration Values ### 1. Update load-config.sh ```bash # In frontend/scripts/load-config.sh if [ -f "$SECRETS_DIR/new-api-key" ]; then NEW_API_KEY=$(cat "$SECRETS_DIR/new-api-key") echo "[Config] Loaded New API Key" else NEW_API_KEY="" fi # In the config.js generation: cat > "$CONFIG_FILE" < ./secrets/app/new-api-key.txt ``` ## Docker-Compose Configuration The frontend service mounts secrets from the host filesystem: ```yaml mvp-frontend: environment: SECRETS_DIR: /run/secrets volumes: - ./secrets/app/google-maps-api-key.txt:/run/secrets/google-maps-api-key:ro ``` - `:ro` flag makes secrets read-only - Secrets are available at container startup - Changes require container restart (no image rebuild) ## Kubernetes Deployment When deploying to Kubernetes, update the deployment manifest: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: motovaultpro-frontend spec: template: spec: containers: - name: frontend image: motovaultpro-frontend:latest env: - name: SECRETS_DIR value: /run/secrets volumeMounts: - name: google-maps-key mountPath: /run/secrets/google-maps-api-key subPath: google-maps-api-key readOnly: true volumes: - name: google-maps-key secret: secretName: google-maps-api-key items: - key: api-key path: google-maps-api-key ``` ## Development Setup ### Local Development For `npm run dev` (Vite dev server): ```bash # Copy secrets to secrets directory mkdir -p ./secrets/app echo "your-test-api-key" > ./secrets/app/google-maps-api-key.txt # Set environment variable export SECRETS_DIR=./secrets/app # Start Vite dev server npm run dev ``` To access config in your app: ```typescript // In development, config.js may not exist // Use graceful fallback: export function useConfig() { const [config, setConfig] = useState(null); useEffect(() => { if (window.CONFIG) { setConfig(window.CONFIG); } else { // Fallback for dev without config.js setConfig({ googleMapsApiKey: process.env.REACT_APP_GOOGLE_MAPS_KEY || '', }); } }, []); return config; } ``` ### Container-Based Testing Recommended approach (per CLAUDE.md): ```bash # Ensure secrets exist mkdir -p ./secrets/app echo "your-api-key" > ./secrets/app/google-maps-api-key.txt # Rebuild and start containers make rebuild make logs # Verify config.js was generated docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js # Should output: # window.CONFIG = { # googleMapsApiKey: 'your-api-key' # }; ``` ## Debugging ### Verify Secrets are Mounted ```bash docker compose exec mvp-frontend cat /run/secrets/google-maps-api-key ``` ### Check Generated config.js ```bash docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js ``` ### View Container Logs ```bash docker compose logs mvp-frontend ``` Look for lines like: ``` [Config] Loaded Google Maps API key from /run/secrets/google-maps-api-key [Config] Generated /usr/share/nginx/html/config.js ``` ### Browser Console Check browser console for any config loading errors: ```javascript console.log(window.CONFIG); // Should show: { googleMapsApiKey: "your-key" } ``` ## Best Practices 1. **Never Log Secrets**: The load-config.sh script only logs that a key was loaded, never the actual value 2. **Always Validate**: Use `getConfig()` which throws errors if config is missing: ```typescript try { const config = getConfig(); } catch (error) { // Handle missing config gracefully } ``` 3. **Use Fallbacks**: For optional features, use graceful fallbacks: ```typescript const apiKey = getGoogleMapsApiKey(); // Returns empty string if not available ``` 4. **Documentation**: Update this file when adding new configuration values 5. **Testing**: Test with and without secrets in containers ## Security Considerations - Secrets are mounted as files, not environment variables - Files are read-only (`:ro` flag) - config.js is generated at startup, not included in image - Browser console can see config values (like any JavaScript) - For highly sensitive values, consider additional encryption ## Troubleshooting ### config.js not generated **Symptom**: Browser shows `window.CONFIG is undefined` **Solutions**: 1. Check secret file exists: `docker compose exec mvp-frontend cat /run/secrets/google-maps-api-key` 2. Check load-config.sh runs: `docker compose logs mvp-frontend` 3. Verify permissions: `docker compose exec mvp-frontend ls -la /run/secrets/` ### Container fails to start **Symptom**: Container crashes during startup **Solution**: 1. Check logs: `docker compose logs mvp-frontend` 2. Verify script has execute permissions (in Dockerfile) 3. Test script locally: `sh frontend/scripts/load-config.sh` ### Secret changes not reflected **Symptom**: Container still uses old secret after file change **Solution**: ```bash # Restart container to reload secrets docker compose restart mvp-frontend # Or fully rebuild make rebuild ``` ## References - [Kubernetes Secrets Documentation](https://kubernetes.io/docs/concepts/configuration/secret/) - [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/) - [12Factor Config](https://12factor.net/config)