docs: single-domain routing design spec
Path-based routing on one hostname. SPA assets move to /_app/, Logto gets /assets/ + /oidc/ + /interaction/. Server-ui at /server/. Includes requirements for server team (split JWK/issuer, BASE_PATH). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<base href="/">` to `<base href="/server/">` 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.
|
||||||
Reference in New Issue
Block a user