Files
cameleer-saas/CLAUDE.md
hsiegeln 63e6c6b1b5
All checks were successful
CI / build (push) Successful in 1m18s
CI / docker (push) Successful in 19s
SonarQube Analysis / sonarqube (push) Successful in 1m12s
docs: update CLAUDE.md with key classes, network topology, and runtime env vars
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>
2026-04-08 23:11:03 +02:00

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 -javaagent JAR.
  • 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-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.

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-closed
  • TenantContext.java — ThreadLocal tenant ID storage
  • WebConfig.java — registers TenantIsolationInterceptor
  • PublicConfigController.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, suspend
  • TenantController.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 lookups
  • LicenseController.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: 1 header)

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 root
  • router.tsx — /login, /callback, / -> OrgResolver -> Layout -> pages
  • config.ts — fetch Logto config from /platform/api/config
  • auth/useAuth.ts — auth hook (isAuthenticated, logout, signIn)
  • auth/useOrganization.ts — Zustand store for current tenant
  • auth/useScopes.ts — decode JWT scopes, hasScope()
  • auth/ProtectedRoute.tsx — guard (redirects to /login)
  • pages/DashboardPage.tsx — tenant dashboard
  • pages/LicensePage.tsx — license info
  • pages/AdminTenantsPage.tsx — platform admin tenant management

Custom Sign-in UI (ui/sign-in/src/)

  • SignInPage.tsx — form with @cameleer/design-system components
  • experience-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/ (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)

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-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)
  • 4-role model: saas-vendor (global, hosted only), org owner -> server:admin, org operator -> server:operator, org viewer -> server:viewer
  • saas-vendor global role injected via docker/vendor-seed.sh (not standard bootstrap) — has platform:admin + all tenant scopes
  • 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 + <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):

  1. PRE_FLIGHT — validate config, check JAR exists
  2. PULL_IMAGE — pull base image if missing
  3. CREATE_NETWORK — ensure cameleer-traefik and cameleer-env-{slug} networks
  4. START_REPLICAS — create N containers with Traefik labels
  5. HEALTH_CHECK — poll /cameleer/health on agent port 9464
  6. SWAP_TRAFFIC — stop old deployment (blue/green)
  7. COMPLETE — mark RUNNING or DEGRADED

Key files:

  • DeploymentExecutor.java (in cameleer3-server) — async staged deployment
  • DockerRuntimeOrchestrator.java (in cameleer3-server) — Docker client, container lifecycle
  • docker/runtime-base/Dockerfile — base image with agent JAR, maps env vars to -D system properties
  • ServerApiClient.java — M2M token acquisition for SaaS->server API calls (agent status). Uses X-Cameleer-Protocol-Version: 1 header
  • 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:

  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 + server API role) 3b. Create API resource scopes (10 platform + 3 server scopes)
  4. Create org roles (owner, operator, viewer with API resource scope assignments) + M2M server role (cameleer-m2m-server with server:admin scope)
  5. Create users (platform owner with Logto console access, viewer for testing read-only OIDC)
  6. Create organization, add users with org roles (owner + viewer)
  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

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
  • 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
    • cameleer-runtime-base — base image for deployed apps (agent JAR + JRE). CI downloads latest agent SNAPSHOT from Gitea Maven registry. Uses CAMELEER_SERVER_URL env var (not CAMELEER_EXPORT_ENDPOINT).
  • 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). 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.