fabro/docker/split-web
Scott Werner 2bd04c7935
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
Demote control-plane config to plain String; native FABRO_WEB_URL read (#510)
Third reducing PR of the interpolation unification (**D11 resolution
(c)**): the control plane never interpolates. `InterpString` is now
strictly the user-facing workflow config language; server identity,
storage, listen, object-store, and GitHub App identifiers are plain
`String`, consumed where needed with no resolution point.

## Demoted to `String` (was `InterpString`)

Both the layer and resolved types:

- `server.listen.unix.path`, `server.api.url`, `server.web.url`
- `server.storage.root`, `server.artifacts.prefix`,
`server.slatedb.prefix`
- object store: `Local.root`, S3 `bucket` / `region` / `endpoint`
(shared by artifacts + slatedb)
- `github.app_id` / `client_id` / `slug`

**Kept `InterpString`:** `slack.default_channel` (run-time consumption —
the one server-defined survivor). `server.listen.tcp.address` stays the
`SocketAddr` `parsed_value` special case.

## Native `FABRO_WEB_URL` read

Deployment-time late binding now goes through a native env read instead
of a `{{ env.* }}` token: `FABRO_WEB_URL` overrides `server.web.url`
(**env override > settings literal > default**), applied in
`canonical_origin` and reused by the JWT issuer, cookie-secure check,
and system-info. `docker/split-web` no longer ferries the value through
a settings token (compose still sets the env var). `canonical_origin`'s
error message now advertises a knob that is actually true for everyone.

## Behavior change (release notes)

- `{{ env.* }}` / `{{ vars.* }}` tokens in the demoted server fields are
now **literal text**, not interpolated. The resolve layer emits
`warn_if_demoted_template` for every demoted field, so operators with
tokens still in server config **fail loud** rather than silently
treating the token as a literal.
- Operators who relied on env-based storage location should use the
existing native `FABRO_STORAGE_DIR` (`--storage-dir`) override.
`FABRO_STORAGE_ROOT` promotion is intentionally deferred (not a proven
need).

## Cleanup

`fabro-server`'s `crate::interp` shrinks to just the process-env lookup
facade; `resolve_interp` / `_path` / `_with` and the
`AppState::resolve_interp` seam are deleted (nothing resolves
server-scope `InterpString` anymore).

## Verification

- `cargo build --workspace` 
- `cargo +nightly clippy --workspace --all-targets -- -D warnings` 
(incl. the `as_source` gate)
- `cargo +nightly fmt --check --all` 
- `cargo nextest run --workspace`: 6305 passed; added two tests covering
the `FABRO_WEB_URL` override precedence (env-wins and settings-literal
fallback).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 10:03:14 -04:00
..
Caddyfile.edge Add split web Docker Compose PoC (#445) 2026-05-28 00:03:00 -04:00
Caddyfile.static fix(server): allow required CSP handoffs 2026-06-01 18:24:45 -04:00
README.md Add split web Docker Compose PoC (#445) 2026-05-28 00:03:00 -04:00
settings.toml Demote control-plane config to plain String; native FABRO_WEB_URL read (#510) 2026-06-18 10:03:14 -04:00

Split Web Compose PoC

This Compose stack proves that Fabro can serve the React SPA from a separate static process while the Rust server remains the API and browser-auth origin.

Request ownership:

  • /api/* -> fabro-api:32276
  • /auth/* -> fabro-api:32276
  • /health -> fabro-api:32276
  • everything else -> fabro-web:80

The Rust server still contains bundled SPA assets. In this PoC they are simply not reachable through the edge service for normal web paths.

The edge proxy adds X-Fabro-PoC-Upstream to responses so manual checks can confirm which service handled a request.

Run

Build the local Fabro image from the current tree:

cargo dev docker-build --tag fabro-sh/fabro:split-web-poc

Set local auth secrets:

export SESSION_SECRET="$(openssl rand -hex 32)"
export FABRO_DEV_TOKEN="fabro_dev_$(openssl rand -hex 32)"

Start the split stack:

docker compose -f docker-compose.split-web.yaml up

Open http://localhost:8080.

Use SPLIT_WEB_PORT to expose a different local port, or FABRO_IMAGE to use a different API image.

Validate

curl -i http://localhost:8080/health
curl -i http://localhost:8080/api/v1/health
curl -I http://localhost:8080/runs
curl -I http://localhost:8080/assets/app.css

curl -c /tmp/fabro.cookies \
  -H "content-type: application/json" \
  -d "{\"token\":\"$FABRO_DEV_TOKEN\"}" \
  http://localhost:8080/auth/login/dev-token

curl -b /tmp/fabro.cookies http://localhost:8080/api/v1/auth/me

Expected results:

  • /runs and /assets/* are served by the static fabro-web container.
  • /api/*, /auth/*, and /health are served by the Rust fabro-api container through the same browser origin.
  • Dev-token login sets a same-origin session cookie, and /api/v1/auth/me accepts it.