Files
cameleer-saas/docs/superpowers/specs/2026-04-05-single-domain-routing-design.md
hsiegeln 63c194dab7
Some checks failed
CI / build (push) Failing after 18s
CI / docker (push) Has been skipped
chore: rename cameleer3 to cameleer
Rename Java packages from net.siegeln.cameleer3 to net.siegeln.cameleer,
update all references in workflows, Docker configs, docs, and bootstrap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:28:44 +02:00

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/* 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:

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 + <base> tag
CAMELEER_API_URL http://cameleer-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 (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

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