# 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.