Skip to content

Part 4: SPA Runtime Config

Replace build-time environment variables with runtime configuration so a single Docker image works across all environments without rebuilding.

What you'll learn

  • The difference between build-time and runtime configuration for SPAs
  • Enabling type: spa and spa_root in appmecca.yaml
  • Reading window.__APPMECCA_CONFIG__ in SvelteKit
  • Managing environment variables with mecca var commands
  • The PUBLIC_* naming convention for frontend-visible variables

What you'll need

  • The full-stack app from Part 3 deployed and working

The problem with build-time config

In Part 3 you passed VITE_API_URL as a Docker build arg. This means:

  1. The URL is baked into the JavaScript bundle at build time
  2. Promoting the same image from dev to staging requires a rebuild
  3. Every environment-specific change triggers a new build

AppMecca solves this with SPA runtime config injection.

How it works

When you set type: spa on a service, AppMecca generates a file called __appmecca_config.js inside the container's spa_root directory before starting the process. This file sets window.__APPMECCA_CONFIG__ with all PUBLIC_* environment variables.

The generated file looks like this:

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

Update appmecca.yaml

Change the frontend service to use 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: 3000
    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"

Removed build arg

The args block on the frontend container is gone. The API URL is now injected at deploy time, not build time.

Add the config script to your HTML

Edit frontend/src/app.html to include the config script before SvelteKit's own scripts:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <script src="/__appmecca_config.js"></script>
    %sveltekit.head%
  </head>
  <body data-sveltekit-prerender-data>
    <div style="display: contents">%sveltekit.body%</div>
  </body>
</html>

Create a config helper

Create frontend/src/lib/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
 * @returns {string}
 */
export function getConfig(key, fallback = "") {
  if (typeof window !== "undefined" && window.__APPMECCA_CONFIG__) {
    return window.__APPMECCA_CONFIG__[key] ?? fallback;
  }
  return import.meta.env[`VITE_${key}`] ?? fallback;
}

Local development

The helper falls back to import.meta.env so you can still use a .env file with VITE_PUBLIC_API_URL=http://localhost:3000 during local development.

Update the Svelte page

Replace the hardcoded API URL in +page.svelte:

<script>
  import { onMount } from "svelte";
  import { getConfig } from "$lib/config.js";

  const API_URL = getConfig("PUBLIC_API_URL");
  let todos = [];
  let newTitle = "";

  onMount(async () => {
    const res = await fetch(`${API_URL}/api/todos`);
    todos = await res.json();
  });

  // ... rest of the component unchanged
</script>

Manage environment variables

Set, list, and remove environment variables from the CLI. The app name is the first argument:

# Set one or more variables
mecca var set todo-app PUBLIC_APP_TITLE="My Todos"
mecca var set todo-app PUBLIC_APP_TITLE="My Todos" PUBLIC_FEATURE_FLAG=on

# List all variables, grouped by branch
mecca var list todo-app
mecca var list todo-app -b main --dotenv > .env   # .env lines; needs one branch

# Remove a variable
mecca var unset todo-app PUBLIC_APP_TITLE

var set writes to every branch by default

With no -b, the value lands on 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

Redeploy required

Changing environment variables does not take effect until you redeploy. The config file is generated at container start time. A --no-build deploy is enough:

mecca app deploy todo-app --no-build

Rebuild and deploy

Since you changed the Dockerfile and removed the build arg, rebuild:

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

After deployment, verify the config is injected by visiting https://todo-app-frontend-yourgroup.dev.example.com/__appmecca_config.js in your browser.

What's next

In Part 5: Background Workers, you will add a BullMQ worker for background job processing and learn about portless services.