From b459a690838864fcd0158f555495f5914adf6397 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:19:05 +0200 Subject: [PATCH] docs: add architecture document Comprehensive technical reference covering system topology, auth model (Logto OIDC, scopes, token types, Spring Security pipeline), data model (7 tables from Flyway migrations), deployment flow, agent-server protocol, API endpoints, security boundaries, frontend architecture, and full configuration reference. All class names, paths, and properties verified against the codebase. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/architecture.md | 904 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 904 insertions(+) create mode 100644 docs/architecture.md diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..df77c2f --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,904 @@ +# Cameleer SaaS Architecture + +**Last updated:** 2026-04-05 +**Status:** Living document -- update as the system evolves + +--- + +## 1. System Overview + +Cameleer SaaS is a multi-tenant platform that provides managed observability for +Apache Camel applications. Customers deploy their Camel JARs through the SaaS +platform and get zero-code instrumentation, execution tracing, route topology +visualization, and runtime control -- without running any observability +infrastructure themselves. + +The system comprises three components: + +**Cameleer Agent** (`cameleer3` repo) -- A Java agent using ByteBuddy for +zero-code bytecode instrumentation. Captures route executions, processor traces, +payloads, metrics, and route graph topology. Deployed as a `-javaagent` JAR +alongside the customer's application. + +**Cameleer Server** (`cameleer3-server` repo) -- A Spring Boot observability +backend. Receives telemetry from agents via HTTP, pushes configuration and +commands to agents via SSE. Stores data in PostgreSQL and ClickHouse. Provides +a React SPA dashboard for direct observability access. JWT auth with Ed25519 +config signing. + +**Cameleer SaaS** (this repo) -- The multi-tenancy, deployment, and management +layer. Handles user authentication via Logto OIDC, tenant provisioning, JAR +upload and deployment, API key management, license generation, and audit +logging. Serves a React SPA that wraps the full user experience. + +--- + +## 2. Component Topology + +``` + Internet / LAN + | + +-----+-----+ + | Traefik | :80 / :443 + | (v3) | Reverse proxy + TLS termination + +-----+-----+ + | + +---------------+---------------+-------------------+ + | | | | + PathPrefix(/api) PathPrefix(/) PathPrefix(/oidc) PathPrefix(/observe) + PathPrefix(/api) priority=1 PathPrefix( PathPrefix(/dashboard) + | | /interaction) | + v v v v + +--------------+ +--------------+ +-----------+ +------------------+ + | cameleer-saas| | cameleer-saas| | Logto | | cameleer3-server | + | (API) | | (SPA) | | | | | + | :8080 | | :8080 | | :3001 | | :8081 | + +--------------+ +--------------+ +-----------+ +------------------+ + | | | + +------+-------------------------+------------------+ + | | | + +------+------+ +------+------+ +------+------+ + | PostgreSQL | | PostgreSQL | | ClickHouse | + | :5432 | | (logto DB) | | :8123 | + | cameleer_ | | :5432 | | cameleer | + | saas DB | +--------------+ +-------------+ + +--------------+ + | + +------+------+ + | Customer | + | App + Agent | + | (container) | + +-------------+ +``` + +### Services + +| Service | Image | Internal Port | Network | Purpose | +|-------------------|---------------------------------------------|---------------|----------|----------------------------------| +| traefik | `traefik:v3` | 80, 443 | cameleer | Reverse proxy, TLS, routing | +| postgres | `postgres:16-alpine` | 5432 | cameleer | Shared PostgreSQL (3 databases) | +| logto | `ghcr.io/logto-io/logto:latest` | 3001 | cameleer | OIDC identity provider | +| logto-bootstrap | `postgres:16-alpine` (ephemeral) | -- | cameleer | One-shot bootstrap script | +| cameleer-saas | `gitea.siegeln.net/cameleer/cameleer-saas` | 8080 | cameleer | SaaS API + SPA serving | +| cameleer3-server | `gitea.siegeln.net/cameleer/cameleer3-server`| 8081 | cameleer | Observability backend | +| clickhouse | `clickhouse/clickhouse-server:latest` | 8123 | cameleer | Time-series telemetry storage | + +### Docker Network + +All services share a single Docker bridge network named `cameleer`. Customer app +containers are also attached to this network so agents can reach the +cameleer3-server. + +### Volumes + +| Volume | Mounted By | Purpose | +|-----------------|---------------------|--------------------------------------------| +| `pgdata` | postgres | PostgreSQL data persistence | +| `chdata` | clickhouse | ClickHouse data persistence | +| `acme` | traefik | TLS certificate storage | +| `jardata` | cameleer-saas | Uploaded customer JAR files | +| `bootstrapdata` | logto-bootstrap, cameleer-saas | Bootstrap output JSON (shared) | + +### Databases on PostgreSQL + +The shared PostgreSQL instance hosts three databases: + +- `cameleer_saas` -- SaaS platform tables (tenants, environments, apps, etc.) +- `logto` -- Logto identity provider data +- `cameleer3` -- cameleer3-server operational data + +The `docker/init-databases.sh` init script creates all three during first start. + +--- + +## 3. Authentication & Authorization + +### 3.1 Design Principles + +1. **Logto is the single identity provider** for all human users. +2. **Zero trust** -- every service validates tokens independently via JWKS or its + own signing key. No identity in HTTP headers. +3. **No custom crypto** -- standard protocols only (OAuth2, OIDC, JWT, SHA-256). +4. **API keys for agents** -- per-environment opaque secrets, exchanged for + server-issued JWTs via the bootstrap registration flow. + +### 3.2 Token Types + +| Token | Issuer | Algorithm | Validator | Used By | +|--------------------|-----------------|------------------|----------------------|--------------------------------| +| Logto user JWT | Logto | ES384 (asymmetric)| Any service via JWKS | SaaS UI users, server users | +| Logto M2M JWT | Logto | ES384 (asymmetric)| Any service via JWKS | SaaS platform -> server calls | +| Server internal JWT| cameleer3-server| HS256 (symmetric) | Issuing server only | Agents (after registration) | +| API key (opaque) | SaaS platform | N/A (SHA-256 hash)| cameleer3-server | Agent initial registration | +| Ed25519 signature | cameleer3-server| EdDSA | Agent | Server -> agent command signing| + +### 3.3 Scope Model + +The Logto API resource `https://api.cameleer.local` has 10 scopes, created by +the bootstrap script (`docker/logto-bootstrap.sh`): + +| Scope | Description | Platform Admin | Org Admin | Org Member | +|--------------------|--------------------------------|:--------------:|:---------:|:----------:| +| `platform:admin` | SaaS platform administration | x | | | +| `tenant:manage` | Manage tenant settings | x | x | | +| `billing:manage` | Manage billing | x | x | | +| `team:manage` | Manage team members | x | x | | +| `apps:manage` | Create and delete apps | x | x | | +| `apps:deploy` | Deploy apps | x | x | x | +| `secrets:manage` | Manage secrets | x | x | | +| `observe:read` | View observability data | x | x | x | +| `observe:debug` | Debug and replay operations | x | x | x | +| `settings:manage` | Manage settings | x | x | | + +**Role hierarchy:** + +- **Global role `platform-admin`** -- All 10 scopes. Assigned to SaaS owner. +- **Organization role `admin`** -- 9 tenant-level scopes (all except `platform:admin`). +- **Organization role `member`** -- 3 scopes: `apps:deploy`, `observe:read`, + `observe:debug`. + +### 3.4 Authentication Flows + +**Human user -> SaaS Platform:** + +``` + Browser Logto cameleer-saas + | | | + |--- OIDC auth code flow ->| | + |<-- id_token, auth code --| | + | | | + |--- getAccessToken(resource, orgId) ---------------->| + | (org-scoped JWT with scope claim) | + | | | + |--- GET /api/me, Authorization: Bearer ------->| + | | validate via JWKS | + | | extract organization_id| + | | resolve to tenant | + |<-- { userId, tenants } -----------------------------| +``` + +1. User authenticates with Logto (OIDC authorization code flow via `@logto/react`). +2. Frontend obtains org-scoped access token via `getAccessToken(resource, orgId)`. +3. Backend validates via Logto JWKS (Spring OAuth2 Resource Server). +4. `organization_id` claim in JWT resolves to internal tenant ID via + `TenantResolutionFilter`. + +**SaaS platform -> cameleer3-server API (M2M):** + +1. SaaS platform obtains Logto M2M token (`client_credentials` grant) via + `LogtoManagementClient`. +2. Calls server API with `Authorization: Bearer `. +3. Server validates via Logto JWKS (OIDC resource server support). +4. Server grants ADMIN role to valid M2M tokens. + +**Agent -> cameleer3-server:** + +1. Agent reads `CAMELEER_AUTH_TOKEN` environment variable (API key). +2. Calls `POST /api/v1/agents/register` with the key as Bearer token. +3. Server validates via `BootstrapTokenValidator` (constant-time comparison). +4. Server issues internal HMAC JWT (access + refresh) + Ed25519 public key. +5. Agent uses JWT for all subsequent requests, refreshes on expiry. + +**Server -> Agent (commands):** + +1. Server signs command payload with Ed25519 private key. +2. Sends via SSE with signature field. +3. Agent verifies using server's public key (received at registration). +4. Destructive commands require a nonce (replay protection). + +### 3.5 Spring Security Configuration + +`SecurityConfig.java` configures a single stateless filter chain: + +```java +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(s -> s.sessionCreationPolicy(STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/actuator/health").permitAll() + .requestMatchers("/api/config").permitAll() + .requestMatchers("/", "/index.html", "/login", "/callback", + "/environments/**", "/license", "/admin/**").permitAll() + .requestMatchers("/assets/**", "/favicon.ico").permitAll() + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> + jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))) + .addFilterAfter(tenantResolutionFilter, + BearerTokenAuthenticationFilter.class); + return http.build(); + } +} +``` + +**JWT processing pipeline:** + +1. `BearerTokenAuthenticationFilter` (Spring built-in) extracts the Bearer token. +2. `JwtDecoder` validates the token signature (ES384 via Logto JWKS) and issuer. + Accepts both `JWT` and `at+jwt` token types (RFC 9068 / Logto convention). +3. `JwtAuthenticationConverter` maps the `scope` claim to Spring authorities: + `scope: "platform:admin observe:read"` becomes `SCOPE_platform:admin` and + `SCOPE_observe:read`. +4. `TenantResolutionFilter` reads `organization_id` from the JWT, resolves it + to an internal tenant UUID via `TenantService.getByLogtoOrgId()`, and stores + it on `TenantContext` (ThreadLocal). + +**Authorization pattern** (used in controllers): + +```java +@PreAuthorize("hasAuthority('SCOPE_platform:admin')") +public ResponseEntity> listAll() { ... } +``` + +### 3.6 Frontend Auth Architecture + +**Logto SDK integration** (`main.tsx`): + +The `LogtoProvider` is configured with scopes including `UserScope.Organizations` +and `UserScope.OrganizationRoles`, requesting organization-aware tokens from +Logto. + +**Token management** (`TokenSync` component in `main.tsx`): + +When an organization is selected, `setTokenProvider` is called with +`getAccessToken(resource, orgId)` to produce org-scoped JWTs. When no org is +selected, a non-org-scoped token is used. + +**Organization resolution** (`OrgResolver.tsx`): + +1. Calls `GET /api/me` to fetch the user's tenant memberships. +2. Populates the Zustand org store (`useOrgStore`) with org-to-tenant mappings. +3. Auto-selects the first org if the user belongs to exactly one. +4. Decodes the access token JWT to extract scopes and stores them via + `setScopes()`. + +**Scope-based UI gating:** + +The `useOrgStore` exposes a `scopes: Set` that components check to +conditionally render UI elements. For example, admin-only controls check for +`platform:admin` in the scope set. + +**Route protection** (`ProtectedRoute.tsx`): + +Wraps authenticated routes. Redirects to `/login` when the user is not +authenticated. Uses a ref to avoid showing a spinner after the initial auth +check completes (the Logto SDK sets `isLoading=true` for every async method, +not just initial load). + +--- + +## 4. Data Model + +### 4.1 Entity Relationship Diagram + +``` + +-------------------+ + | tenants | + +-------------------+ + | id (PK, UUID) | + | name | + | slug (UNIQUE) | + | tier | + | status | + | logto_org_id | + | stripe_customer_id| + | stripe_sub_id | + | settings (JSONB) | + | created_at | + | updated_at | + +--------+----------+ + | + +-----+-----+------------------+ + | | | + v v v + +----------+ +----------+ +-----------+ + | licenses | | environ- | | audit_log | + | | | ments | | | + +----------+ +----------+ +-----------+ + | id (PK) | | id (PK) | | id (PK) | + | tenant_id| | tenant_id| | tenant_id | + | tier | | slug | | actor_id | + | features | | display_ | | action | + | limits | | name | | resource | + | token | | status | | result | + | issued_at| | created_ | | metadata | + | expires_ | | at | | created_at| + | at | +-----+----+ +-----------+ + +----------+ | + +----+----+ + | | + v v + +----------+ +-----------+ + | api_keys | | apps | + +----------+ +-----------+ + | id (PK) | | id (PK) | + | environ_ | | environ_ | + | ment_id | | ment_id | + | key_hash | | slug | + | key_ | | display_ | + | prefix | | name | + | status | | jar_* | + | created_ | | exposed_ | + | at | | port | + | revoked_ | | current_ | + | at | | deploy_id| + +----------+ | previous_ | + | deploy_id| + +-----+-----+ + | + v + +-------------+ + | deployments | + +-------------+ + | id (PK) | + | app_id | + | version | + | image_ref | + | desired_ | + | status | + | observed_ | + | status | + | orchestrator| + | _metadata | + | error_msg | + | deployed_at | + | stopped_at | + | created_at | + +-------------+ +``` + +### 4.2 Table Descriptions + +**`tenants`** (V001) -- Top-level multi-tenancy entity. Each tenant maps to a +Logto organization via `logto_org_id`. The `tier` column (`LOW` default) drives +license feature gates. The `status` column tracks provisioning state +(`PROVISIONING`, `ACTIVE`, etc.). `settings` is a JSONB bag for tenant-specific +configuration. Stripe columns support future billing integration. + +**`licenses`** (V002) -- Per-tenant license tokens with feature flags and usage +limits. The `token` column stores the generated license string. `features` and +`limits` are JSONB columns holding structured capability data. Licenses have +explicit expiry and optional revocation. + +**`environments`** (V003) -- Logical deployment environments within a tenant +(e.g., `dev`, `staging`, `production`). Scoped by `(tenant_id, slug)` unique +constraint. Each environment gets its own set of API keys and apps. + +**`api_keys`** (V004) -- Per-environment opaque API keys for agent +authentication. The plaintext is never stored -- only `key_hash` (SHA-256 hex, +64 chars) and `key_prefix` (first 12 chars of the `cmk_`-prefixed key, for +identification). Status lifecycle: `ACTIVE` -> `ROTATED` or `REVOKED`. + +**`apps`** (V005) -- Customer applications within an environment. Tracks +uploaded JAR metadata (`jar_storage_path`, `jar_checksum`, `jar_size_bytes`, +`jar_original_filename`), optional `exposed_port` for inbound HTTP routing, +and deployment references (`current_deployment_id`, `previous_deployment_id` +for rollback). + +**`deployments`** (V006) -- Versioned deployment records for each app. Tracks a +two-state lifecycle: `desired_status` (what the user wants: `RUNNING` or +`STOPPED`) and `observed_status` (what the system sees: `BUILDING`, `STARTING`, +`RUNNING`, `STOPPED`, `FAILED`). `orchestrator_metadata` (JSONB) stores the +Docker container ID. Versioned with `(app_id, version)` unique constraint. + +**`audit_log`** (V007) -- Append-only audit trail. Records actor, tenant, +action, resource, environment, result, and optional metadata JSONB. Indexed +by `(tenant_id, created_at)`, `(actor_id, created_at)`, and +`(action, created_at)` for efficient querying. + +### 4.3 Audit Actions + +Defined in `AuditAction.java`: + +| Category | Actions | +|---------------|----------------------------------------------------------------| +| Auth | `AUTH_REGISTER`, `AUTH_LOGIN`, `AUTH_LOGIN_FAILED`, `AUTH_LOGOUT`| +| Tenant | `TENANT_CREATE`, `TENANT_UPDATE`, `TENANT_SUSPEND`, `TENANT_REACTIVATE`, `TENANT_DELETE` | +| Environment | `ENVIRONMENT_CREATE`, `ENVIRONMENT_UPDATE`, `ENVIRONMENT_DELETE`| +| App lifecycle | `APP_CREATE`, `APP_DEPLOY`, `APP_PROMOTE`, `APP_ROLLBACK`, `APP_SCALE`, `APP_STOP`, `APP_DELETE` | +| Secrets | `SECRET_CREATE`, `SECRET_READ`, `SECRET_UPDATE`, `SECRET_DELETE`, `SECRET_ROTATE` | +| Config | `CONFIG_UPDATE` | +| Team | `TEAM_INVITE`, `TEAM_REMOVE`, `TEAM_ROLE_CHANGE` | +| License | `LICENSE_GENERATE`, `LICENSE_REVOKE` | + +--- + +## 5. Deployment Model + +### 5.1 Server-Per-Tenant + +Each tenant gets a dedicated cameleer3-server instance. The SaaS platform +provisions and manages these servers. In the current Docker Compose topology, a +single shared cameleer3-server is used for the default tenant. Production +deployments will run per-tenant servers as separate containers or K8s pods. + +### 5.2 Customer App Deployment Flow + +The deployment lifecycle is managed by `DeploymentService`: + +``` + User uploads JAR Build Docker image Start container + via AppController --> from base image + --> on cameleer network + (multipart POST) uploaded JAR with agent env vars + | | | + v v v + apps.jar_storage_path deployments.image_ref deployments.orchestrator_metadata + apps.jar_checksum deployments.observed_ {"containerId": "..."} + apps.jar_size_bytes status = BUILDING +``` + +**Step-by-step (from `DeploymentService.deploy()`):** + +1. **Validate** -- Ensure the app has an uploaded JAR. +2. **Version** -- Increment deployment version via + `deploymentRepository.findMaxVersionByAppId()`. +3. **Image ref** -- Generate `cameleer-runtime-{env}-{app}:v{n}`. +4. **Persist** -- Save deployment record with `observed_status = BUILDING`. +5. **Audit** -- Log `APP_DEPLOY` action. +6. **Async execution** (`@Async("deploymentExecutor")`): + a. Build Docker image from base image + customer JAR. + b. Stop previous container if one exists. + c. Start new container with environment variables: + + | Variable | Value | + |-----------------------------|----------------------------------------| + | `CAMELEER_AUTH_TOKEN` | API key for agent registration | + | `CAMELEER_EXPORT_TYPE` | `HTTP` | + | `CAMELEER_EXPORT_ENDPOINT` | cameleer3-server internal URL | + | `CAMELEER_APPLICATION_ID` | App slug | + | `CAMELEER_ENVIRONMENT_ID` | Environment slug | + | `CAMELEER_DISPLAY_NAME` | `{tenant}-{env}-{app}` | + + d. Apply resource limits (`container-memory-limit`, `container-cpu-shares`). + e. Configure Traefik labels for inbound routing if `exposed_port` is set: + `{app}.{env}.{tenant}.{domain}`. + f. Poll container health for up to `health-check-timeout` seconds. + g. Update deployment status to `RUNNING` or `FAILED`. + h. Update app's `current_deployment_id` and `previous_deployment_id`. + +### 5.3 Container Resource Limits + +Configured via `RuntimeConfig`: + +| Property | Default | Description | +|-----------------------------------|-------------|-----------------------------| +| `cameleer.runtime.container-memory-limit` | `512m` | Docker memory limit | +| `cameleer.runtime.container-cpu-shares` | `512` | Docker CPU shares | +| `cameleer.runtime.max-jar-size` | `200MB` | Max upload size | +| `cameleer.runtime.health-check-timeout` | `60` | Seconds to wait for healthy | +| `cameleer.runtime.deployment-thread-pool-size` | `4`| Concurrent deployments | + +--- + +## 6. Agent-Server Protocol + +The agent-server protocol is defined in full in +`cameleer3/cameleer3-common/PROTOCOL.md`. This section summarizes the key +aspects relevant to the SaaS platform. + +### 6.1 Agent Registration + +1. Agent starts with `CAMELEER_AUTH_TOKEN` environment variable (an API key + generated by the SaaS platform, prefixed with `cmk_`). +2. Agent calls `POST /api/v1/agents/register` on the cameleer3-server with the + API key as a Bearer token. +3. Server validates the key and returns: + - HMAC JWT access token (short-lived, ~1 hour) + - HMAC JWT refresh token (longer-lived, ~7 days) + - Ed25519 public key (for verifying server commands) +4. Agent uses the access token for all subsequent API calls. +5. On access token expiry, agent uses refresh token to obtain a new pair. +6. On refresh token expiry, agent re-registers using the original API key. + +### 6.2 Telemetry Ingestion + +Agents send telemetry to the server via HTTP POST: +- Route executions with processor-level traces +- Payload captures (configurable granularity with redaction) +- Route graph topology (tree + graph dual representation) +- Metrics and heartbeats + +### 6.3 Server-to-Agent Commands (SSE) + +The server maintains an SSE (Server-Sent Events) push channel to each agent: +- Configuration changes (engine level, payload capture settings) +- Deep trace requests for specific correlation IDs +- Exchange replay commands +- Per-processor payload capture overrides + +**Command signing:** All commands are signed with the server's Ed25519 private +key. The agent verifies signatures using the public key received during +registration. Destructive commands include a nonce for replay protection. + +--- + +## 7. API Overview + +All endpoints under `/api/` require authentication unless noted otherwise. +Authentication is via Logto JWT Bearer token. + +### 7.1 Platform Configuration + +| Method | Path | Auth | Description | +|--------|-------------------|----------|--------------------------------------------| +| GET | `/api/config` | Public | Frontend config (Logto endpoint, client ID, API resource) | +| GET | `/api/health/secured` | JWT | Auth verification endpoint | +| GET | `/actuator/health`| Public | Spring Boot health check | + +### 7.2 Identity + +| Method | Path | Auth | Description | +|--------|-------------------|----------|--------------------------------------------| +| GET | `/api/me` | JWT | Current user info + tenant memberships | + +`MeController` extracts `organization_id` from the JWT to resolve the tenant. +For non-org-scoped tokens, it falls back to `LogtoManagementClient.getUserOrganizations()` +to enumerate all organizations the user belongs to. + +### 7.3 Tenants + +| Method | Path | Auth | Description | +|--------|----------------------------|----------------------------------|------------------------| +| GET | `/api/tenants` | `SCOPE_platform:admin` | List all tenants | +| POST | `/api/tenants` | `SCOPE_platform:admin` | Create tenant | +| GET | `/api/tenants/{id}` | JWT | Get tenant by UUID | +| GET | `/api/tenants/by-slug/{slug}` | JWT | Get tenant by slug | + +### 7.4 Environments + +| Method | Path | Auth | Description | +|--------|----------------------------------------------------|------|--------------------------| +| POST | `/api/tenants/{tenantId}/environments` | JWT | Create environment | +| GET | `/api/tenants/{tenantId}/environments` | JWT | List environments | +| GET | `/api/tenants/{tenantId}/environments/{envId}` | JWT | Get environment | +| PATCH | `/api/tenants/{tenantId}/environments/{envId}` | JWT | Update display name | +| DELETE | `/api/tenants/{tenantId}/environments/{envId}` | JWT | Delete environment | + +### 7.5 Apps + +| Method | Path | Auth | Description | +|--------|----------------------------------------------------|------|------------------------| +| POST | `/api/environments/{envId}/apps` | JWT | Create app (multipart: metadata + JAR) | +| GET | `/api/environments/{envId}/apps` | JWT | List apps | +| GET | `/api/environments/{envId}/apps/{appId}` | JWT | Get app | +| PUT | `/api/environments/{envId}/apps/{appId}/jar` | JWT | Re-upload JAR | +| DELETE | `/api/environments/{envId}/apps/{appId}` | JWT | Delete app | +| PATCH | `/api/environments/{envId}/apps/{appId}/routing` | JWT | Set exposed port | + +### 7.6 Deployments + +| Method | Path | Auth | Description | +|--------|----------------------------------------------------|------|--------------------------| +| POST | `/api/apps/{appId}/deploy` | JWT | Deploy app (async, 202) | +| POST | `/api/apps/{appId}/stop` | JWT | Stop running deployment | +| POST | `/api/apps/{appId}/restart` | JWT | Stop + redeploy | +| GET | `/api/apps/{appId}/deployments` | JWT | List deployment history | +| GET | `/api/apps/{appId}/deployments/{deploymentId}` | JWT | Get deployment details | + +### 7.7 Observability + +| Method | Path | Auth | Description | +|--------|--------------------------------------------------|------|---------------------------| +| GET | `/api/apps/{appId}/agent-status` | JWT | Agent connectivity status | +| GET | `/api/apps/{appId}/observability-status` | JWT | Observability data status | +| GET | `/api/apps/{appId}/logs` | JWT | Container logs (query params: `since`, `until`, `limit`, `stream`) | + +### 7.8 Licenses + +| Method | Path | Auth | Description | +|--------|-------------------------------------------------|------|--------------------------| +| POST | `/api/tenants/{tenantId}/license` | JWT | Generate license (365d) | +| GET | `/api/tenants/{tenantId}/license` | JWT | Get active license | + +### 7.9 SPA Routing + +The `SpaController` forwards all non-API paths to `index.html` for client-side +routing: + +```java +@GetMapping(value = {"/", "/login", "/callback", "/environments/**", "/license"}) +public String spa() { return "forward:/index.html"; } +``` + +--- + +## 8. Security Model + +### 8.1 Tenant Isolation + +- Each tenant maps to a Logto organization via `logto_org_id`. +- `TenantResolutionFilter` runs after JWT authentication on every request, + extracting `organization_id` from the JWT and storing the resolved tenant UUID + in `TenantContext` (ThreadLocal). +- Environment and app queries are scoped by tenant through foreign key + relationships (`environments.tenant_id`). +- Customer app containers run in isolated Docker containers with per-container + resource limits. + +### 8.2 API Key Security + +- Keys are generated with 32 bytes of `SecureRandom` entropy, prefixed with + `cmk_` and Base64url-encoded. +- Only the SHA-256 hash is stored in the database (`key_hash` column, 64 hex + chars). The `key_prefix` (first 12 chars) is stored for identification in + UI listings. +- The plaintext key is returned exactly once at creation time and never stored. +- Key lifecycle: `ACTIVE` -> `ROTATED` (old keys remain for grace period) or + `REVOKED` (immediately invalidated, `revoked_at` timestamp set). +- Validation is via SHA-256 hash comparison: + `ApiKeyService.validate(plaintext)` -> hash -> lookup by hash and status. + +### 8.3 Token Lifetimes + +| Token | Lifetime | Notes | +|----------------------|-------------|------------------------------------| +| Logto access token | ~1 hour | Configured in Logto, refreshed by SDK | +| Logto refresh token | ~14 days | Used by `@logto/react` for silent refresh | +| Server agent JWT | ~1 hour | cameleer3-server `CAMELEER_JWT_SECRET` | +| Server refresh token | ~7 days | Agent re-registers when expired | + +### 8.4 Audit Logging + +All state-changing operations are logged to the `audit_log` table via +`AuditService.log()`. Each entry records: + +- `actor_id` -- UUID of the user (from JWT subject) +- `tenant_id` -- UUID of the affected tenant +- `action` -- Enum value from `AuditAction` +- `resource` -- Identifier of the affected resource (e.g., app slug) +- `environment` -- Environment slug if applicable +- `result` -- `SUCCESS` or error indicator +- `metadata` -- Optional JSONB for additional context + +Audit entries are immutable (append-only, no UPDATE/DELETE operations). + +### 8.5 Security Boundaries + +- CSRF is disabled (stateless API, Bearer token auth only). +- Sessions are disabled (`SessionCreationPolicy.STATELESS`). +- The Docker socket is mounted read-write on cameleer-saas for container + management. This is the highest-privilege access in the system. +- Logto's admin endpoint (`:3002`) is not exposed through Traefik. +- ClickHouse has no external port exposure. + +--- + +## 9. Frontend Architecture + +### 9.1 Stack + +| Technology | Purpose | +|-----------------------|-------------------------------------------| +| React 19 | UI framework | +| Vite | Build tool and dev server | +| `@logto/react` | OIDC SDK (auth code flow, token mgmt) | +| Zustand | Org/tenant state management (`useOrgStore`)| +| TanStack React Query | Server state, caching, background refresh | +| React Router (v7) | Client-side routing | +| `@cameleer/design-system` | Shared component library (Gitea npm) | + +### 9.2 Component Hierarchy + +``` + + + + + + + -- Manages org-scoped token provider + + + + /login -- LoginPage + /callback -- CallbackPage (OIDC redirect) + + -- Fetches /api/me, populates org store + + / -- DashboardPage + /environments -- EnvironmentsPage + /environments/:envId -- EnvironmentDetailPage + /environments/:envId/apps/:appId -- AppDetailPage + /license -- LicensePage + /admin/tenants -- AdminTenantsPage +``` + +### 9.3 Auth Data Flow + +``` + LogtoProvider + | + v + ProtectedRoute -- Gates on isAuthenticated, redirects to /login + | + v + OrgResolver -- Calls GET /api/me + | -- Maps tenants to OrgInfo objects + | -- Stores in useOrgStore.organizations + | -- Decodes JWT to extract scopes + | -- Stores scopes in useOrgStore.scopes + v + Layout + pages -- Read from useOrgStore for tenant context + -- Read from useAuth() for auth state + -- Read scopes for UI gating +``` + +### 9.4 State Stores + +**`useOrgStore`** (Zustand) -- `ui/src/auth/useOrganization.ts`: + +| Field | Type | Purpose | +|------------------|------------------|------------------------------------| +| `currentOrgId` | `string | null` | Logto org ID (for token scoping) | +| `currentTenantId`| `string | null` | DB UUID (for API calls) | +| `organizations` | `OrgInfo[]` | All orgs the user belongs to | +| `scopes` | `Set` | OAuth2 scopes from access token | + +**`useAuth()`** hook -- `ui/src/auth/useAuth.ts`: + +Combines `@logto/react` state (`isAuthenticated`, `isLoading`) with org store +state (`currentTenantId`). Provides `logout` and `signIn` callbacks. + +--- + +## 10. Configuration Reference + +### 10.1 cameleer-saas + +**Spring / Database:** + +| Variable | Default | Description | +|------------------------------|----------------------------------------------|----------------------------------| +| `SPRING_DATASOURCE_URL` | `jdbc:postgresql://postgres:5432/cameleer_saas` | PostgreSQL JDBC URL | +| `SPRING_DATASOURCE_USERNAME`| `cameleer` | PostgreSQL user | +| `SPRING_DATASOURCE_PASSWORD`| `cameleer_dev` | PostgreSQL password | + +**Logto / OIDC:** + +| Variable | Default | Description | +|---------------------------|------------|--------------------------------------------| +| `LOGTO_ENDPOINT` | (empty) | Logto internal URL (Docker-internal) | +| `LOGTO_PUBLIC_ENDPOINT` | (empty) | Logto public URL (browser-accessible) | +| `LOGTO_ISSUER_URI` | (empty) | OIDC issuer URI for JWT validation | +| `LOGTO_JWK_SET_URI` | (empty) | JWKS endpoint for JWT signature validation | +| `LOGTO_M2M_CLIENT_ID` | (empty) | M2M app client ID (from bootstrap) | +| `LOGTO_M2M_CLIENT_SECRET` | (empty) | M2M app client secret (from bootstrap) | +| `LOGTO_SPA_CLIENT_ID` | (empty) | SPA app client ID (fallback; bootstrap preferred) | + +**Runtime / Deployment:** + +| Variable | Default | Description | +|-----------------------------------|------------------------------------|----------------------------------| +| `CAMELEER3_SERVER_ENDPOINT` | `http://cameleer3-server:8081` | cameleer3-server internal URL | +| `CAMELEER_JAR_STORAGE_PATH` | `/data/jars` | JAR upload storage directory | +| `CAMELEER_RUNTIME_BASE_IMAGE` | `cameleer-runtime-base:latest` | Base Docker image for app builds | +| `CAMELEER_DOCKER_NETWORK` | `cameleer` | Docker network for containers | +| `CAMELEER_CONTAINER_MEMORY_LIMIT`| `512m` | Per-container memory limit | +| `CAMELEER_CONTAINER_CPU_SHARES` | `512` | Per-container CPU shares | +| `CLICKHOUSE_URL` | `jdbc:clickhouse://clickhouse:8123/cameleer` | ClickHouse JDBC URL | +| `CLICKHOUSE_ENABLED` | `true` | Enable ClickHouse integration | +| `CLICKHOUSE_USERNAME` | `default` | ClickHouse user | +| `CLICKHOUSE_PASSWORD` | (empty) | ClickHouse password | +| `DOMAIN` | `localhost` | Base domain for Traefik routing | + +### 10.2 cameleer3-server + +| Variable | Default | Description | +|------------------------------|----------------------------------------------|----------------------------------| +| `SPRING_DATASOURCE_URL` | `jdbc:postgresql://postgres:5432/cameleer3` | PostgreSQL JDBC URL | +| `SPRING_DATASOURCE_USERNAME`| `cameleer` | PostgreSQL user | +| `SPRING_DATASOURCE_PASSWORD`| `cameleer_dev` | PostgreSQL password | +| `CLICKHOUSE_URL` | `jdbc:clickhouse://clickhouse:8123/cameleer` | ClickHouse JDBC URL | +| `CAMELEER_AUTH_TOKEN` | `default-bootstrap-token` | Agent bootstrap token | +| `CAMELEER_JWT_SECRET` | `cameleer-dev-jwt-secret-...` | HMAC secret for internal JWTs | +| `CAMELEER_TENANT_ID` | `default` | Tenant slug for data isolation | +| `CAMELEER_OIDC_ISSUER_URI` | (empty) | Logto issuer for M2M token validation | +| `CAMELEER_OIDC_AUDIENCE` | (empty) | Expected JWT audience | + +### 10.3 logto + +| Variable | Default | Description | +|---------------------|--------------------------|---------------------------------| +| `LOGTO_PUBLIC_ENDPOINT` | `http://localhost:3001`| Public-facing Logto URL | +| `LOGTO_ADMIN_ENDPOINT` | `http://localhost:3002`| Admin console URL (not exposed) | + +### 10.4 postgres + +| Variable | Default | Description | +|---------------------|-------------------|---------------------------------| +| `POSTGRES_DB` | `cameleer_saas` | Default database name | +| `POSTGRES_USER` | `cameleer` | PostgreSQL superuser | +| `POSTGRES_PASSWORD` | `cameleer_dev` | PostgreSQL password | + +### 10.5 logto-bootstrap + +| Variable | Default | Description | +|----------------------|----------------------------|--------------------------------| +| `SAAS_ADMIN_USER` | `admin` | Platform admin username | +| `SAAS_ADMIN_PASS` | `admin` | Platform admin password | +| `TENANT_ADMIN_USER` | `camel` | Default tenant admin username | +| `TENANT_ADMIN_PASS` | `camel` | Default tenant admin password | +| `CAMELEER_AUTH_TOKEN`| `default-bootstrap-token` | Agent bootstrap token | + +### 10.6 Bootstrap Output + +The bootstrap script writes `/data/logto-bootstrap.json` containing: + +```json +{ + "spaClientId": "", + "m2mClientId": "", + "m2mClientSecret": "", + "tradAppId": "", + "tradAppSecret": "", + "apiResourceIndicator": "https://api.cameleer.local", + "organizationId": "", + "tenantName": "Example Tenant", + "tenantSlug": "default", + "bootstrapToken": "", + "platformAdminUser": "", + "tenantAdminUser": "", + "oidcIssuerUri": "http://logto:3001/oidc", + "oidcAudience": "https://api.cameleer.local" +} +``` + +This file is mounted read-only into cameleer-saas via the `bootstrapdata` +volume. `PublicConfigController` reads it to serve SPA client IDs and the API +resource indicator without requiring environment variable configuration. + +--- + +## Appendix: Key Source Files + +| File | Purpose | +|------|---------| +| `docker-compose.yml` | Service topology and configuration | +| `docker/logto-bootstrap.sh` | Idempotent Logto + DB bootstrap | +| `src/.../config/SecurityConfig.java` | Spring Security filter chain | +| `src/.../config/TenantResolutionFilter.java` | JWT org_id -> tenant resolution | +| `src/.../config/TenantContext.java` | ThreadLocal tenant ID holder | +| `src/.../config/MeController.java` | User identity + tenant endpoint | +| `src/.../config/PublicConfigController.java` | SPA configuration endpoint | +| `src/.../tenant/TenantController.java` | Tenant CRUD (platform:admin gated) | +| `src/.../environment/EnvironmentController.java` | Environment CRUD | +| `src/.../app/AppController.java` | App CRUD + JAR upload | +| `src/.../deployment/DeploymentService.java` | Async deployment orchestration | +| `src/.../deployment/DeploymentController.java` | Deploy/stop/restart endpoints | +| `src/.../apikey/ApiKeyService.java` | API key generation, rotation, revocation | +| `src/.../identity/LogtoManagementClient.java` | Logto Management API client | +| `src/.../audit/AuditService.java` | Audit log writer | +| `src/.../runtime/RuntimeConfig.java` | Container runtime configuration | +| `ui/src/main.tsx` | React app entry, Logto provider setup | +| `ui/src/router.tsx` | Client-side route definitions | +| `ui/src/auth/OrgResolver.tsx` | Org + scope resolution from JWT | +| `ui/src/auth/useOrganization.ts` | Zustand org/tenant store | +| `ui/src/auth/useAuth.ts` | Auth convenience hook | +| `ui/src/auth/ProtectedRoute.tsx` | Route guard component |