---
id: w1/m21/t002
title: Serving — publishPath from CDN origin + redirects/rewrites + custom headers
worker: worker1
status: done
estimate: 90m
depends_on: [w1/m21/t001]
---

## Objective

Serve the built static output publicly from the object-store/CDN origin at the app's URL, honoring `publishPath`, plus the static-site-only edge rules: `/routes` redirects/rewrites and `/headers` custom response headers.

## Context

- Traefik fronts app traffic (cert-manager TLS, `*.onbex.co`); serving static content means routing the app host to the object-store origin with the edge rules applied at the proxy.
- **Design decision (milestone README, 2026-07-11):** serving goes through **one shared static-server proxy** behind Traefik (host-based routing), never direct bucket exposure — Wasabi has no S3 website hosting, and the edge rules need an in-path layer anyway. The proxy does signed GETs (bucket stays private), maps `/` → `index.html` (+ SPA fallback), applies `/routes`/`/headers` from the App spec, and caches aggressively (keeps egress inside Wasabi's egress ≤ storage fair-use policy). Per app there is still no Deployment/Service; the proxy is a single platform component.
- `.pm/DO_NOT_DO.md` line 23 parks these edge rules as non-goals **until `w1/012` is promoted** — which it now is, so they are in-scope here.
- Render semantics: redirects (301/302) + rewrites (200 path swap), ordered; custom response headers scoped by path.

## Steps

1. Stand up the shared static-server proxy (single platform component): resolves the request host → App → current revision prefix, signed GETs against the bucket, `/` → `index.html` + SPA fallback, response caching.
2. Route static-site app host(s) to the proxy via Traefik (host-based routing; cert-manager TLS as for compute apps).
3. Implement `/routes` redirects + rewrites (ordered, first-match) in the proxy from the App spec.
4. Implement `/headers` custom response headers scoped by path pattern.

## Files

- `lego/operator/internal/controller/…` (serving/ingress wiring)
- shared static-server component (new; binary vs. configured off-the-shelf proxy decided in-task)
- Traefik routing config as needed

## Acceptance criteria

- [ ] `curl` the app URL returns the built `index.html`; a configured redirect returns 301/302 to the target; a rewrite serves the rewritten path with 200; a configured header appears in the response.
- [ ] SPA fallback (rewrite `/*` → `/index.html`) works.

## Out of scope

- API/UI exposure of these rules (t003/t004).
