# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Cameleer SaaS — multi-tenant SaaS platform wrapping the Cameleer observability stack (Java agent + server) for Apache Camel applications. Customers get managed observability for their Camel integrations without running infrastructure. ## Ecosystem This repo is the SaaS layer on top of two proven components: - **cameleer3** (sibling repo) — Java agent using ByteBuddy for zero-code instrumentation of Camel apps. Captures route executions, processor traces, payloads, metrics, and route graph topology. Deploys as `-javaagent` JAR. - **cameleer3-server** (sibling repo) — Spring Boot observability backend. Receives agent data via HTTP, pushes config/commands via SSE. PostgreSQL + OpenSearch storage. React SPA dashboard. JWT auth with Ed25519 config signing. - **cameleer-website** — Marketing site (Astro 5) - **design-system** — Shared React component library (`@cameleer/design-system` on Gitea npm registry) Agent-server protocol is defined in `cameleer3/cameleer3-common/PROTOCOL.md`. The agent and server are mature, proven components — this repo wraps them with multi-tenancy, billing, and self-service onboarding. ## Architecture Context The existing cameleer3-server already has single-tenant auth (JWT, RBAC, bootstrap tokens, OIDC). The SaaS layer must: - Add multi-tenancy (tenant isolation of agent data, diagrams, configs) - Provide self-service signup, billing, and team management - Generate per-tenant bootstrap tokens for agent registration - Proxy or federate access to tenant-specific cameleer3-server instances - Enforce usage quotas and metered billing ### Routing (single-domain, path-based via Traefik) All services on one hostname. Two env vars control everything: `PUBLIC_HOST` + `PUBLIC_PROTOCOL`. | Path | Target | Notes | |------|--------|-------| | `/platform/*` | cameleer-saas:8080 | SPA + API (`server.servlet.context-path: /platform`) | | `/server/*` | cameleer3-server-ui:80 | Server dashboard (strip-prefix + `BASE_PATH=/server`) | | `/` | redirect → `/platform/` | Via `docker/traefik-dynamic.yml` | | `/*` (catch-all) | cameleer-logto:3001 (priority=1) | Custom sign-in UI, OIDC, interaction | - SPA assets at `/_app/` (Vite `assetsDir: '_app'`) to avoid conflict with Logto's `/assets/` - Logto `ENDPOINT` = `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` (same domain, same origin) - TLS: self-signed cert init container (`traefik-certs`) for dev, ACME for production - Root `/` → `/platform/` redirect via Traefik file provider (`docker/traefik-dynamic.yml`) - LoginPage auto-redirects to Logto OIDC (no intermediate button) ### Custom sign-in UI (`ui/sign-in/`) Separate Vite+React SPA replacing Logto's default sign-in page. Visually matches cameleer3-server LoginPage. - Built as custom Logto Docker image (`cameleer-logto`): `ui/sign-in/Dockerfile` = node build stage + `FROM ghcr.io/logto-io/logto:latest` + COPY dist over `/etc/logto/packages/experience/dist/` - Uses `@cameleer/design-system` components (Card, Input, Button, FormField, Alert) - Authenticates via Logto Experience API (4-step: init → verify password → identify → submit → redirect) - `CUSTOM_UI_PATH` env var does NOT work for Logto OSS — must volume-mount or replace the experience dist directory - Favicon bundled in `ui/sign-in/public/favicon.svg` (served by Logto, not SaaS) ### Auth enforcement - All API endpoints enforce OAuth2 scopes via `@PreAuthorize("hasAuthority('SCOPE_xxx')")` annotations - Tenant isolation enforced by `TenantIsolationInterceptor` (a single `HandlerInterceptor` on `/api/**` that resolves JWT org_id to TenantContext and validates `{tenantId}`, `{environmentId}`, `{appId}` path variables; fail-closed, platform admins bypass) - 13 OAuth2 scopes on the Logto API resource (`https://api.cameleer.local`): 10 platform scopes + 3 server scopes (`server:admin`, `server:operator`, `server:viewer`), served to the frontend from `GET /platform/api/config` - Server scopes map to server RBAC roles via JWT `scope` claim (SaaS platform path) or `roles` claim (server-ui OIDC login path) - Org role `admin` gets `server:admin`, org role `member` gets `server:viewer` - Custom `JwtDecoder` in `SecurityConfig.java` — ES384 algorithm, `at+jwt` token type, split issuer-uri (string validation) / jwk-set-uri (Docker-internal fetch), audience validation (`https://api.cameleer.local`) - Logto Custom JWT (Phase 7b in bootstrap) injects a `roles` claim into access tokens based on org roles and global roles — this makes role data available to the server without Logto-specific code ### Server integration (cameleer3-server env vars) | Env var | Value | Purpose | |---------|-------|---------| | `CAMELEER_OIDC_ISSUER_URI` | `${PUBLIC_PROTOCOL}://${PUBLIC_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 — disable in production) | | `CAMELEER_CORS_ALLOWED_ORIGINS` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` | Allow browser requests through Traefik | | `BASE_PATH` (server-ui) | `/server` | React Router basename + `` tag | ### Server OIDC role extraction (two paths) | Path | Token type | Role source | How it works | |------|-----------|-------------|--------------| | SaaS platform → server API | Logto org-scoped access token | `scope` claim | `JwtAuthenticationFilter.extractRolesFromScopes()` reads `server:admin` from scope | | Server-ui SSO login | Logto JWT access token (via Traditional Web App) | `roles` claim | `OidcTokenExchanger` decodes access_token, reads `roles` injected by Custom JWT | The server's OIDC config (`OidcConfig`) includes `audience` (RFC 8707 resource indicator) and `additionalScopes`. The `audience` is sent as `resource` in both the authorization request and token exchange, which makes Logto return a JWT access token instead of opaque. The Custom JWT script maps org roles to `roles: ["server:admin"]`. If OIDC returns no roles and the user already exists, `syncOidcRoles` preserves existing local roles. ### Bootstrap (`docker/logto-bootstrap.sh`) Idempotent script run via `logto-bootstrap` init container. Phases: 1. Wait for Logto + server health 2. Get Management API token (reads `m-default` secret from DB) 3. Create Logto apps (SPA, Traditional with `skipConsent`, M2M with Management API role) 3b. Create API resource scopes (10 platform + 3 server scopes) 4. Create roles (platform-admin, org admin/member with API resource scope assignments) 5. Create users (SaaS admin with platform-admin role + Logto console access, tenant admin) 6. Create organization, add users with org roles 7. Configure cameleer3-server OIDC (`rolesClaim: "roles"`, `audience`, `defaultRoles: ["VIEWER"]`) 7b. Configure Logto Custom JWT for access tokens (maps org roles → `roles` claim: admin→server:admin, member→server:viewer) 8. Configure Logto sign-in branding (Cameleer colors `#C6820E`/`#D4941E`, logo from `/platform/logo.svg`) 9. Cleanup seeded Logto apps 10. Write bootstrap results to `/data/logto-bootstrap.json` SaaS admin credentials (`SAAS_ADMIN_USER`/`SAAS_ADMIN_PASS`) work for both the SaaS platform and the Logto console (port 3002). ## Related Conventions - Gitea-hosted: `gitea.siegeln.net/cameleer/` - CI: `.gitea/workflows/` — Gitea Actions - K8s target: k3s cluster at 192.168.50.86 - Docker images: CI builds and pushes all images — Dockerfiles use multi-stage builds, no local builds needed - `cameleer-saas` — SaaS app (frontend + JAR baked in) - `cameleer-logto` — custom Logto with sign-in UI baked in - Docker builds: `--no-cache`, `--provenance=false` for Gitea compatibility - `docker-compose.dev.yml` — exposes ports for direct access, sets `SPRING_PROFILES_ACTIVE: dev`. Volume-mounts `./ui/dist` into the container so local UI builds are served without rebuilding the Docker image (`SPRING_WEB_RESOURCES_STATIC_LOCATIONS` overrides classpath) - Design system: import from `@cameleer/design-system` (Gitea npm registry) ## Disabled Skills - Do NOT use any `gsd:*` skills in this project. This includes all `/gsd:` prefixed commands.