Part 4: SPA Runtime Config¶
Inject environment variables into your React app at deploy time instead of build time.
What you'll learn¶
- Why build-time environment variables are insufficient for SPAs
- Configuring
type: spaandspa_rootinappmecca.yaml - Reading
window.__APPMECCA_CONFIG__in React - Managing environment variables with the CLI
- The
PUBLIC_*naming convention
What you'll need¶
- A multi-container app from Part 3: Going Full-Stack
The problem with build-time config¶
In Part 3, the API URL was baked into the frontend image at build time via VITE_API_URL. This creates two problems:
- Environment coupling -- the same image cannot be promoted from dev to staging to production
- Rebuild overhead -- changing a URL requires a full container rebuild
AppMecca solves this with SPA runtime configuration. At deploy time, it generates a __appmecca_config.js file inside the container and injects it into your HTML.
Step 1: Update appmecca.yaml¶
Replace the frontend deploy block with type: spa:
database:
enabled: true
containers:
- name: backend
dockerfile: backend/Dockerfile
context: backend/
- name: frontend
dockerfile: frontend/Dockerfile
context: frontend/
deploy:
- name: backend
container: backend
port: 8000
path: /api
health_check:
path: /health
- name: frontend
container: frontend
type: spa
command: 'nginx -g "daemon off;"' # serves the built bundle from a container
spa_root: /usr/share/nginx/html
port: 3000
health_check:
path: /
environment:
PUBLIC_API_URL: "${service:backend}"
PUBLIC_APP_TITLE: "Todo App"
The PUBLIC_* convention
Only environment variables prefixed with PUBLIC_ are injected into the SPA config file. This prevents accidentally leaking secrets to the browser. Backend-only variables (database credentials, API keys) are never exposed.
Step 2: Remove build-time args¶
Remove the args section from the frontend container since the URL is now injected at runtime:
containers:
- name: frontend
dockerfile: frontend/Dockerfile
context: frontend/
# No args needed -- config is injected at deploy time
Update frontend/Dockerfile to remove the ARG and ENV lines:
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 3000
CMD ["nginx", "-g", "daemon off;"]
Step 3: Load the config in HTML¶
Add the script tag to frontend/index.html before your app bundle:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo App</title>
<script src="/__appmecca_config.js"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
How it works
At container startup, AppMecca writes a file at {spa_root}/__appmecca_config.js that sets window.__APPMECCA_CONFIG__ with all PUBLIC_* variables. The generated file looks like:
Step 4: Read config in React¶
Create frontend/src/config.js:
/**
* Read a runtime config value injected by AppMecca.
* Falls back to import.meta.env for local development.
*
* @param {string} key - Config key (e.g. "PUBLIC_API_URL")
* @param {string} [fallback] - Default value if not found
* @returns {string}
*/
export function getConfig(key, fallback = "") {
if (window.__APPMECCA_CONFIG__ && window.__APPMECCA_CONFIG__[key]) {
return window.__APPMECCA_CONFIG__[key];
}
return import.meta.env[`VITE_${key}`] || fallback;
}
Update frontend/src/App.jsx to use it:
import { useState, useEffect } from "react";
import { getConfig } from "./config";
const API_URL = getConfig("PUBLIC_API_URL", "http://localhost:8000");
const APP_TITLE = getConfig("PUBLIC_APP_TITLE", "Todos");
export default function App() {
const [todos, setTodos] = useState([]);
const [title, setTitle] = useState("");
useEffect(() => {
fetch(`${API_URL}/api/todos/`)
.then((r) => r.json())
.then(setTodos);
}, []);
// ... rest of the component unchanged
return (
<div style={{ maxWidth: 600, margin: "2rem auto", fontFamily: "sans-serif" }}>
<h1>{APP_TITLE}</h1>
{/* ... form and list unchanged */}
</div>
);
}
For local development, create frontend/.env:
Step 5: Deploy and verify¶
Open the frontend URL and check the browser console:
console.log(window.__APPMECCA_CONFIG__);
// { PUBLIC_API_URL: "https://...", PUBLIC_APP_TITLE: "Todo App" }
Step 6: Manage env vars without rebuilding¶
Change the app title without triggering a rebuild. Several KEY=value pairs can go in one command:
mecca var set todo-app PUBLIC_APP_TITLE="My Todo List" -e dev
mecca var set todo-app PUBLIC_APP_TITLE="My Todo List" PUBLIC_FEATURE_FLAG=on -e dev
var set writes to every branch by default
With no -b, the value is written to all of the app's branches. Narrow it explicitly when that is not what you want:
Redeploy to pick up the change (no rebuild needed):
List and remove environment variables:
mecca var list todo-app -e dev
mecca var list todo-app -b main --dotenv -e dev > .env # one branch only
mecca var unset todo-app PUBLIC_APP_TITLE -e dev
Redeploy required
Environment variable changes take effect on the next deploy, not immediately. The SPA config file is generated at container startup.
What's next¶
Your app handles all requests synchronously. In Part 5: Background Workers, you will add a Celery worker for asynchronous task processing.