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
new file mode 100644
index 0000000..a015404
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-05-single-domain-routing-design.md
@@ -0,0 +1,119 @@
+# Single-Domain Routing Design
+
+## 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.
+
+## 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.
+
+## Architecture
+
+### Routing (all on `${PUBLIC_HOST}`)
+
+| 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) |
+
+SPA assets at `/_app/*` are served by cameleer-saas through the catch-all route.
+
+### Logto Configuration
+
+- `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/*`
+
+### TLS
+
+- Self-signed cert init container generates wildcard cert on first boot (dev)
+- Traefik ACME (Let's Encrypt) for production
+- HTTP→HTTPS redirect
+
+### Customer Bootstrap
+
+```
+# .env
+PUBLIC_HOST=cameleer.mycompany.com
+PUBLIC_PROTOCOL=https
+
+# DNS: 1 record
+cameleer.mycompany.com → server IP
+
+# 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.