Files
motovaultpro/frontend/docs/RUNTIME-CONFIG.md
2025-11-04 18:46:46 -06:00

7.9 KiB

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 <script src="/config.js"></script>
    • 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

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 <MapComponent apiKey={apiKey} />;
}

Conditional Features

import { isConfigLoaded } from '@/core/config/config.types';

export function FeatureGate() {
  if (!isConfigLoaded()) {
    return <LoadingFallback />;
  }

  return <AdvancedFeature />;
}

Adding New Runtime Configuration Values

1. Update load-config.sh

# 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" <<EOF
window.CONFIG = {
  googleMapsApiKey: '$GOOGLE_MAPS_API_KEY',
  newApiKey: '$NEW_API_KEY'
};
EOF

2. Update config.types.ts

export interface AppConfig {
  googleMapsApiKey: string;
  newApiKey: string;  // Add new field
}

export function getNewApiKey(): string {
  try {
    const config = getConfig();
    return config.newApiKey || '';
  } catch {
    console.warn('New API Key not available.');
    return '';
  }
}

3. Update docker-compose.yml

mvp-frontend:
  volumes:
    - ./secrets/app/google-maps-api-key.txt:/run/secrets/google-maps-api-key:ro
    - ./secrets/app/new-api-key.txt:/run/secrets/new-api-key:ro  # Add new secret

4. Create Secret File

mkdir -p ./secrets/app
echo "your-api-key-value" > ./secrets/app/new-api-key.txt

Docker-Compose Configuration

The frontend service mounts secrets from the host filesystem:

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:

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):

# 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:

// In development, config.js may not exist
// Use graceful fallback:
export function useConfig() {
  const [config, setConfig] = useState<AppConfig | null>(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):

# 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

docker compose exec mvp-frontend cat /run/secrets/google-maps-api-key

Check Generated config.js

docker compose exec mvp-frontend cat /usr/share/nginx/html/config.js

View Container Logs

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:

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:

    try {
      const config = getConfig();
    } catch (error) {
      // Handle missing config gracefully
    }
    
  3. Use Fallbacks: For optional features, use graceful fallbacks:

    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:

# Restart container to reload secrets
docker compose restart mvp-frontend

# Or fully rebuild
make rebuild

References