Files
cameleer-saas/CLAUDE.md
hsiegeln 1d6c0cf451
All checks were successful
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 18s
docs: update documentation for Docker orchestration and env var rename
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 22:09:19 +02:00

9.9 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 + 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)
  • 4-role model: saas-vendor (global, hosted only), org ownerserver:admin, org operatorserver:operator, org viewerserver: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 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 fully async via DeploymentExecutor (separate @Service so Spring @Async proxy works):

  1. Build image: FROM cameleer-runtime-base:latest + COPY app.jar (via DockerRuntimeOrchestrator)
  2. Stop/remove old container (by deployment metadata + orphan name cleanup)
  3. Start new container on the compose network with env vars (CAMELEER_AUTH_TOKEN, CAMELEER_SERVER_URL, etc.), Traefik labels, resource limits
  4. Wait for Docker health check (/cameleer/health on agent port 9464)
  5. Update deployment status to RUNNING or FAILED

Key files:

  • DeploymentExecutor.java — async deployment logic, extracted from DeploymentService to avoid Spring @Async self-invocation
  • DockerRuntimeOrchestrator.java — Docker client (zerodep transport for Unix socket), image build, 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.yml (not root group membership in Dockerfile)
  • Network: deployed containers join ${COMPOSE_PROJECT_NAME}_cameleer network

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.

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