diff --git a/CLAUDE.md b/CLAUDE.md
index acc0234..a67d956 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -26,10 +26,38 @@ The existing cameleer3-server already has single-tenant auth (JWT, RBAC, bootstr
- Proxy or federate access to tenant-specific cameleer3-server instances
- Enforce usage quotas and metered billing
-Auth enforcement (current state):
+### Routing (single-domain, path-based via Traefik)
+
+All services on one hostname. Two env vars control everything: `PUBLIC_HOST` + `PUBLIC_PROTOCOL`.
+
+| Path | Target | Notes |
+|------|--------|-------|
+| `/platform/*` | cameleer-saas:8080 | SPA + API (`server.servlet.context-path: /platform`) |
+| `/server/*` | cameleer3-server-ui:80 | Server dashboard (strip-prefix + `BASE_PATH=/server`) |
+| `/` | redirect → `/platform/` | Via `docker/traefik-dynamic.yml` |
+| `/*` (catch-all) | logto:3001 (priority=1) | Sign-in, OIDC, interaction, assets |
+
+- SPA assets at `/_app/` (Vite `assetsDir: '_app'`) to avoid conflict with Logto's `/assets/`
+- Logto `ENDPOINT` = `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` (same domain, same origin)
+- TLS: self-signed cert init container (`traefik-certs`) for dev, ACME for production
+- Root `/` → `/platform/` redirect via Traefik file provider (`docker/traefik-dynamic.yml`)
+
+### Auth enforcement
+
- All API endpoints enforce OAuth2 scopes via `@PreAuthorize("hasAuthority('SCOPE_xxx')")` annotations
- Tenant isolation enforced by `TenantIsolationInterceptor` (a single `HandlerInterceptor` on `/api/**` that resolves JWT org_id to TenantContext and validates `{tenantId}`, `{environmentId}`, `{appId}` path variables; fail-closed, platform admins bypass)
-- 10 OAuth2 scopes defined on the Logto API resource (`https://api.cameleer.local`), served to the frontend from `GET /api/config`
+- 10 OAuth2 scopes defined on the Logto API resource (`https://api.cameleer.local`), served to the frontend from `GET /platform/api/config`
+- Custom `JwtDecoder` in `SecurityConfig.java` — ES384 algorithm, `at+jwt` token type, split issuer-uri (string validation) / jwk-set-uri (Docker-internal fetch)
+
+### Server integration (cameleer3-server env vars)
+
+| Env var | Value | Purpose |
+|---------|-------|---------|
+| `CAMELEER_OIDC_ISSUER_URI` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/oidc` | Token issuer claim validation |
+| `CAMELEER_OIDC_JWK_SET_URI` | `http://logto:3001/oidc/jwks` | Docker-internal JWK fetch |
+| `CAMELEER_OIDC_TLS_SKIP_VERIFY` | `true` | Skip cert verify for OIDC discovery (dev) |
+| `CAMELEER_CORS_ALLOWED_ORIGINS` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` | Allow browser requests through Traefik |
+| `BASE_PATH` (server-ui) | `/server` | React Router basename + `` tag |
## Related Conventions
diff --git a/docs/superpowers/specs/2026-04-05-single-domain-routing-design.md b/docs/superpowers/specs/2026-04-05-single-domain-routing-design.md
index a015404..d5cde7c 100644
--- a/docs/superpowers/specs/2026-04-05-single-domain-routing-design.md
+++ b/docs/superpowers/specs/2026-04-05-single-domain-routing-design.md
@@ -2,118 +2,103 @@
## Problem
-Customers cannot always provision subdomains (`auth.domain.com`, `server.domain.com`). The current architecture requires 3 DNS records. Some enterprise environments only allow a single hostname pointing at the server.
-
-## Root Cause
-
-Logto's sign-in UI serves assets at `/assets/*`. The SPA (Vite-built) also outputs assets to `/assets/*`. With path-based Traefik routing on a single domain, both conflict on the same prefix.
+Customers cannot always provision subdomains. The platform must work with a single hostname and one DNS record.
## Solution
-Move the SPA's asset output directory from `/assets/` to `/_app/`. Then Traefik can route `/assets/*` to Logto while the SPA's assets are served from `/_app/*` via the catch-all. Single domain, single DNS record, no subdomains.
+Path-based routing on one domain. SaaS app at `/platform`, server-ui at `/server/`, Logto as catch-all. SPA assets moved to `/_app/` to avoid conflict with Logto's `/assets/`.
-## Architecture
+## Routing (all on `${PUBLIC_HOST}`)
-### Routing (all on `${PUBLIC_HOST}`)
+| Path | Target | Priority | Notes |
+|------|--------|----------|-------|
+| `/platform/*` | cameleer-saas:8080 | default | Spring context-path `/platform` |
+| `/server/*` | cameleer3-server-ui:80 | default | Strip-prefix + `BASE_PATH=/server` |
+| `/` | redirect → `/platform/` | 100 | Via `docker/traefik-dynamic.yml` |
+| `/*` | logto:3001 | 1 (lowest) | Catch-all: sign-in, OIDC, assets |
-| Path | Target | Priority |
-|------|--------|----------|
-| `/oidc/*` | logto:3001 | default |
-| `/interaction/*` | logto:3001 | default |
-| `/assets/*` | logto:3001 | default |
-| `/server/*` | cameleer3-server-ui:80 (prefix stripped) | default |
-| `/api/*` | cameleer-saas:8080 | default |
-| `/*` (catch-all) | cameleer-saas:8080 | 1 (lowest) |
+## Configuration
-SPA assets at `/_app/*` are served by cameleer-saas through the catch-all route.
+Two env vars control everything:
+```env
+PUBLIC_HOST=cameleer.mycompany.com
+PUBLIC_PROTOCOL=https
+```
-### Logto Configuration
+## TLS
+
+- **Dev**: `traefik-certs` init container auto-generates self-signed cert on first boot
+- **Production**: Traefik ACME (Let's Encrypt)
+- HTTP→HTTPS redirect via Traefik entrypoint config
+
+## Logto
- `ENDPOINT` = `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` (same domain as SPA)
- OIDC issuer = `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/oidc`
-- Same origin for browser — no CORS, cookies work
-- Logto Management API (`/api/*` on port 3001) is only accessed via Docker-internal networking, never through Traefik — no conflict with cameleer-saas `/api/*`
+- Same origin — no CORS, cookies work
+- Management API only via Docker-internal networking (`http://logto:3001`)
+- Bootstrap (`docker/logto-bootstrap.sh`) creates apps, users, orgs, roles, scopes
+- Traditional app has `skipConsent: true` for first-party SSO
-### TLS
+## SaaS App (cameleer-saas)
-- Self-signed cert init container generates wildcard cert on first boot (dev)
-- Traefik ACME (Let's Encrypt) for production
-- HTTP→HTTPS redirect
+- `server.servlet.context-path: /platform` — Spring handles prefix transparently
+- Vite `base: '/platform/'`, `assetsDir: '_app'`
+- BrowserRouter `basename="/platform"`
+- API client: `API_BASE = '/platform/api'`
+- Custom `JwtDecoder`: ES384 algorithm, `at+jwt` token type, split issuer-uri / jwk-set-uri
+- Redirect URIs: `${PROTO}://${HOST}/platform/callback`
-### Customer Bootstrap
+## Server Integration (cameleer3-server)
+| Env var | Value | Purpose |
+|---------|-------|---------|
+| `CAMELEER_OIDC_ISSUER_URI` | `${PROTO}://${HOST}/oidc` | Token issuer claim validation |
+| `CAMELEER_OIDC_JWK_SET_URI` | `http://logto:3001/oidc/jwks` | Docker-internal JWK fetch |
+| `CAMELEER_OIDC_TLS_SKIP_VERIFY` | `true` | Skip cert verify for OIDC discovery (dev only) |
+| `CAMELEER_CORS_ALLOWED_ORIGINS` | `${PROTO}://${HOST}` | Browser requests through Traefik |
+
+Server OIDC requirements:
+- ES384 signing algorithm (Logto default)
+- `at+jwt` token type acceptance
+- `X-Forwarded-Prefix` support for correct redirect_uri construction
+- Branding endpoint (`/api/v1/branding/logo`) must be publicly accessible
+
+## Server UI (cameleer3-server-ui)
+
+| Env var | Value | Purpose |
+|---------|-------|---------|
+| `BASE_PATH` | `/server` | React Router basename + `` tag |
+| `CAMELEER_API_URL` | `http://cameleer3-server:8081` | nginx API proxy target |
+
+Traefik strip-prefix removes `/server` before forwarding to nginx. Server-ui injects `` via `BASE_PATH`.
+
+## Bootstrap Redirect URIs
+
+```sh
+# SPA (cameleer-saas)
+SPA_REDIRECT_URIS=["${PROTO}://${HOST}/platform/callback"]
+SPA_POST_LOGOUT_URIS=["${PROTO}://${HOST}/platform/login"]
+
+# Traditional (cameleer3-server) — both variants until X-Forwarded-Prefix is consistent
+TRAD_REDIRECT_URIS=["${PROTO}://${HOST}/oidc/callback","${PROTO}://${HOST}/server/oidc/callback"]
+TRAD_POST_LOGOUT_URIS=["${PROTO}://${HOST}","${PROTO}://${HOST}/server"]
```
-# .env
-PUBLIC_HOST=cameleer.mycompany.com
-PUBLIC_PROTOCOL=https
-# DNS: 1 record
-cameleer.mycompany.com → server IP
+## Customer Bootstrap
-# Start
+```bash
+# 1. Set domain
+echo "PUBLIC_HOST=cameleer.mycompany.com" >> .env
+echo "PUBLIC_PROTOCOL=https" >> .env
+
+# 2. Point DNS (1 record)
+# cameleer.mycompany.com → server IP
+
+# 3. Start
docker compose up -d
```
-## Changes by Repository
-
-### cameleer-saas (this repo)
-
-1. **`ui/vite.config.ts`** — Set `build.assetsDir: '_app'`
-2. **`docker-compose.yml`** — Revert to path-based routing:
- - Logto: `PathPrefix(/oidc) || PathPrefix(/interaction) || PathPrefix(/assets)`
- - Server-UI: `PathPrefix(/server)` with strip prefix middleware
- - SPA: `PathPrefix(/)` priority 1 (catch-all) — same as original
- - Remove Host-based routing labels
- - Logto `ENDPOINT` = `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` (no `auth.` prefix)
- - All `LOGTO_PUBLIC_ENDPOINT`, `LOGTO_ISSUER_URI` use `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` (no `auth.`)
-3. **`docker/logto-bootstrap.sh`** — Host header = `${HOST}` (matches ENDPOINT, no `auth.` prefix). Redirect URIs use `${PROTO}://${HOST}/callback`.
-4. **`ui/src/config.ts`** — Fallback `logtoEndpoint` = `${window.location.origin}` (same origin)
-5. **`ui/src/components/Layout.tsx`** — Dashboard link = `/server/`
-
-### cameleer3-server-ui (server team)
-
-Add configurable `BASE_PATH` env var so the SPA can be served from a subpath.
-
-**Build changes:**
-- Accept `BASE_PATH` env var (default: `/`)
-- At build time or container start, set Vite's `base` to `BASE_PATH` value
-- If using runtime config: nginx rewrites `` to `` at container startup
-
-**nginx changes:**
-- Serve SPA at the configured base path
-- API proxy remains internal (`CAMELEER_API_URL` → `http://cameleer3-server:8081`)
-
-**In cameleer-saas docker-compose.yml:**
-```yaml
-cameleer3-server-ui:
- environment:
- CAMELEER_API_URL: http://cameleer3-server:8081
- BASE_PATH: /server/
-```
-
-### cameleer3-server (server team)
-
-Add split JWK/issuer OIDC configuration so token validation works in Docker environments where the public issuer URL is not directly reachable from containers.
-
-**What to add:**
-- New config property: `cameleer.oidc.jwk-set-uri` (or equivalent)
-- When set, fetch JWKs from this URL instead of deriving from the OIDC discovery document
-- Validate token `iss` claim against `cameleer.oidc.issuer-uri` (string comparison)
-- Accept `at+jwt` token type header (Logto uses this instead of plain `jwt`)
-- Algorithm: ES384 (Logto's signing algorithm)
-
-**Reference implementation:** `cameleer-saas/src/main/java/.../config/SecurityConfig.java` — custom `JwtDecoder` bean that does exactly this.
-
-**In cameleer-saas docker-compose.yml:**
-```yaml
-cameleer3-server:
- environment:
- CAMELEER_OIDC_ISSUER_URI: ${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/oidc
- CAMELEER_OIDC_JWK_SET_URI: http://logto:3001/oidc/jwks
-```
-
## Future: Custom Sign-In UI (Roadmap)
-For full auth UX control (branding, custom flows), build a custom sign-in experience using Logto's Experience API. This eliminates Logto's interaction pages entirely — Logto becomes a pure OIDC/API backend. The SPA renders all auth screens (login, register, forgot password, MFA, social login).
-
-This is the right long-term play for a SaaS product but is a separate project.
+For full auth UX control, build a custom sign-in experience using Logto's Experience API. Eliminates Logto's interaction pages — Logto becomes a pure OIDC/API backend. Separate project.