Skip to content

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: spa and spa_root in appmecca.yaml
  • Reading window.__APPMECCA_CONFIG__ in React
  • Managing environment variables with the CLI
  • The PUBLIC_* naming convention

What you'll need


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:

  1. Environment coupling -- the same image cannot be promoted from dev to staging to production
  2. 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:

window.__APPMECCA_CONFIG__ = {
  "PUBLIC_API_URL": "https://todo-app-backend-yourgroup.dev.yourdomain.com",
  "PUBLIC_APP_TITLE": "Todo App"
};

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:

VITE_PUBLIC_API_URL=http://localhost:8000
VITE_PUBLIC_APP_TITLE=Todo App (Local)

Step 5: Deploy and verify

mecca build trigger -e dev -g git@github.com:yourorg/todo-app.git
mecca app deploy todo-app -e dev

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:

mecca var set todo-app PUBLIC_APP_TITLE="Staging Todos" -b staging -e dev

Redeploy to pick up the change (no rebuild needed):

mecca app deploy todo-app --no-build -e dev

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.