Skip to content

Step 15 — Splitting the front end off

About 35 minutes. Branch: step-15.

The one new idea: one app, one URL, services mounted on paths.

This step needs path-based routing enabled on your environment

If mecca app redeploy refuses with "path-based routing (one URL per app) is not available yet", yours does not have it on. Ask whoever runs it, or skip to step 16step-14 is a complete, working application and 16 and 17 follow from it.

The problem worth fixing

Since step 6 the React bundle has been inside the Python image. So a one-word copy change rebuilds Python, reinstalls pip packages and replaces a container — and the bundle is served by your container rather than from the edge.

Splitting them fixes both. What it must not do is give Tally two URLs and a cross-origin problem.

Three services, one hostname

deploy:
  - name: web
    primary: true
    type: static
    visibility: public
    static_site:
      build_command: "cd frontend && npm ci --no-audit --no-fund && npm run build"
      output_dir: frontend/dist
      spa_fallback: true
      index_document: index.html
      error_document: index.html
    environment:
      VITE_API_BASE: "${service:api:path}"

  - name: api
    container: tally-api
    path: /api
    visibility: public
    port: 8000

  - name: worker
    container: tally-api
    command: "python -m app.worker"

Read what each one declares, and what it does not:

  • web has no path:. That is what makes it the front door — it answers for everything the API has not claimed. The front door is the one service that cannot declare a path.
  • api is mounted at /api. It claims that prefix on the same hostname.
  • worker has neither a port nor a path, so it is not reachable from outside at all. Correct for a worker.

The trap: the mount is not stripped

⚠ A request to /api/items reaches your container as /api/items. The mount selects which service gets the request; it does not remove the prefix.

So Tally's routes are written /api/... to match, and they always have been. Get this backwards — mount at /api and serve /items — and every route 404s while the config looks perfect.

${service:api:path}, and its three siblings

      VITE_API_BASE: "${service:api:path}"

Resolved in the build, before vite build runs, so it is compiled into the bundle. It evaluates to /api.

Form Gives you Use it when
${service:api} https://tally-…appmecca.net you need an absolute URL
${service:api:url} the same, spelled out you want the intent obvious
${service:api:path} /api same app, same hostname — this case
${service:api:internal} a private address container to container, never through the internet

:path is the right one here, and the reason is the point of the whole step: a frontend served from the same hostname should never have to be told what that hostname is. No CORS, no preflight, no environment-specific URL to get wrong.

Which is also why step 15 could not have come earlier

Two separate apps would mean two origins, and the frontend's first cross-origin request would fail its preflight — which cannot follow a redirect and cannot carry credentials. The error would say No 'Access-Control-Allow-Origin' header while your CORS config was perfectly correct.

One URL removes the problem instead of configuring around it.

One constraint to know

A static service can be the front door, or it can be its own app. It cannot be mounted on a non-empty path — the bundle is uploaded to the root of its prefix and the mount is not stripped, so every request would 403.

Nor can a private app take this shape: a private app is served through the auth gateway, which has no route to storage. Tally is public, so this is fine.

Deploy and check both halves

mecca app redeploy tally -b tutorial

Then confirm they are one app on one hostname: the board loads at /, and /api/items returns JSON on the same host.

If it didn't work

Symptom Try Usually
Refused: path routing unavailable Not enabled here. Stay on step-14
Every API call 404s Routes not written under the mount
Board loads, API calls fail check VITE_API_BASE in the bundle The build did not resolve it
Refused: static service on a path A static service may only be the front door

Reset to the reference

git fetch upstream && git reset --hard upstream/step-15 && git push --force origin tutorial

Next: Step 16 — Speed and reach