# 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 # .env CONTEXT_PATH=/platform # Change to /saas, /app, etc. No rebuild needed. ``` ## Implementation ### 1. Spring Boot — `application.yml` ```yaml 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: ```ts build: { outDir: 'dist', emptyOutDir: true, assetsDir: '_app', // removed: base (default '/' for dev, entrypoint injects for production) }, ``` Change `base` to `'./'` so index.html references become relative: ```html ``` ### 3. Container entrypoint — inject `` Create `docker/entrypoint.sh`: ```sh #!/bin/sh # Inject into index.html for runtime base path support CONTEXT_PATH="${CONTEXT_PATH:-/platform}" sed -i "s|||" /app/static/index.html exec java -jar /app/app.jar ``` In Dockerfile (or docker-compose override for dev): ```yaml 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`: ```ts const basePath = new URL(document.baseURI).pathname.replace(/\/$/, ''); // basePath = "/platform" fetch(basePath + '/api/config') ``` **`ui/src/api/client.ts`** — dynamic API base: ```ts const basePath = new URL(document.baseURI).pathname.replace(/\/$/, ''); const API_BASE = basePath + '/api'; ``` **`ui/src/main.tsx`** — router basename: ```tsx const basePath = new URL(document.baseURI).pathname.replace(/\/$/, '') || '/'; ``` ### 5. Docker Compose — Traefik labels **cameleer-saas:** ```yaml 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):** ```yaml 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): ```yaml 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: ```sh 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` ```yaml 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 property - `src/main/resources/application-test.yml` — add context-path for tests - `ui/vite.config.ts` — `base: './'` for relative assets - `ui/src/config.ts` — derive base path from `document.baseURI` - `ui/src/api/client.ts` — dynamic `API_BASE` - `ui/src/main.tsx` — `BrowserRouter basename` from `document.baseURI` - `docker/entrypoint.sh` — NEW, injects `` into index.html - `docker-compose.yml` — Traefik labels (SaaS at `/platform`, Logto catch-all), pass `CONTEXT_PATH` to bootstrap - `docker/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-path - `WebConfig.java` — interceptor pattern relative to context-path - `ui/src/api/hooks.ts` — uses centralized `API_BASE` - All test files — MockMvc is context-path aware ## Customer experience ```env # .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/ ```