# Single-Domain Routing Design
## Problem
Customers cannot always provision subdomains. The platform must work with a single hostname and one DNS record.
## Solution
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/`.
## Routing (all on `${PUBLIC_HOST}`)
| Path | Target | Priority | Notes |
|------|--------|----------|-------|
| `/platform/*` | cameleer-saas:8080 | default | Spring context-path `/platform` |
| `/server/*` | cameleer-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 |
## Configuration
Two env vars control everything:
```env
PUBLIC_HOST=cameleer.mycompany.com
PUBLIC_PROTOCOL=https
```
## 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 — 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
## SaaS App (cameleer-saas)
- `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`
## Server Integration (cameleer-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 (cameleer-server-ui)
| Env var | Value | Purpose |
|---------|-------|---------|
| `BASE_PATH` | `/server` | React Router basename + `` tag |
| `CAMELEER_API_URL` | `http://cameleer-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 (cameleer-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"]
```
## Customer Bootstrap
```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
```
## Future: Custom Sign-In UI (Roadmap)
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.