- Read user profile from Logto ID token in OrgResolver, store in Zustand org store, display in sidebar footer and TopBar avatar - Fix license limits showing "—" by aligning frontend LIMIT_LABELS keys with backend snake_case convention (max_agents, retention_days, max_environments) - Bump @cameleer/design-system to v0.1.38 (font-size floor) - Add dev volume mount for local UI hot-reload without image rebuild Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8.1 KiB
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
-javaagentJAR. - 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-systemon 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/(ViteassetsDir: '_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-systemcomponents (Card, Input, Button, FormField, Alert) - Authenticates via Logto Experience API (4-step: init → verify password → identify → submit → redirect)
CUSTOM_UI_PATHenv 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 singleHandlerInterceptoron/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 fromGET /platform/api/config - Server scopes map to server RBAC roles via JWT
scopeclaim (SaaS platform path) orrolesclaim (server-ui OIDC login path) - Org role
admingetsserver:admin, org rolemembergetsserver:viewer - Custom
JwtDecoderinSecurityConfig.java— ES384 algorithm,at+jwttoken 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
rolesclaim 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 + <base> 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:
- Wait for Logto + server health
- Get Management API token (reads
m-defaultsecret from DB) - Create Logto apps (SPA, Traditional with
skipConsent, M2M with Management API role) 3b. Create API resource scopes (10 platform + 3 server scopes) - Create roles (platform-admin, org admin/member with API resource scope assignments)
- Create users (SaaS admin with platform-admin role + Logto console access, tenant admin)
- Create organization, add users with org roles
- Configure cameleer3-server OIDC (
rolesClaim: "roles",audience,defaultRoles: ["VIEWER"]) 7b. Configure Logto Custom JWT for access tokens (maps org roles →rolesclaim: admin→server:admin, member→server:viewer) - Configure Logto sign-in branding (Cameleer colors
#C6820E/#D4941E, logo from/platform/logo.svg) - Cleanup seeded Logto apps
- 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=falsefor Gitea compatibility docker-compose.dev.yml— exposes ports for direct access, setsSPRING_PROFILES_ACTIVE: dev. Volume-mounts./ui/distinto the container so local UI builds are served without rebuilding the Docker image (SPRING_WEB_RESOURCES_STATIC_LOCATIONSoverrides 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.