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>
5.4 KiB
Configurable Base Path for SaaS App
Problem
Logto uses many root-level paths (/sign-in, /register, /consent, /social, /api/interaction, /api/experience, /assets, etc.) that conflict with the SaaS app's catch-all routing. Enumerating Logto's paths in Traefik is fragile and keeps growing.
Solution
Move the SaaS app to a configurable base path (default: /platform). Logto becomes the Traefik catch-all. Zero path enumeration — any path Logto adds in the future just works.
Routing
| Path | Target | Priority |
|---|---|---|
/platform/* |
cameleer-saas:8080 | default |
/server/* |
cameleer-server-ui:80 | default |
/* |
logto:3001 (catch-all) | 1 (lowest) |
Configuration
# .env
CONTEXT_PATH=/platform # Change to /saas, /app, etc. No rebuild needed.
Implementation
1. Spring Boot — application.yml
server:
servlet:
context-path: ${CONTEXT_PATH:/platform}
Spring automatically prefixes all endpoints. Controllers, SecurityConfig matchers, interceptor patterns — all relative to context-path. No changes needed in Java code.
2. Vite — ui/vite.config.ts
Build with relative base so assets work from any prefix:
build: {
outDir: 'dist',
emptyOutDir: true,
assetsDir: '_app',
// removed: base (default '/' for dev, entrypoint injects <base> for production)
},
Change base to './' so index.html references become relative:
<!-- Before: <script src="/_app/index.js"> (absolute, breaks with prefix) -->
<!-- After: <script src="_app/index.js"> (relative, works from any base) -->
3. Container entrypoint — inject <base href>
Create docker/entrypoint.sh:
#!/bin/sh
# Inject <base href> into index.html for runtime base path support
CONTEXT_PATH="${CONTEXT_PATH:-/platform}"
sed -i "s|<head>|<head><base href=\"${CONTEXT_PATH}/\">|" /app/static/index.html
exec java -jar /app/app.jar
In Dockerfile (or docker-compose override for dev):
entrypoint: ["sh", "/app/entrypoint.sh"]
For dev mode (mounted dist), the docker-compose.dev.yml entrypoint runs the sed on the mounted file.
4. Frontend — derive base path at runtime
ui/src/config.ts — use document.baseURI:
const basePath = new URL(document.baseURI).pathname.replace(/\/$/, '');
// basePath = "/platform"
fetch(basePath + '/api/config')
ui/src/api/client.ts — dynamic API base:
const basePath = new URL(document.baseURI).pathname.replace(/\/$/, '');
const API_BASE = basePath + '/api';
ui/src/main.tsx — router basename:
const basePath = new URL(document.baseURI).pathname.replace(/\/$/, '') || '/';
<BrowserRouter basename={basePath}>
5. Docker Compose — Traefik labels
cameleer-saas:
labels:
- traefik.http.routers.saas.rule=PathPrefix(`${CONTEXT_PATH:-/platform}`)
- traefik.http.routers.saas.entrypoints=websecure
- traefik.http.routers.saas.tls=true
- traefik.http.services.saas.loadbalancer.server.port=8080
logto (catch-all):
labels:
- traefik.http.routers.logto.rule=PathPrefix(`/`)
- traefik.http.routers.logto.priority=1
- traefik.http.routers.logto.entrypoints=websecure
- traefik.http.routers.logto.tls=true
- traefik.http.services.logto.loadbalancer.server.port=3001
Remove all the enumerated Logto paths — Logto is now the catch-all.
6. Logto ENDPOINT
Logto's ENDPOINT stays at root (no prefix):
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
OIDC issuer = https://domain.com/oidc. Same domain as the SPA.
7. Bootstrap — redirect URIs
Update redirect URIs to include the context path:
SPA_REDIRECT_URIS="[\"${PROTO}://${HOST}${CONTEXT_PATH}/callback\"]"
SPA_POST_LOGOUT_URIS="[\"${PROTO}://${HOST}${CONTEXT_PATH}/login\"]"
Pass CONTEXT_PATH to the bootstrap container.
8. Tests — application-test.yml
server:
servlet:
context-path: /platform
Test MockMvc paths are relative to context-path, so existing test paths (/api/tenants, etc.) continue to work without changes.
Files to modify
src/main/resources/application.yml— add context-path propertysrc/main/resources/application-test.yml— add context-path for testsui/vite.config.ts—base: './'for relative assetsui/src/config.ts— derive base path fromdocument.baseURIui/src/api/client.ts— dynamicAPI_BASEui/src/main.tsx—BrowserRouter basenamefromdocument.baseURIdocker/entrypoint.sh— NEW, injects<base href>into index.htmldocker-compose.yml— Traefik labels (SaaS at/platform, Logto catch-all), passCONTEXT_PATHto bootstrapdocker/logto-bootstrap.sh— context path in redirect URIs
Files that do NOT change
- All 11 Java controllers — Spring context-path handles prefix transparently
SecurityConfig.java— matchers are relative to context-pathWebConfig.java— interceptor pattern relative to context-pathui/src/api/hooks.ts— uses centralizedAPI_BASE- All test files — MockMvc is context-path aware
Customer experience
# .env
PUBLIC_HOST=cameleer.mycompany.com
PUBLIC_PROTOCOL=https
CONTEXT_PATH=/platform
# DNS: 1 record
# cameleer.mycompany.com → server IP
# docker compose up -d
# SaaS at https://cameleer.mycompany.com/platform/
# Logto at https://cameleer.mycompany.com/ (login, OIDC)
# Server UI at https://cameleer.mycompany.com/server/