Reflects current state: path-based routing, SaaS at /platform, Logto catch-all, TLS init container, server integration env vars, custom JwtDecoder for ES384, skip consent for SSO. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3.8 KiB
3.8 KiB
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/* |
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 |
Configuration
Two env vars control everything:
PUBLIC_HOST=cameleer.mycompany.com
PUBLIC_PROTOCOL=https
TLS
- Dev:
traefik-certsinit 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: truefor 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+jwttoken type, split issuer-uri / jwk-set-uri - Redirect URIs:
${PROTO}://${HOST}/platform/callback
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+jwttoken type acceptanceX-Forwarded-Prefixsupport 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 + <base> tag |
CAMELEER_API_URL |
http://cameleer3-server:8081 |
nginx API proxy target |
Traefik strip-prefix removes /server before forwarding to nginx. Server-ui injects <base href="/server/"> via BASE_PATH.
Bootstrap Redirect URIs
# 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"]
Customer Bootstrap
# 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.