Add key class locations for Java backend and React frontend, document cameleer-traefik network topology with DNS alias, add server runtime env vars table, update deployment pipeline to 7-stage flow, add database migration reference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
14 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 + ClickHouse storage. React SPA dashboard. JWT auth with Ed25519 config signing. Docker container orchestration for app deployments.
- 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.
Key Classes
Java Backend (src/main/java/net/siegeln/cameleer/saas/)
config/ — Security, tenant isolation, web config
SecurityConfig.java— OAuth2 JWT decoder (ES384, issuer/audience validation, scope extraction)TenantIsolationInterceptor.java— HandlerInterceptor on/api/**; JWT org_id -> TenantContext, path variable validation, fail-closedTenantContext.java— ThreadLocal tenant ID storageWebConfig.java— registers TenantIsolationInterceptorPublicConfigController.java— GET /api/config (Logto endpoint, SPA client ID, scopes)MeController.java— GET /api/me (authenticated user, tenant list)
tenant/ — Tenant lifecycle
TenantEntity.java— JPA entity (id, name, slug, tier, status, logto_org_id, stripe IDs, settings JSONB)TenantService.java— create tenant -> Logto org, activate, suspendTenantController.java— POST create, GET list, GET by ID
license/ — License management
LicenseEntity.java— JPA entity (id, tenant_id, tier, features JSONB, limits JSONB, expires_at)LicenseService.java— generation, validation, feature/limit lookupsLicenseController.java— POST issue, GET verify, DELETE revoke
identity/ — Logto & server integration
LogtoConfig.java— Logto endpoint, M2M credentials (reads from bootstrap file)LogtoManagementClient.java— Logto Management API calls (create org, create user, add to org)ServerApiClient.java— M2M client for cameleer3-server API (Logto M2M token,X-Cameleer-Protocol-Version: 1header)
audit/ — Audit logging
AuditEntity.java— JPA entity (actor_id, tenant_id, action, resource, status)AuditService.java— log audit events (TENANT_CREATE, TENANT_UPDATE, etc.)
React Frontend (ui/src/)
main.tsx— React 19 rootrouter.tsx— /login, /callback, / -> OrgResolver -> Layout -> pagesconfig.ts— fetch Logto config from /platform/api/configauth/useAuth.ts— auth hook (isAuthenticated, logout, signIn)auth/useOrganization.ts— Zustand store for current tenantauth/useScopes.ts— decode JWT scopes, hasScope()auth/ProtectedRoute.tsx— guard (redirects to /login)pages/DashboardPage.tsx— tenant dashboardpages/LicensePage.tsx— license infopages/AdminTenantsPage.tsx— platform admin tenant management
Custom Sign-in UI (ui/sign-in/src/)
SignInPage.tsx— form with @cameleer/design-system componentsexperience-api.ts— Logto Experience API client (4-step: init -> verify -> identify -> submit)
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)
Docker Networks
Two networks in docker-compose.yml:
| Network | Name on Host | Purpose |
|---|---|---|
cameleer |
cameleer-saas_cameleer |
Compose default — all services (DB, Logto, SaaS, server) |
cameleer-traefik |
cameleer-traefik (fixed name:) |
Traefik + server + deployed app containers |
The cameleer-traefik network uses name: cameleer-traefik (no compose project prefix) so DockerNetworkManager.ensureNetwork("cameleer-traefik") in the server finds it. The server joins with DNS alias cameleer3-server, matching CAMELEER_SERVER_URL=http://cameleer3-server:8081. Per-environment networks (cameleer-env-{slug}) are created dynamically by the server's DockerNetworkManager.
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) - 4-role model:
saas-vendor(global, hosted only), orgowner->server:admin, orgoperator->server:operator, orgviewer->server:viewer saas-vendorglobal role injected viadocker/vendor-seed.sh(not standard bootstrap) — hasplatform:admin+ all tenant scopes- 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 runtime env vars (docker-compose.dev.yml)
| Env var | Value | Purpose |
|---|---|---|
CAMELEER_RUNTIME_ENABLED |
true |
Enable Docker orchestration |
CAMELEER_JAR_STORAGE_PATH |
/data/jars |
Where JARs are stored inside server container |
CAMELEER_RUNTIME_BASE_IMAGE |
gitea.siegeln.net/cameleer/cameleer-runtime-base:latest |
Base image for deployed apps |
CAMELEER_SERVER_URL |
http://cameleer3-server:8081 |
Server URL agents connect to |
CAMELEER_ROUTING_DOMAIN |
${PUBLIC_HOST} |
Domain for Traefik routing labels |
CAMELEER_ROUTING_MODE |
path |
path or subdomain routing |
CAMELEER_JAR_DOCKER_VOLUME |
cameleer-saas_jardata |
Named volume for Docker-in-Docker JAR mounting |
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.
Deployment pipeline
App deployment is handled by the cameleer3-server's DeploymentExecutor (7-stage async flow):
- PRE_FLIGHT — validate config, check JAR exists
- PULL_IMAGE — pull base image if missing
- CREATE_NETWORK — ensure cameleer-traefik and cameleer-env-{slug} networks
- START_REPLICAS — create N containers with Traefik labels
- HEALTH_CHECK — poll
/cameleer/healthon agent port 9464 - SWAP_TRAFFIC — stop old deployment (blue/green)
- COMPLETE — mark RUNNING or DEGRADED
Key files:
DeploymentExecutor.java(in cameleer3-server) — async staged deploymentDockerRuntimeOrchestrator.java(in cameleer3-server) — Docker client, container lifecycledocker/runtime-base/Dockerfile— base image with agent JAR, maps env vars to-Dsystem propertiesServerApiClient.java— M2M token acquisition for SaaS->server API calls (agent status). UsesX-Cameleer-Protocol-Version: 1header- Docker socket access:
group_add: ["0"]in docker-compose.dev.yml (not root group membership in Dockerfile) - Network: deployed containers join
cameleer-traefik(routing) +cameleer-env-{slug}(isolation)
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 + server API role) 3b. Create API resource scopes (10 platform + 3 server scopes) - Create org roles (owner, operator, viewer with API resource scope assignments) + M2M server role (
cameleer-m2m-serverwithserver:adminscope) - Create users (platform owner with Logto console access, viewer for testing read-only OIDC)
- Create organization, add users with org roles (owner + viewer)
- 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
Platform owner credentials (SAAS_ADMIN_USER/SAAS_ADMIN_PASS) work for both the SaaS platform and the Logto console (port 3002). The saas-vendor global role (hosted only) is created separately via docker/vendor-seed.sh.
Database Migrations
PostgreSQL (Flyway): src/main/resources/db/migration/
- V001 — tenants (id, name, slug, tier, status, logto_org_id, stripe IDs, settings JSONB)
- V002 — licenses (id, tenant_id, tier, features JSONB, limits JSONB, expires_at)
- V003 — environments (tenant -> environments 1:N)
- V004 — api_keys (auth tokens for agent registration)
- V005 — apps (Camel applications)
- V006 — deployments (app versions, deployment history)
- V007 — audit_log
- V008 — app resource limits
- V010 — cleanup of migrated tables
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 incameleer-runtime-base— base image for deployed apps (agent JAR + JRE). CI downloads latest agent SNAPSHOT from Gitea Maven registry. UsesCAMELEER_SERVER_URLenv var (not CAMELEER_EXPORT_ENDPOINT).
- 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). Adds Docker socket mount, jardata volume, and runtime env vars for container orchestration.- 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.