49 Commits

Author SHA1 Message Date
hsiegeln
1fbafbb16d feat: add vendor tenant metrics dashboard
All checks were successful
CI / build (push) Successful in 1m24s
CI / docker (push) Successful in 1m0s
Fleet overview page at /vendor/metrics showing per-tenant operational
metrics (agents, CPU, heap, HTTP requests, ingestion drops, uptime).
Queries each tenant's server via the new POST /api/v1/admin/server-metrics/query
REST API instead of direct ClickHouse access, supporting future per-tenant
CH instances.

Backend: TenantMetricsService fires 11 metric queries per tenant
concurrently over a 5-minute window, assembles into a summary snapshot.
ServerApiClient.queryServerMetrics() handles the M2M authenticated POST.

Frontend: VendorMetricsPage with KPI strip (fleet totals) and per-tenant
table with color-coded badges and heap usage bars. Auto-refreshes every 60s.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 14:02:57 +02:00
hsiegeln
6c1241ed89 docs(docker): replace obsolete 504 workaround note with the real wiring
Some checks failed
CI / build (push) Successful in 1m23s
CI / docker (push) Successful in 18s
SonarQube Analysis / sonarqube (push) Failing after 1m20s
Pre-fix the paragraph claimed every dynamically-created container MUST
carry `traefik.docker.network=cameleer-traefik` to avoid a 504, because
Traefik's Docker provider pointed at `network: cameleer` (a literal
name that never matched any real network). After the one-line static
config fix (df64573), Traefik's provider targets `cameleer-traefik`
directly — the network every managed container already joins — so the
per-container label is just defense-in-depth, not required.

Rewritten to describe current behaviour and keep a short note about the
pre-fix 504 for operators who roll back to an old image.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:22:32 +02:00
hsiegeln
df64573bfb fix(traefik): point docker provider network at cameleer-traefik
All checks were successful
CI / build (push) Successful in 1m22s
CI / docker (push) Successful in 16s
The static config set `provider.docker.network: cameleer`, but no network
by that literal name exists. The `cameleer` network defined in the
compose file gets namespaced by compose to `cameleer_cameleer`, and
managed app containers created at runtime only ever attach to
`cameleer-traefik` (per `DockerNetworkManager.TRAEFIK_NETWORK`).

Symptom: when the Docker provider's preferred network doesn't match any
network on a container, Traefik picks an arbitrary container IP and may
route to one on a bridge Traefik itself isn't attached to — requests
hang until Traefik's upstream timeout fires (504 Gateway Timeout).

Fix is one line: match the network that `cameleer-server` actually
attaches its managed containers to.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:15:22 +02:00
hsiegeln
4526d97bda fix: generate CAMELEER_SERVER_SECURITY_JWTSECRET in installer and wire into containers
All checks were successful
CI / build (push) Successful in 1m16s
CI / docker (push) Successful in 59s
The server now requires a non-empty JWT secret. The installer (bash + ps1)
generates a random value for both SaaS and standalone modes, and the compose
templates map it into the respective containers. Also fixes container names
in generated INSTALL.md docs to use the cameleer- prefix consistently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 09:30:11 +02:00
hsiegeln
132143c083 refactor: decompose CLAUDE.md into directory-scoped files
Some checks failed
CI / build (push) Successful in 1m59s
CI / docker (push) Successful in 1m24s
SonarQube Analysis / sonarqube (push) Failing after 2m4s
Root CLAUDE.md reduced from 475 to 175 lines (75 excl. GitNexus).
Detailed context now loads automatically only when editing code in
the relevant directory:

- provisioning/CLAUDE.md — env vars, provisioning flow, lifecycle
- config/CLAUDE.md — auth, scopes, JWT, OIDC role extraction
- docker/CLAUDE.md — routing, networks, bootstrap, deployment pipeline
- installer/CLAUDE.md — deployment modes, compose templates, env naming
- ui/CLAUDE.md — frontend files, sign-in UI

No information lost — everything moved, nothing deleted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:30:21 +02:00
hsiegeln
b824942408 docs: fix scope breakdown and add missing InfrastructurePage
All checks were successful
CI / build (push) Successful in 2m12s
CI / docker (push) Successful in 19s
- OAuth2 scopes: 1 platform + 9 tenant + 3 server (not "10 platform")
- Add InfrastructurePage.tsx to vendor pages list

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:21:28 +02:00
hsiegeln
31e8dd05f0 docs: update CLAUDE.md for runtime base image, env var accuracy
All checks were successful
CI / build (push) Successful in 1m21s
CI / docker (push) Successful in 3m2s
- Fix OIDC env var names (OIDC_ISSUERURI not OIDCISSUERURI)
- Fix CAMELEER_SERVER_TENANT_ID value (slug, not UUID)
- Add missing env vars (ClickHouse, JWT secret, license token, base image)
- Complete provisioning properties table (was 6/16, now all listed)
- Add semantic note: CAMELEER_SAAS_PROVISIONING_* = "forwarded to tenant"
- Update runtime-base description (log appender JAR, entrypoint override,
  runtime type detection, PropertiesLauncher version handling)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 09:16:55 +02:00
hsiegeln
eba9f560ac fix: name JAR volume explicitly to match JARDOCKERVOLUME env var
Some checks failed
CI / build (push) Successful in 1m17s
CI / docker (push) Successful in 19s
SonarQube Analysis / sonarqube (push) Failing after 1m23s
The compose volume `jars` gets created as `<project>_jars` by Docker
Compose, but JARDOCKERVOLUME tells the server to mount `cameleer-jars`
on deployed app containers. These are different Docker volumes, so
the app JAR was never visible inside the app container — causing
ClassNotFoundException on startup.

Fix: add `name: cameleer-jars` to the volume definition so both the
server and deployed app containers share the same named volume.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 00:03:48 +02:00
hsiegeln
3c2bf4a9b1 fix: pass self-reference in VendorTenantServiceTest for async proxy
All checks were successful
CI / build (push) Successful in 1m17s
CI / docker (push) Successful in 44s
The @Lazy self-proxy pattern requires a non-null reference in tests.
Construct the instance then re-create with itself as the self param.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:33:07 +02:00
hsiegeln
97b2235914 fix: update tests for ProvisioningProperties runtimeBaseImage field
Some checks failed
CI / build (push) Failing after 1m22s
CI / docker (push) Has been skipped
Add missing runtimeBaseImage arg to ProvisioningProperties constructor
calls in tests. Also add missing self-proxy arg to VendorTenantService
constructor (pre-existing from async provisioning commit).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:27:53 +02:00
hsiegeln
338db5dcda fix: forward runtime base image to provisioned tenant servers
Some checks failed
CI / build (push) Failing after 59s
CI / docker (push) Has been skipped
CAMELEER_SERVER_RUNTIME_BASEIMAGE was never set on provisioned
per-tenant server containers, causing them to fall back to the
server's hardcoded default. Added CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE
as a configurable property that gets forwarded during provisioning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:20:46 +02:00
hsiegeln
fd50a147a2 fix: make tenant provisioning truly async via self-proxy
Some checks failed
CI / build (push) Failing after 41s
CI / docker (push) Has been skipped
@Async on provisionAsync() was bypassed because all call sites were
internal (this.provisionAsync), skipping the Spring proxy. Inject self
via @Lazy to route through the proxy so provisioning runs in a
background thread and the API returns immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:32:05 +02:00
hsiegeln
0dd52624b7 fix: use semicolon as COMPOSE_FILE separator on Windows
All checks were successful
CI / build (push) Successful in 1m59s
CI / docker (push) Successful in 46s
Windows Docker Compose uses ; not : as the path separator in COMPOSE_FILE.
The colon was being interpreted as part of the filename, causing CreateFile errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:11:34 +02:00
hsiegeln
1ce0ea411d chore: update design-system to 0.1.54
Some checks failed
CI / build (push) Successful in 1m25s
CI / docker (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:08:17 +02:00
hsiegeln
81be25198c chore: update design-system to 0.1.53
All checks were successful
CI / build (push) Successful in 1m16s
CI / docker (push) Successful in 1m32s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:56:14 +02:00
hsiegeln
dc4ea33c9b feat: externalize docker-compose templates from installer scripts
All checks were successful
CI / build (push) Successful in 1m16s
CI / docker (push) Successful in 20s
Replace inline heredoc compose generation with static template files.
Templates are copied to the install dir and composed via COMPOSE_FILE in .env.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:53:26 +02:00
hsiegeln
186f7639ad docs: update CLAUDE.md with template-based installer architecture
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:11:04 +02:00
hsiegeln
6c7895b0d6 chore(installer): remove generated install output, add to gitignore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:09:30 +02:00
hsiegeln
6170f61eeb refactor(installer): replace ps1 compose generation with template copying
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:08:34 +02:00
hsiegeln
2ed527ac74 refactor(installer): replace sh compose generation with template copying
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 21:03:01 +02:00
hsiegeln
cb1f6b8ccf feat(installer): add .env.example with documented variables
Reference .env file documenting all configuration variables across both
deployment modes, with section headers for compose assembly, public access,
credentials, TLS, Docker, provisioning, and monitoring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:59:15 +02:00
hsiegeln
758585cc9a feat(installer): add TLS and monitoring overlay templates
Optional compose overlays: TLS overlay mounts user-supplied certs into
traefik, monitoring overlay replaces the noop bridge with an external
Docker network for Prometheus scraping.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:59:10 +02:00
hsiegeln
141b44048c feat(installer): add standalone docker-compose and traefik templates
Standalone mode: server + server-ui services with postgres image override
to stock postgres:16-alpine. Includes traefik-dynamic.yml for default TLS
certificate store configuration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:59:05 +02:00
hsiegeln
3c343f9441 feat(installer): add SaaS docker-compose template
Logto identity provider and cameleer-saas management plane services.
Includes Traefik labels, CORS config, bootstrap healthcheck, and all
provisioning env vars parameterized from .env.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:59:00 +02:00
hsiegeln
bdb24f8de6 feat(installer): add infra base docker-compose template
Shared infrastructure base (traefik, postgres, clickhouse) always loaded
regardless of deployment mode. Uses parameterized images, fail-if-unset
password variables, and a noop monitoring network bridge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:58:54 +02:00
hsiegeln
933b56f68f docs: add implementation plan for externalizing compose templates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:54:31 +02:00
hsiegeln
19c463051a docs: add design spec for externalizing docker compose templates
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:47:14 +02:00
hsiegeln
41052d01e8 fix: replace admin password fallback defaults with fail-if-unset
All checks were successful
CI / build (push) Successful in 1m15s
CI / docker (push) Successful in 16s
Docker compose templates defaulted to admin/admin when .env was missing.
Now uses :? to fail with a clear error instead of silently using weak creds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:17:46 +02:00
hsiegeln
99e75b0a4e fix: update sign-in UI to design-system 0.1.51
All checks were successful
CI / build (push) Successful in 1m15s
CI / docker (push) Successful in 1m31s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:33:00 +02:00
hsiegeln
eb6897bf10 chore: update design-system to 0.1.51 (renamed assets)
Some checks failed
CI / build (push) Failing after 1m9s
CI / docker (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 16:30:50 +02:00
hsiegeln
63c194dab7 chore: rename cameleer3 to cameleer
Some checks failed
CI / build (push) Failing after 18s
CI / docker (push) Has been skipped
Rename Java packages from net.siegeln.cameleer3 to net.siegeln.cameleer,
update all references in workflows, Docker configs, docs, and bootstrap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:28:44 +02:00
hsiegeln
44a0e413e9 fix: include cameleer3-log-appender.jar in runtime base image
All checks were successful
CI / build (push) Successful in 1m20s
CI / docker (push) Successful in 13s
The log appender JAR was missing from the cameleer-runtime-base Docker
image, causing agent log forwarding to silently fail with "No supported
logging framework found, log forwarding disabled". This meant only
container stdout logs (source=container) were captured — no application
or agent logs reached ClickHouse.

CI now downloads the appender JAR from the Maven registry alongside the
agent JAR, and the Dockerfile COPYs it to /app/cameleer3-log-appender.jar
where the server's Docker entrypoint expects it (-Dloader.path for
Spring Boot, -cp for plain Java).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:11:04 +02:00
hsiegeln
15306dddc0 fix: force-pull images on install and fix provisioning test assertions
All checks were successful
CI / build (push) Successful in 1m11s
CI / docker (push) Successful in 47s
Installers now use `--pull always --force-recreate` on `docker compose up`
to ensure fresh images are used on every install/reinstall, preventing
stale containers from missing schema changes like db_password.

Fix VendorTenantServiceTest to expect two repository saves in provisioning
tests (one for dbPassword, one for final status).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:50:40 +02:00
hsiegeln
6eb848f353 fix: add missing TenantDatabaseService mock to VendorTenantServiceTest
Some checks failed
CI / build (push) Failing after 58s
CI / docker (push) Has been skipped
Constructor gained an 11th parameter (TenantDatabaseService) but the
test was not updated, breaking CI compilation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 08:32:14 +02:00
hsiegeln
d53afe43cc docs: update CLAUDE.md for per-tenant PG isolation and consolidated migrations
Some checks failed
CI / build (push) Failing after 42s
CI / docker (push) Has been skipped
SonarQube Analysis / sonarqube (push) Failing after 33s
- TenantDatabaseService added to key classes
- TenantDataCleanupService now ClickHouse-only
- Per-tenant JDBC URL with currentSchema/ApplicationName in env vars table
- Provisioning flow updated with DB creation step
- Delete flow updated with schema+user drop
- Database migrations section reflects consolidated V001 baseline

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:26:36 +02:00
hsiegeln
24a443ef30 refactor: consolidate Flyway migrations into single V001 baseline
Some checks failed
CI / build (push) Failing after 51s
CI / docker (push) Has been skipped
Replace 14 incremental migrations (V001-V015) with a single V001__init.sql
representing the final schema. Tables that were created and later dropped
(environments, api_keys, apps, deployments) are excluded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:24:25 +02:00
hsiegeln
d7eb700860 refactor: move PG cleanup to TenantDatabaseService, keep only ClickHouse
TenantDataCleanupService now handles only ClickHouse GDPR erasure;
the dropPostgresSchema private method is removed and the public method
renamed cleanupClickHouse(). VendorTenantService updated accordingly
with the TODO comment removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 00:17:00 +02:00
hsiegeln
c1458e4995 feat: create per-tenant PG database during provisioning, drop on delete
Inject TenantDatabaseService; call createTenantDatabase() at the start
of provisionAsync() (stores generated password on TenantEntity), and
dropTenantDatabase() in delete() before GDPR data erasure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 00:16:06 +02:00
hsiegeln
b79a7fe405 feat: construct per-tenant JDBC URL with currentSchema and ApplicationName
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 00:14:35 +02:00
hsiegeln
6d6c1f3562 feat: add TenantDatabaseService for per-tenant PG user+schema
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 00:13:34 +02:00
hsiegeln
0e3f383cf4 feat: add dbPassword to TenantProvisionRequest 2026-04-15 00:13:27 +02:00
hsiegeln
cd6dd1e5af feat: add dbPassword field to TenantEntity 2026-04-15 00:13:12 +02:00
hsiegeln
dfa2a6bfa2 feat: add db_password column to tenants table (V015) 2026-04-15 00:13:11 +02:00
hsiegeln
a7196ff4c1 docs: per-tenant PostgreSQL isolation implementation plan
8-task plan covering migration, entity change, TenantDatabaseService,
provisioner JDBC URL construction, VendorTenantService integration,
and TenantDataCleanupService refactor.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:11:34 +02:00
hsiegeln
17c6723f7e docs: per-tenant PostgreSQL isolation design spec
Per-tenant PG users and schemas for DB-level data isolation.
Each tenant server gets its own credentials and currentSchema/ApplicationName
JDBC parameters, aligned with server team's commit 7a63135.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:08:35 +02:00
hsiegeln
91e93696ed fix: improve Infrastructure page readability
All checks were successful
CI / build (push) Successful in 1m11s
CI / docker (push) Successful in 54s
Use Card and KpiStrip design system components, add database icons to
section headers, right-align numeric columns, replace text toggles with
chevron icons, and constrain max width to prevent ultra-wide stretching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:26:43 +02:00
hsiegeln
57e41e407c fix: remove "Open Server Dashboard" link from tenant sidebar
All checks were successful
CI / build (push) Successful in 1m11s
CI / docker (push) Successful in 55s
The server dashboard link in the sidebar footer is premature — tenant
servers may not be provisioned yet and the link target depends on org
context that isn't always available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:16:39 +02:00
hsiegeln
bc46af5cea fix: use configured credentials for tenant schema cleanup
All checks were successful
CI / build (push) Successful in 1m9s
CI / docker (push) Successful in 39s
Same hardcoded dev credentials bug as InfrastructureService —
TenantDataCleanupService.dropPostgresSchema() used "cameleer"/"cameleer_dev"
instead of the provisioning properties, causing schema DROP to fail on
production installs during tenant deletion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:11:16 +02:00
hsiegeln
03fb414981 fix: use configured credentials for infrastructure PostgreSQL queries
All checks were successful
CI / build (push) Successful in 1m8s
CI / docker (push) Successful in 43s
pgConnection() had hardcoded dev credentials ("cameleer"/"cameleer_dev")
instead of using the provisioning properties, causing "password
authentication failed" on production installs where the password is
generated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:01:00 +02:00
159 changed files with 10260 additions and 2233 deletions

View File

@@ -111,12 +111,17 @@ jobs:
- name: Build and push runtime base image
run: |
AGENT_VERSION=$(curl -sf "https://gitea.siegeln.net/api/packages/cameleer/maven/com/cameleer3/cameleer3-agent/1.0-SNAPSHOT/maven-metadata.xml" \
AGENT_VERSION=$(curl -sf "https://gitea.siegeln.net/api/packages/cameleer/maven/com/cameleer/cameleer-agent/1.0-SNAPSHOT/maven-metadata.xml" \
| sed -n 's/.*<value>\([^<]*\)<\/value>.*/\1/p' | tail -1)
echo "Agent version: $AGENT_VERSION"
curl -sf -o docker/runtime-base/agent.jar \
"https://gitea.siegeln.net/api/packages/cameleer/maven/com/cameleer3/cameleer3-agent/1.0-SNAPSHOT/cameleer3-agent-${AGENT_VERSION}-shaded.jar"
ls -la docker/runtime-base/agent.jar
"https://gitea.siegeln.net/api/packages/cameleer/maven/com/cameleer/cameleer-agent/1.0-SNAPSHOT/cameleer-agent-${AGENT_VERSION}-shaded.jar"
APPENDER_VERSION=$(curl -sf "https://gitea.siegeln.net/api/packages/cameleer/maven/com/cameleer/cameleer-log-appender/1.0-SNAPSHOT/maven-metadata.xml" \
| sed -n 's/.*<value>\([^<]*\)<\/value>.*/\1/p' | tail -1)
echo "Log appender version: $APPENDER_VERSION"
curl -sf -o docker/runtime-base/cameleer-log-appender.jar \
"https://gitea.siegeln.net/api/packages/cameleer/maven/com/cameleer/cameleer-log-appender/1.0-SNAPSHOT/cameleer-log-appender-${APPENDER_VERSION}.jar"
ls -la docker/runtime-base/agent.jar docker/runtime-base/cameleer-log-appender.jar
TAGS="-t gitea.siegeln.net/cameleer/cameleer-runtime-base:${{ github.sha }}"
for TAG in $IMAGE_TAGS; do
TAGS="$TAGS -t gitea.siegeln.net/cameleer/cameleer-runtime-base:$TAG"

10
.gitignore vendored
View File

@@ -22,7 +22,15 @@ Thumbs.db
# Worktrees
.worktrees/
# Claude
.claude/
.superpowers/
.playwright-mcp/
.gitnexus
# Installer output (generated by install.sh / install.ps1)
installer/cameleer/
# Generated by postinstall from @cameleer/design-system
ui/public/favicon.svg
docker/runtime-base/agent.jar
.gitnexus

View File

@@ -1,7 +1,7 @@
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
This project is indexed by GitNexus as **cameleer-saas** (2676 symbols, 5768 relationships, 224 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **cameleer-saas** (2816 symbols, 5989 relationships, 238 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

343
CLAUDE.md
View File

@@ -4,338 +4,55 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project
Cameleer SaaS — **vendor management plane** for the Cameleer observability stack. Two personas: **vendor** (platform:admin) manages the platform and provisions tenants; **tenant admin** (tenant:manage) manages their observability instance. The vendor creates tenants, which provisions per-tenant cameleer3-server + UI instances via Docker API. No example tenant — clean slate bootstrap, vendor creates everything.
Cameleer SaaS — **vendor management plane** for the Cameleer observability stack. Two personas: **vendor** (platform:admin) manages the platform and provisions tenants; **tenant admin** (tenant:manage) manages their observability instance. The vendor creates tenants, which provisions per-tenant cameleer-server + UI instances via Docker API. No example tenant — clean slate bootstrap, vendor creates everything.
## 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** (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.
- **cameleer-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.
Agent-server protocol is defined in `cameleer/cameleer-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
## Key Packages
### 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<UUID> 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)
| Package | Purpose | Key classes |
|---------|---------|-------------|
| `config/` | Security, tenant isolation, web config | `SecurityConfig`, `TenantIsolationInterceptor`, `TenantContext`, `PublicConfigController`, `MeController` |
| `tenant/` | Tenant data model | `TenantEntity` (JPA: id, name, slug, tier, status, logto_org_id, db_password) |
| `vendor/` | Vendor console (platform:admin) | `VendorTenantService`, `VendorTenantController`, `InfrastructureService` |
| `portal/` | Tenant admin portal (org-scoped) | `TenantPortalService`, `TenantPortalController` |
| `provisioning/` | Pluggable tenant provisioning | `DockerTenantProvisioner`, `TenantDatabaseService`, `TenantDataCleanupService` |
| `certificate/` | TLS certificate lifecycle | `CertificateService`, `CertificateController`, `TenantCaCertService` |
| `license/` | License management | `LicenseService`, `LicenseController` |
| `identity/` | Logto & server integration | `LogtoManagementClient`, `ServerApiClient` |
| `audit/` | Audit logging | `AuditService` |
**tenant/** — Tenant data model
- `TenantEntity.java` — JPA entity (id, name, slug, tier, status, logto_org_id, stripe IDs, settings JSONB)
### Frontend
**vendor/**Vendor console (platform:admin only)
- `VendorTenantService.java` — orchestrates tenant creation (sync: DB + Logto + license, async: Docker provisioning + config push), suspend/activate, delete, restart server, upgrade server (force-pull + re-provision), license renewal
- `VendorTenantController.java` — REST at `/api/vendor/tenants` (platform:admin required). List endpoint returns `VendorTenantSummary` with fleet health data (agentCount, environmentCount, agentLimit) fetched in parallel via `CompletableFuture`.
- `InfrastructureService.java` — raw JDBC queries against shared PostgreSQL and ClickHouse for per-tenant infrastructure monitoring (schema sizes, table stats, row counts, disk usage)
- `InfrastructureController.java` — REST at `/api/vendor/infrastructure` (platform:admin required). PostgreSQL and ClickHouse overview with per-tenant breakdown.
**portal/** — Tenant admin portal (org-scoped)
- `TenantPortalService.java` — customer-facing: dashboard (health + agent/env counts from server via M2M), license, SSO connectors, team, settings (public endpoint URL), server restart/upgrade, password management (own + team + server admin)
- `TenantPortalController.java` — REST at `/api/tenant/*` (org-scoped, includes CA cert management at `/api/tenant/ca`, password endpoints at `/api/tenant/password` and `/api/tenant/server/admin-password`)
**provisioning/** — Pluggable tenant provisioning
- `TenantProvisioner.java` — pluggable interface (like server's RuntimeOrchestrator)
- `DockerTenantProvisioner.java` — Docker implementation, creates per-tenant server + UI containers. `upgrade(slug)` force-pulls latest images and removes server+UI containers (preserves app containers, volumes, networks) for re-provisioning. `remove(slug)` does full cleanup: label-based container removal, env networks, tenant network, JAR volume.
- `TenantDataCleanupService.java` — GDPR data erasure on tenant delete: drops PostgreSQL `tenant_{slug}` schema, deletes ClickHouse data across all tables with `tenant_id` column
- `TenantProvisionerAutoConfig.java` — auto-detects Docker socket
- `DockerCertificateManager.java` — file-based cert management with atomic `.wip` swap (Docker volume)
- `DisabledCertificateManager.java` — no-op when certs dir unavailable
- `CertificateManagerAutoConfig.java` — auto-detects `/certs` directory
**certificate/** — TLS certificate lifecycle management
- `CertificateManager.java` — provider interface (Docker now, K8s later)
- `CertificateService.java` — orchestrates stage/activate/restore/discard, DB metadata, tenant CA staleness
- `CertificateController.java` — REST at `/api/vendor/certificates` (platform:admin required)
- `CertificateEntity.java` — JPA entity (status: ACTIVE/STAGED/ARCHIVED, subject, fingerprint, etc.)
- `CertificateStartupListener.java` — seeds DB from filesystem on boot (for bootstrap-generated certs)
- `TenantCaCertEntity.java` — JPA entity for per-tenant CA certs (PEM stored in DB, multiple per tenant)
- `TenantCaCertRepository.java` — queries by tenant, status, all active across tenants
- `TenantCaCertService.java` — stage/activate/delete tenant CAs, rebuilds aggregated `ca.pem` on changes
**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, get user, SSO connectors, JIT provisioning, password updates via `PATCH /api/users/{id}/password`)
- `ServerApiClient.java` — M2M client for cameleer3-server API (Logto M2M token, `X-Cameleer-Protocol-Version: 1` header). Health checks, license/OIDC push, agent count, environment count, server admin password reset per tenant server.
**audit/** — Audit logging
- `AuditEntity.java` — JPA entity (actor_id, actor_email, tenant_id, action, resource, status)
- `AuditService.java` — log audit events (TENANT_CREATE, TENANT_UPDATE, etc.); auto-resolves actor name from Logto when actorEmail is null (cached in-memory)
### React Frontend (`ui/src/`)
- `main.tsx` — React 19 root
- `router.tsx``/vendor/*` + `/tenant/*` with `RequireScope` guards and `LandingRedirect` that waits for scopes
- `Layout.tsx` — persona-aware sidebar: vendor sees expandable "Vendor" section (Tenants, Audit Log, Certificates, Infrastructure, Identity/Logto), tenant admin sees Dashboard/License/SSO/Team/Audit/Settings
- `OrgResolver.tsx` — merges global + org-scoped token scopes (vendor's platform:admin is global)
- `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)
- **Vendor pages**: `VendorTenantsPage.tsx`, `CreateTenantPage.tsx`, `TenantDetailPage.tsx`, `VendorAuditPage.tsx`, `CertificatesPage.tsx`
- **Tenant pages**: `TenantDashboardPage.tsx` (restart + upgrade server), `TenantLicensePage.tsx`, `SsoPage.tsx`, `TeamPage.tsx` (reset member passwords), `TenantAuditPage.tsx`, `SettingsPage.tsx` (change own password, reset server admin password)
### 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)
- **`ui/src/`** — React 19 SPA at `/platform/*` (vendor + tenant admin pages)
- **`ui/sign-in/`** — Custom Logto sign-in UI (built into `cameleer-logto` Docker image)
## Architecture Context
The SaaS platform is a **vendor management plane**. It does not proxy requests to servers — instead it provisions dedicated per-tenant cameleer3-server instances via Docker API. Each tenant gets isolated server + UI containers with their own database schemas, networks, and Traefik routing.
The SaaS platform is a **vendor management plane**. It does not proxy requests to servers — instead it provisions dedicated per-tenant cameleer-server instances via Docker API. Each tenant gets isolated server + UI containers with their own database schemas, networks, and Traefik routing.
### Routing (single-domain, path-based via Traefik)
All services on one hostname. Infrastructure containers (Traefik, Logto) use `PUBLIC_HOST` + `PUBLIC_PROTOCOL` env vars directly. The SaaS app reads these via `CAMELEER_SAAS_PROVISIONING_PUBLICHOST` / `CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL` (Spring Boot properties `cameleer.saas.provisioning.publichost` / `cameleer.saas.provisioning.publicprotocol`).
| Path | Target | Notes |
|------|--------|-------|
| `/platform/*` | cameleer-saas:8080 | SPA + API (`server.servlet.context-path: /platform`) |
| `/platform/vendor/*` | (SPA routes) | Vendor console (platform:admin) |
| `/platform/tenant/*` | (SPA routes) | Tenant admin portal (org-scoped) |
| `/t/{slug}/*` | per-tenant server-ui | Provisioned tenant UI containers (Traefik labels) |
| `/` | 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: `traefik-certs` init container generates self-signed cert (dev) or copies user-supplied cert via `CERT_FILE`/`KEY_FILE`/`CA_FILE` env vars. Default cert configured in `docker/traefik-dynamic.yml` (NOT static `traefik.yml` — Traefik v3 ignores `tls.stores.default` in static config). Runtime cert replacement via vendor UI (stage/activate/restore). ACME for production (future). Server containers import `/certs/ca.pem` into JVM truststore at startup via `docker-entrypoint.sh` for OIDC trust.
- Root `/` -> `/platform/` redirect via Traefik file provider (`docker/traefik-dynamic.yml`)
- LoginPage auto-redirects to Logto OIDC (no intermediate button)
- Per-tenant server containers get Traefik labels for `/t/{slug}/*` routing at provisioning time
### Docker Networks
Compose-defined networks:
| Network | Name on Host | Purpose |
|---------|-------------|---------|
| `cameleer` | `cameleer-saas_cameleer` | Compose default — shared services (DB, Logto, SaaS) |
| `cameleer-traefik` | `cameleer-traefik` (fixed `name:`) | Traefik + provisioned tenant containers |
Per-tenant networks (created dynamically by `DockerTenantProvisioner`):
| Network | Name Pattern | Purpose |
|---------|-------------|---------|
| Tenant network | `cameleer-tenant-{slug}` | Internal bridge, no internet — isolates tenant server + apps |
| Environment network | `cameleer-env-{tenantId}-{envSlug}` | Tenant-scoped (includes tenantId to prevent slug collision across tenants) |
Server containers join three networks: tenant network (primary), shared services network (`cameleer`), and traefik network. Apps deployed by the server use the tenant network as primary.
**IMPORTANT:** Dynamically-created containers MUST have `traefik.docker.network=cameleer-traefik` label. Traefik's Docker provider defaults to `network: cameleer` (compose-internal name) for IP resolution, which doesn't match dynamically-created containers connected via Docker API using the host network name (`cameleer-saas_cameleer`). Without this label, Traefik returns 504 Gateway Timeout for `/t/{slug}/api/*` paths.
### 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)
- Org roles: `owner` -> `server:admin` + `tenant:manage`, `operator` -> `server:operator`, `viewer` -> `server:viewer`
- `saas-vendor` global role created by bootstrap Phase 12 and always assigned to the admin user — 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
### Auth routing by persona
| Persona | Logto role | Key scope | Landing route |
|---------|-----------|-----------|---------------|
| SaaS admin | `saas-vendor` (global) | `platform:admin` | `/vendor/tenants` |
| Tenant admin | org `owner` | `tenant:manage` | `/tenant` (dashboard) |
| Regular user (operator/viewer) | org member | `server:operator` or `server:viewer` | Redirected to server dashboard directly |
- `LandingRedirect` component waits for scopes to load, then routes to the correct persona landing page
- `RequireScope` guard on route groups enforces scope requirements
- SSO bridge: Logto session carries over to provisioned server's OIDC flow (Traditional Web App per tenant)
### Per-tenant server env vars (set by DockerTenantProvisioner)
These env vars are injected into provisioned per-tenant server containers:
| Env var | Value | Purpose |
|---------|-------|---------|
| `CAMELEER_SERVER_SECURITY_OIDCISSUERURI` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/oidc` | Token issuer claim validation |
| `CAMELEER_SERVER_SECURITY_OIDCJWKSETURI` | `http://cameleer-logto:3001/oidc/jwks` | Docker-internal JWK fetch |
| `CAMELEER_SERVER_SECURITY_OIDCTLSSKIPVERIFY` | `true` (conditional) | Skip cert verify for OIDC discovery; only set when no `/certs/ca.pem` exists. When ca.pem exists, the server's `docker-entrypoint.sh` imports it into the JVM truststore instead. |
| `CAMELEER_SERVER_SECURITY_OIDCAUDIENCE` | `https://api.cameleer.local` | JWT audience validation for OIDC tokens |
| `CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` | Allow browser requests through Traefik |
| `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` | (generated) | Bootstrap auth token for M2M communication |
| `CAMELEER_SERVER_RUNTIME_ENABLED` | `true` | Enable Docker orchestration |
| `CAMELEER_SERVER_RUNTIME_SERVERURL` | `http://cameleer3-server-{slug}:8081` | Per-tenant server URL (DNS alias on tenant network) |
| `CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN` | `${PUBLIC_HOST}` | Domain for Traefik routing labels |
| `CAMELEER_SERVER_RUNTIME_ROUTINGMODE` | `path` | `path` or `subdomain` routing |
| `CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH` | `/data/jars` | Directory for uploaded JARs |
| `CAMELEER_SERVER_RUNTIME_DOCKERNETWORK` | `cameleer-tenant-{slug}` | Primary network for deployed app containers |
| `CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME` | `cameleer-jars-{slug}` | Docker volume name for JAR sharing between server and deployed containers |
| `CAMELEER_SERVER_TENANT_ID` | (tenant UUID) | Tenant identifier for data isolation |
| `CAMELEER_SERVER_SECURITY_INFRASTRUCTUREENDPOINTS` | `false` | Hides Database/ClickHouse admin from tenant admins |
| `BASE_PATH` (server-ui) | `/t/{slug}` | React Router basename + `<base>` tag |
| `CAMELEER_API_URL` (server-ui) | `http://cameleer-server-{slug}:8081` | Nginx upstream proxy target (NOT `API_URL` — image uses `${CAMELEER_API_URL}`) |
### Per-tenant volume mounts (set by DockerTenantProvisioner)
| Mount | Container path | Purpose |
|-------|---------------|---------|
| `/var/run/docker.sock` | `/var/run/docker.sock` | Docker socket for app deployment orchestration |
| `cameleer-jars-{slug}` (volume, via `CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME`) | `/data/jars` | Shared JAR storage — server writes, deployed app containers read |
| `cameleer-saas_certs` (volume, ro) | `/certs` | Platform TLS certs + CA bundle for OIDC trust |
### SaaS app configuration (env vars for cameleer-saas itself)
SaaS properties use the `cameleer.saas.*` prefix (env vars: `CAMELEER_SAAS_*`). Two groups:
**Identity** (`cameleer.saas.identity.*` / `CAMELEER_SAAS_IDENTITY_*`):
- Logto endpoint, M2M credentials, bootstrap file path — used by `LogtoConfig.java`
**Provisioning** (`cameleer.saas.provisioning.*` / `CAMELEER_SAAS_PROVISIONING_*`):
| Env var | Spring property | Purpose |
|---------|----------------|---------|
| `CAMELEER_SAAS_PROVISIONING_SERVERIMAGE` | `cameleer.saas.provisioning.serverimage` | Docker image for per-tenant server containers |
| `CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE` | `cameleer.saas.provisioning.serveruiimage` | Docker image for per-tenant UI containers |
| `CAMELEER_SAAS_PROVISIONING_NETWORKNAME` | `cameleer.saas.provisioning.networkname` | Shared services Docker network (compose default) |
| `CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK` | `cameleer.saas.provisioning.traefiknetwork` | Traefik Docker network for routing |
| `CAMELEER_SAAS_PROVISIONING_PUBLICHOST` | `cameleer.saas.provisioning.publichost` | Public hostname (same value as infrastructure `PUBLIC_HOST`) |
| `CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL` | `cameleer.saas.provisioning.publicprotocol` | Public protocol (same value as infrastructure `PUBLIC_PROTOCOL`) |
**Note:** `PUBLIC_HOST` and `PUBLIC_PROTOCOL` remain as infrastructure env vars for Traefik and Logto containers. The SaaS app reads its own copies via the `CAMELEER_SAAS_PROVISIONING_*` prefix. `LOGTO_ENDPOINT` and `LOGTO_DB_PASSWORD` are infrastructure env vars for the Logto service and are unchanged.
### 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"]`.
**CRITICAL:** `additionalScopes` MUST include `urn:logto:scope:organizations` and `urn:logto:scope:organization_roles` — without these, Logto doesn't populate `context.user.organizationRoles` in the Custom JWT script, so the `roles` claim is empty and all users get `defaultRoles` (VIEWER). The server's `OidcAuthController.applyClaimMappings()` uses OIDC token roles (from Custom JWT) as fallback when no DB claim mapping rules exist: claim mapping rules > OIDC token roles > defaultRoles.
### 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-tenant-{slug}` (primary, isolation) + `cameleer-traefik` (routing) + `cameleer-env-{tenantId}-{envSlug}` (environment isolation)
### Bootstrap (`docker/logto-bootstrap.sh`)
Idempotent script run inside the Logto container entrypoint. **Clean slate** — no example tenant, no viewer user, no server configuration. Phases:
1. Wait for Logto health (no server to wait for — servers are provisioned per-tenant)
2. Get Management API token (reads `m-default` secret from DB)
3. Create Logto apps (SPA, Traditional Web App 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 admin user (SaaS admin with Logto console access)
7b. Configure Logto Custom JWT for access tokens (maps org roles -> `roles` claim: owner->server:admin, operator->server:operator, viewer->server:viewer; saas-vendor global role -> server:admin)
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`
12. Create `saas-vendor` global role with all API scopes and assign to admin user (always runs — admin IS the platform admin).
The multi-tenant compose stack is: Traefik + PostgreSQL + ClickHouse + Logto (with bootstrap entrypoint) + cameleer-saas. No `cameleer3-server` or `cameleer3-server-ui` in compose — those are provisioned per-tenant by `DockerTenantProvisioner`.
### Deployment Modes (installer)
The installer (`installer/install.sh`) supports two deployment modes:
| | Multi-tenant SaaS (`DEPLOYMENT_MODE=saas`) | Standalone (`DEPLOYMENT_MODE=standalone`) |
|---|---|---|
| **Containers** | traefik, postgres, clickhouse, logto, cameleer-saas | traefik, postgres, clickhouse, server, server-ui |
| **Auth** | Logto OIDC (SaaS admin + tenant users) | Local auth (built-in admin, no identity provider) |
| **Tenant management** | SaaS admin creates/manages tenants via UI | Single server instance, no fleet management |
| **PostgreSQL** | `cameleer-postgres` image (multi-DB init) | Stock `postgres:16-alpine` (server creates schema via Flyway) |
| **Use case** | Platform vendor managing multiple customers | Single customer running the product directly |
Standalone mode generates a simpler compose with the server running directly. No Logto, no SaaS management plane, no bootstrap. The admin logs in with local credentials at `/`.
### Tenant Provisioning Flow
When SaaS admin creates a tenant via `VendorTenantService`:
**Synchronous (in `createAndProvision`):**
1. Create `TenantEntity` (status=PROVISIONING) + Logto organization
2. Create admin user in Logto with owner org role (if credentials provided)
3. Register OIDC redirect URIs for `/t/{slug}/oidc/callback` on Logto Traditional Web App
5. Generate license (tier-appropriate, 365 days)
6. Return immediately — UI shows provisioning spinner, polls via `refetchInterval`
**Asynchronous (in `provisionAsync`, `@Async`):**
7. Create tenant-isolated Docker network (`cameleer-tenant-{slug}`)
8. Create server container with env vars, Traefik labels (`traefik.docker.network`), health check, Docker socket bind, JAR volume, certs volume (ro)
9. Create UI container with `CAMELEER_API_URL`, `BASE_PATH`, Traefik strip-prefix labels
10. Wait for health check (`/api/v1/health`, not `/actuator/health` which requires auth)
11. Push license token to server via M2M API
12. Push OIDC config (Traditional Web App credentials + `additionalScopes: [urn:logto:scope:organizations, urn:logto:scope:organization_roles]`) to server for SSO
13. Update tenant status -> ACTIVE (or set `provisionError` on failure)
**Server restart** (available to SaaS admin + tenant admin):
- `POST /api/vendor/tenants/{id}/restart` (SaaS admin) and `POST /api/tenant/server/restart` (tenant)
- Calls `TenantProvisioner.stop(slug)` then `start(slug)` — restarts server + UI containers only (same image)
**Server upgrade** (available to SaaS admin + tenant admin):
- `POST /api/vendor/tenants/{id}/upgrade` (SaaS admin) and `POST /api/tenant/server/upgrade` (tenant)
- Calls `TenantProvisioner.upgrade(slug)` — removes server + UI containers, force-pulls latest images (preserves app containers, volumes, networks), then `provisionAsync()` re-creates containers with the new image + pushes license + OIDC config
**Tenant delete** cleanup:
- `DockerTenantProvisioner.remove(slug)` — label-based container removal (`cameleer.tenant={slug}`), env network cleanup, tenant network removal, JAR volume removal
- `TenantDataCleanupService.cleanup(slug)` — drops PostgreSQL `tenant_{slug}` schema, deletes ClickHouse data (GDPR)
**Password management** (tenant portal):
- `POST /api/tenant/password` — tenant admin changes own Logto password (via `@AuthenticationPrincipal` JWT subject)
- `POST /api/tenant/team/{userId}/password` — tenant admin resets a team member's Logto password (validates org membership first)
- `POST /api/tenant/server/admin-password` — tenant admin resets the server's built-in local admin password (via M2M API to `POST /api/v1/admin/users/user:admin/password`)
For detailed architecture docs, see the directory-scoped CLAUDE.md files (loaded automatically when editing code in that directory):
- **Provisioning flow, env vars, lifecycle** → `src/.../provisioning/CLAUDE.md`
- **Auth, scopes, JWT, OIDC** → `src/.../config/CLAUDE.md`
- **Docker, routing, networks, bootstrap, deployment pipeline** → `docker/CLAUDE.md`
- **Installer, deployment modes, compose templates** → `installer/CLAUDE.md`
- **Frontend, sign-in UI** → `ui/CLAUDE.md`
## 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
- V011 — add provisioning fields (server_endpoint, provision_error)
- V012 — certificates table + tenants.ca_applied_at
- V013 — tenant_ca_certs (per-tenant CA certificates with PEM storage)
- V001 — consolidated baseline: tenants (with db_password, server_endpoint, provision_error, ca_applied_at), licenses, audit_log, certificates, tenant_ca_certs
## Related Conventions
@@ -345,8 +62,8 @@ PostgreSQL (Flyway): `src/main/resources/db/migration/`
- Docker images: CI builds and pushes all images — Dockerfiles use multi-stage builds, no local builds needed
- `cameleer-saas` — SaaS vendor management plane (frontend + JAR baked in)
- `cameleer-logto` — custom Logto with sign-in UI baked in
- `cameleer3-server` / `cameleer3-server-ui` — provisioned per-tenant (not in compose, created by `DockerTenantProvisioner`)
- `cameleer-runtime-base` — base image for deployed apps (agent JAR + JRE). CI downloads latest agent SNAPSHOT from Gitea Maven registry. Uses `CAMELEER_SERVER_RUNTIME_SERVERURL` env var (not CAMELEER_EXPORT_ENDPOINT).
- `cameleer-server` / `cameleer-server-ui` — provisioned per-tenant (not in compose, created by `DockerTenantProvisioner`)
- `cameleer-runtime-base` — base image for deployed apps (agent JAR + `cameleer-log-appender.jar` + JRE). CI downloads latest agent and log appender SNAPSHOTs from Gitea Maven registry. The Dockerfile ENTRYPOINT is overridden by `DockerRuntimeOrchestrator` at container creation; agent config uses `CAMELEER_AGENT_*` env vars set by `DeploymentExecutor`.
- 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 for tenant provisioning.
- Design system: import from `@cameleer/design-system` (Gitea npm registry)
@@ -358,7 +75,7 @@ PostgreSQL (Flyway): `src/main/resources/db/migration/`
<!-- gitnexus:start -->
# GitNexus — Code Intelligence
This project is indexed by GitNexus as **cameleer-saas** (2676 symbols, 5768 relationships, 224 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **cameleer-saas** (2816 symbols, 5989 relationships, 238 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first.

View File

@@ -48,7 +48,7 @@ The platform runs as a Docker Compose stack:
*Ports exposed to host only with `docker-compose.dev.yml` overlay.
Per-tenant `cameleer3-server` and `cameleer3-server-ui` containers are provisioned dynamically by `DockerTenantProvisioner` — they are NOT part of the compose stack.
Per-tenant `cameleer-server` and `cameleer-server-ui` containers are provisioned dynamically by `DockerTenantProvisioner` — they are NOT part of the compose stack.
## Installation
@@ -222,7 +222,7 @@ To disable routing, set `exposedPort` to `null`.
### View the Observability Dashboard
The cameleer3-server React SPA dashboard is available at:
The cameleer-server React SPA dashboard is available at:
```
http://localhost/dashboard
@@ -233,7 +233,7 @@ This shows execution traces, route topology graphs, metrics, and logs for all de
### Check Agent & Observability Status
```bash
# Is the agent registered with cameleer3-server?
# Is the agent registered with cameleer-server?
curl "http://localhost:8080/api/apps/$APP_ID/agent-status" \
-H "Authorization: Bearer $TOKEN"
# Returns: registered, state (ACTIVE/STALE/DEAD/UNKNOWN), routeIds
@@ -303,7 +303,7 @@ Query params: `since`, `until` (ISO timestamps), `limit` (default 500), `stream`
### Dashboard
| Path | Description |
|------|-------------|
| `/dashboard` | cameleer3-server observability dashboard (forward-auth protected) |
| `/dashboard` | cameleer-server observability dashboard (forward-auth protected) |
### Vendor: Certificates (platform:admin)
| Method | Path | Description |
@@ -404,7 +404,7 @@ Output goes to `src/main/resources/static/` (configured in `vite.config.ts`). Th
### SPA Routing
Spring Boot serves `index.html` for all non-API routes via `SpaController.java`. React Router handles client-side routing. The SPA lives at `/`, while the observability dashboard (cameleer3-server) is at `/dashboard`.
Spring Boot serves `index.html` for all non-API routes via `SpaController.java`. React Router handles client-side routing. The SPA lives at `/`, while the observability dashboard (cameleer-server) is at `/dashboard`.
## Development

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
audit/03-login-page.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
audit/04-login-error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
audit/06-license-page.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

BIN
audit/11-search-modal.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
audit/21-sidebar-detail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -26,10 +26,10 @@
| Severity | Issue | Element |
|----------|-------|---------|
| Important | **No password visibility toggle** -- the Password input uses `type="password"` with no eye icon to reveal. Most modern login forms offer this. | Password field |
| Important | **Branding says "cameleer3"** not "Cameleer" or "Cameleer SaaS" -- the product name on the login page is the internal repo name, not the user-facing brand | `.logo` text content |
| Important | **Branding says "cameleer"** not "Cameleer" or "Cameleer SaaS" -- the product name on the login page is the internal repo name, not the user-facing brand | `.logo` text content |
| Nice-to-have | **No "Forgot password" link** -- even if it goes to a "contact admin" page, users expect this | Below password field |
| Nice-to-have | **No Enter-key submit hint** -- though Enter does work via form submit, there's no visual affordance | Form area |
| Nice-to-have | **Page title is "Sign in -- cameleer3"** -- should match product branding ("Cameleer SaaS") | `<title>` tag |
| Nice-to-have | **Page title is "Sign in -- cameleer"** -- should match product branding ("Cameleer SaaS") | `<title>` tag |
---
@@ -216,7 +216,7 @@
### Important (17)
1. No password visibility toggle on login
2. Branding says "cameleer3" instead of product name on login
2. Branding says "cameleer" instead of product name on login
3. Breadcrumbs always empty on platform pages
4. Massive empty space below dashboard content
5. Tier badge color mapping inconsistent between Dashboard and License pages
@@ -235,7 +235,7 @@
### Nice-to-have (8)
1. No "Forgot password" link on login
2. Login page title uses "cameleer3" branding
2. Login page title uses "cameleer" branding
3. No external link icon on "Open Server Dashboard"
4. Avatar shows "AD" for "admin"
5. No units on limit values

View File

@@ -356,7 +356,7 @@ These use **different tier names** (enterprise/pro/starter vs BUSINESS/HIGH/MID/
3. **Hardcoded branding** (`SignInPage.tsx:61`):
```tsx
cameleer3
cameleer
```
The brand name is hardcoded text, not sourced from configuration.

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
audit/verify-02-license.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

4974
ci-docker-log.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -26,8 +26,9 @@ services:
SPRING_WEB_RESOURCES_STATIC_LOCATIONS: file:/app/static/,classpath:/static/
CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https}
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: gitea.siegeln.net/cameleer/cameleer3-server:${VERSION:-latest}
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: gitea.siegeln.net/cameleer/cameleer3-server-ui:${VERSION:-latest}
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: gitea.siegeln.net/cameleer/cameleer-server:${VERSION:-latest}
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: gitea.siegeln.net/cameleer/cameleer-server-ui:${VERSION:-latest}
CAMELEER_SAAS_PROVISIONING_RUNTIMEBASEIMAGE: gitea.siegeln.net/cameleer/cameleer-runtime-base:${VERSION:-latest}
CAMELEER_SAAS_PROVISIONING_NETWORKNAME: cameleer-saas_cameleer
CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK: cameleer-traefik

View File

@@ -126,6 +126,7 @@ services:
CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
CAMELEER_SAAS_IDENTITY_M2MCLIENTID: ${LOGTO_M2M_CLIENT_ID:-}
CAMELEER_SAAS_IDENTITY_M2MCLIENTSECRET: ${LOGTO_M2M_CLIENT_SECRET:-}
CAMELEER_SERVER_SECURITY_JWTSECRET: ${CAMELEER_SERVER_SECURITY_JWTSECRET:-cameleer-dev-jwt-secret}
# Provisioning — passed to per-tenant server containers
CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https}

88
docker/CLAUDE.md Normal file
View File

@@ -0,0 +1,88 @@
# Docker & Infrastructure
## Routing (single-domain, path-based via Traefik)
All services on one hostname. Infrastructure containers (Traefik, Logto) use `PUBLIC_HOST` + `PUBLIC_PROTOCOL` env vars directly. The SaaS app reads these via `CAMELEER_SAAS_PROVISIONING_PUBLICHOST` / `CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL` (Spring Boot properties `cameleer.saas.provisioning.publichost` / `cameleer.saas.provisioning.publicprotocol`).
| Path | Target | Notes |
|------|--------|-------|
| `/platform/*` | cameleer-saas:8080 | SPA + API (`server.servlet.context-path: /platform`) |
| `/platform/vendor/*` | (SPA routes) | Vendor console (platform:admin) |
| `/platform/tenant/*` | (SPA routes) | Tenant admin portal (org-scoped) |
| `/t/{slug}/*` | per-tenant server-ui | Provisioned tenant UI containers (Traefik labels) |
| `/` | 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: `traefik-certs` init container generates self-signed cert (dev) or copies user-supplied cert via `CERT_FILE`/`KEY_FILE`/`CA_FILE` env vars. Default cert configured in `docker/traefik-dynamic.yml` (NOT static `traefik.yml` — Traefik v3 ignores `tls.stores.default` in static config). Runtime cert replacement via vendor UI (stage/activate/restore). ACME for production (future). Server containers import `/certs/ca.pem` into JVM truststore at startup via `docker-entrypoint.sh` for OIDC trust.
- Root `/` -> `/platform/` redirect via Traefik file provider (`docker/traefik-dynamic.yml`)
- LoginPage auto-redirects to Logto OIDC (no intermediate button)
- Per-tenant server containers get Traefik labels for `/t/{slug}/*` routing at provisioning time
## Docker Networks
Compose-defined networks:
| Network | Name on Host | Purpose |
|---------|-------------|---------|
| `cameleer` | `cameleer-saas_cameleer` | Compose default — shared services (DB, Logto, SaaS) |
| `cameleer-traefik` | `cameleer-traefik` (fixed `name:`) | Traefik + provisioned tenant containers |
Per-tenant networks (created dynamically by `DockerTenantProvisioner`):
| Network | Name Pattern | Purpose |
|---------|-------------|---------|
| Tenant network | `cameleer-tenant-{slug}` | Internal bridge, no internet — isolates tenant server + apps |
| Environment network | `cameleer-env-{tenantId}-{envSlug}` | Tenant-scoped (includes tenantId to prevent slug collision across tenants) |
Server containers join three networks: tenant network (primary), shared services network (`cameleer`), and traefik network. Apps deployed by the server use the tenant network as primary.
**Backend IP resolution:** Traefik's Docker provider is configured with `network: cameleer-traefik` (static `traefik.yml`). Every cameleer-managed container — saas-provisioned tenant containers (via `DockerTenantProvisioner`) and cameleer-server's per-app containers (via `DockerNetworkManager`) — is attached to `cameleer-traefik` at creation, so Traefik always resolves a reachable backend IP. Provisioned tenant containers additionally emit a `traefik.docker.network=cameleer-traefik` label as per-service defense-in-depth. (Pre-2026-04-23 the static config pointed at `network: cameleer`, a name that never matched any real network — that produced 504 Gateway Timeout on every managed app until the Traefik image was rebuilt.)
## Custom sign-in UI (`ui/sign-in/`)
Separate Vite+React SPA replacing Logto's default sign-in page. Visually matches cameleer-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)
## Deployment pipeline
App deployment is handled by the cameleer-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 cameleer-server) — async staged deployment, runtime type auto-detection
- `DockerRuntimeOrchestrator.java` (in cameleer-server) — Docker client, container lifecycle, builds runtime-type-specific entrypoints (spring-boot uses `-cp` + `PropertiesLauncher` with `-Dloader.path` for log appender; quarkus uses `-jar`; plain-java uses `-cp` + detected main class; native exec directly). Overrides the Dockerfile ENTRYPOINT.
- `docker/runtime-base/Dockerfile` — base image with agent JAR + `cameleer-log-appender.jar` + JRE. The Dockerfile ENTRYPOINT (`-jar /app/app.jar`) is a fallback — `DockerRuntimeOrchestrator` overrides it at container creation.
- `RuntimeDetector.java` (in cameleer-server) — detects runtime type from JAR manifest `Main-Class`; derives correct `PropertiesLauncher` package (Spring Boot 3.2+ vs pre-3.2)
- `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-tenant-{slug}` (primary, isolation) + `cameleer-traefik` (routing) + `cameleer-env-{tenantId}-{envSlug}` (environment isolation)
## Bootstrap (`docker/logto-bootstrap.sh`)
Idempotent script run inside the Logto container entrypoint. **Clean slate** — no example tenant, no viewer user, no server configuration. Phases:
1. Wait for Logto health (no server to wait for — servers are provisioned per-tenant)
2. Get Management API token (reads `m-default` secret from DB)
3. Create Logto apps (SPA, Traditional Web App with `skipConsent`, M2M with Management API role + server API role)
3b. Create API resource scopes (1 platform + 9 tenant + 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 admin user (SaaS admin with Logto console access)
7b. Configure Logto Custom JWT for access tokens (maps org roles -> `roles` claim: owner->server:admin, operator->server:operator, viewer->server:viewer; saas-vendor global role -> server:admin)
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`
12. Create `saas-vendor` global role with all API scopes and assign to admin user (always runs — admin IS the platform admin).
The multi-tenant compose stack is: Traefik + PostgreSQL + ClickHouse + Logto (with bootstrap entrypoint) + cameleer-saas. No `cameleer-server` or `cameleer-server-ui` in compose — those are provisioned per-tenant by `DockerTenantProvisioner`.

View File

@@ -3,7 +3,7 @@ set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE DATABASE logto;
CREATE DATABASE cameleer3;
CREATE DATABASE cameleer;
GRANT ALL PRIVILEGES ON DATABASE logto TO $POSTGRES_USER;
GRANT ALL PRIVILEGES ON DATABASE cameleer3 TO $POSTGRES_USER;
GRANT ALL PRIVILEGES ON DATABASE cameleer TO $POSTGRES_USER;
EOSQL

View File

@@ -18,6 +18,6 @@ providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: cameleer
network: cameleer-traefik
file:
filename: /etc/traefik/dynamic.yml

View File

@@ -4,7 +4,7 @@ set -e
# Cameleer SaaS — Bootstrap Script
# Creates Logto apps, users, organizations, roles.
# Seeds cameleer_saas DB with tenant, environment, license.
# Configures cameleer3-server OIDC.
# Configures cameleer-server OIDC.
# Idempotent: checks existence before creating.
LOGTO_ENDPOINT="${LOGTO_ENDPOINT:-http://cameleer-logto:3001}"
@@ -174,7 +174,7 @@ else
log "Created SPA app: $SPA_ID"
fi
# --- Traditional Web App (for cameleer3-server OIDC) ---
# --- Traditional Web App (for cameleer-server OIDC) ---
TRAD_ID=$(echo "$EXISTING_APPS" | jq -r ".[] | select(.name == \"$TRAD_APP_NAME\" and .type == \"Traditional\") | .id")
TRAD_SECRET=""
if [ -n "$TRAD_ID" ]; then

View File

@@ -1,9 +1,9 @@
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# Agent JAR is copied during CI build from Gitea Maven registry
# ARG AGENT_JAR=cameleer3-agent-1.0-SNAPSHOT-shaded.jar
# Agent JAR and log appender JAR are copied during CI build from Gitea Maven registry
COPY agent.jar /app/agent.jar
COPY cameleer-log-appender.jar /app/cameleer-log-appender.jar
ENTRYPOINT exec java \
-Dcameleer.export.type=${CAMELEER_EXPORT_TYPE:-HTTP} \

View File

@@ -15,12 +15,12 @@ infrastructure themselves.
The system comprises three components:
**Cameleer Agent** (`cameleer3` repo) -- A Java agent using ByteBuddy for
**Cameleer Agent** (`cameleer` 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
**Cameleer Server** (`cameleer-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
@@ -50,7 +50,7 @@ logging. Serves a React SPA that wraps the full user experience.
| | /interaction) |
v v v v
+--------------+ +--------------+ +-----------+ +------------------+
| cameleer-saas| | cameleer-saas| | Logto | | cameleer3-server |
| cameleer-saas| | cameleer-saas| | Logto | | cameleer-server |
| (API) | | (SPA) | | | | |
| :8080 | | :8080 | | :3001 | | :8081 |
+--------------+ +--------------+ +-----------+ +------------------+
@@ -80,14 +80,14 @@ logging. Serves a React SPA that wraps the full user experience.
| 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 |
| cameleer-server | `gitea.siegeln.net/cameleer/cameleer-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.
cameleer-server.
### Volumes
@@ -105,7 +105,7 @@ 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
- `cameleer` -- cameleer-server operational data
The `docker/init-databases.sh` init script creates all three during first start.
@@ -128,9 +128,9 @@ The `docker/init-databases.sh` init script creates all three during first start.
|--------------------|-----------------|------------------|----------------------|--------------------------------|
| 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|
| Server internal JWT| cameleer-server| HS256 (symmetric) | Issuing server only | Agents (after registration) |
| API key (opaque) | SaaS platform | N/A (SHA-256 hash)| cameleer-server | Agent initial registration |
| Ed25519 signature | cameleer-server| EdDSA | Agent | Server -> agent command signing|
### 3.3 Scope Model
@@ -183,7 +183,7 @@ the bootstrap script (`docker/logto-bootstrap.sh`):
4. `organization_id` claim in JWT resolves to internal tenant ID via
`TenantIsolationInterceptor`.
**SaaS platform -> cameleer3-server API (M2M):**
**SaaS platform -> cameleer-server API (M2M):**
1. SaaS platform obtains Logto M2M token (`client_credentials` grant) via
`LogtoManagementClient`.
@@ -191,7 +191,7 @@ the bootstrap script (`docker/logto-bootstrap.sh`):
3. Server validates via Logto JWKS (OIDC resource server support).
4. Server grants ADMIN role to valid M2M tokens.
**Agent -> cameleer3-server:**
**Agent -> cameleer-server:**
1. Agent reads `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` environment variable (API key).
2. Calls `POST /api/v1/agents/register` with the key as Bearer token.
@@ -458,9 +458,9 @@ Defined in `AuditAction.java`:
### 5.1 Server-Per-Tenant
Each tenant gets a dedicated cameleer3-server instance. The SaaS platform
Each tenant gets a dedicated cameleer-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
single shared cameleer-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
@@ -495,7 +495,7 @@ The deployment lifecycle is managed by `DeploymentService`:
|-----------------------------|----------------------------------------|
| `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` | API key for agent registration |
| `CAMELEER_EXPORT_TYPE` | `HTTP` |
| `CAMELEER_SERVER_RUNTIME_SERVERURL` | cameleer3-server internal URL |
| `CAMELEER_SERVER_RUNTIME_SERVERURL` | cameleer-server internal URL |
| `CAMELEER_APPLICATION_ID` | App slug |
| `CAMELEER_ENVIRONMENT_ID` | Environment slug |
| `CAMELEER_DISPLAY_NAME` | `{tenant}-{env}-{app}` |
@@ -524,14 +524,14 @@ Configured via `RuntimeConfig`:
## 6. Agent-Server Protocol
The agent-server protocol is defined in full in
`cameleer3/cameleer3-common/PROTOCOL.md`. This section summarizes the key
`cameleer/cameleer-common/PROTOCOL.md`. This section summarizes the key
aspects relevant to the SaaS platform.
### 6.1 Agent Registration
1. Agent starts with `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` 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
2. Agent calls `POST /api/v1/agents/register` on the cameleer-server with the
API key as a Bearer token.
3. Server validates the key and returns:
- HMAC JWT access token (short-lived, ~1 hour)
@@ -744,7 +744,7 @@ leaks regardless of whether the request succeeded or failed.
|----------------------|-------------|------------------------------------|
| 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 agent JWT | ~1 hour | cameleer-server `CAMELEER_JWT_SECRET` |
| Server refresh token | ~7 days | Agent re-registers when expired |
### 8.4 Audit Logging
@@ -876,28 +876,28 @@ state (`currentTenantId`). Provides `logout` and `signIn` callbacks.
| Variable | Default | Description |
|-----------------------------------|------------------------------------|----------------------------------|
| `CAMELEER_SAAS_PROVISIONING_SERVERIMAGE` | `gitea.siegeln.net/cameleer/cameleer3-server:latest` | Docker image for per-tenant server |
| `CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE` | `gitea.siegeln.net/cameleer/cameleer3-server-ui:latest` | Docker image for per-tenant UI |
| `CAMELEER_SAAS_PROVISIONING_SERVERIMAGE` | `gitea.siegeln.net/cameleer/cameleer-server:latest` | Docker image for per-tenant server |
| `CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE` | `gitea.siegeln.net/cameleer/cameleer-server-ui:latest` | Docker image for per-tenant UI |
| `CAMELEER_SAAS_PROVISIONING_NETWORKNAME` | `cameleer-saas_cameleer` | Shared services Docker network |
| `CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK` | `cameleer-traefik` | Traefik Docker network |
| `CAMELEER_SAAS_PROVISIONING_PUBLICHOST` | `localhost` | Public hostname (same as infrastructure `PUBLIC_HOST`) |
| `CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL` | `https` | Public protocol (same as infrastructure `PUBLIC_PROTOCOL`) |
| `CAMELEER_SAAS_PROVISIONING_DATASOURCEURL` | `jdbc:postgresql://cameleer-postgres:5432/cameleer3` | PostgreSQL URL passed to tenant servers |
| `CAMELEER_SAAS_PROVISIONING_DATASOURCEURL` | `jdbc:postgresql://cameleer-postgres:5432/cameleer` | PostgreSQL URL passed to tenant servers |
| `CAMELEER_SAAS_PROVISIONING_CLICKHOUSEURL` | `jdbc:clickhouse://cameleer-clickhouse:8123/cameleer` | ClickHouse URL passed to tenant servers |
### 10.2 cameleer3-server (per-tenant)
### 10.2 cameleer-server (per-tenant)
Env vars injected into provisioned per-tenant server containers by `DockerTenantProvisioner`. All server properties use the `cameleer.server.*` prefix (env vars: `CAMELEER_SERVER_*`).
| Variable | Default / Value | Description |
|------------------------------|----------------------------------------------|----------------------------------|
| `SPRING_DATASOURCE_URL` | `jdbc:postgresql://cameleer-postgres:5432/cameleer3` | PostgreSQL JDBC URL |
| `SPRING_DATASOURCE_URL` | `jdbc:postgresql://cameleer-postgres:5432/cameleer` | PostgreSQL JDBC URL |
| `SPRING_DATASOURCE_USERNAME`| `cameleer` | PostgreSQL user |
| `SPRING_DATASOURCE_PASSWORD`| `cameleer_dev` | PostgreSQL password |
| `CAMELEER_SERVER_CLICKHOUSE_URL` | `jdbc:clickhouse://cameleer-clickhouse:8123/cameleer` | ClickHouse JDBC URL |
| `CAMELEER_SERVER_TENANT_ID` | *(tenant slug)* | Tenant identifier for data isolation |
| `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` | *(generated)* | Agent bootstrap token |
| `CAMELEER_SERVER_SECURITY_JWTSECRET` | *(generated)* | JWT signing secret |
| `CAMELEER_SERVER_SECURITY_JWTSECRET` | *(generated, must be non-empty)* | JWT signing secret |
| `CAMELEER_SERVER_SECURITY_OIDC_ISSUERURI` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/oidc` | OIDC issuer for M2M tokens |
| `CAMELEER_SERVER_SECURITY_OIDC_JWKSETURI` | `http://cameleer-logto:3001/oidc/jwks` | Docker-internal JWK fetch |
| `CAMELEER_SERVER_SECURITY_OIDC_AUDIENCE` | `https://api.cameleer.local` | JWT audience validation |

View File

@@ -80,7 +80,7 @@ Note: Phase 9 (Frontend) can be developed in parallel with Phases 3-8, building
**PRD Sections:** 6 (Tenant Provisioning), 11 (Networking & Tenant Isolation)
**Gitea Epics:** #3 (Tenant Provisioning), #8 (Networking)
**Depends on:** Phase 2
**Produces:** Automated tenant provisioning pipeline. Signup creates tenant → Flux HelmRelease generated → namespace provisioned → cameleer3-server deployed → PostgreSQL schema + OpenSearch index created → tenant ACTIVE. NetworkPolicies enforced.
**Produces:** Automated tenant provisioning pipeline. Signup creates tenant → Flux HelmRelease generated → namespace provisioned → cameleer-server deployed → PostgreSQL schema + OpenSearch index created → tenant ACTIVE. NetworkPolicies enforced.
**Key deliverables:**
- Provisioning state machine (idempotent, retryable)
@@ -91,7 +91,7 @@ Note: Phase 9 (Frontend) can be developed in parallel with Phases 3-8, building
- Readiness checking (poll tenant server health)
- Tenant lifecycle operations (suspend, reactivate, delete)
- K8s NetworkPolicy templates (default deny + allow rules)
- Helm chart for cameleer3-server tenant deployment
- Helm chart for cameleer-server tenant deployment
---
@@ -143,11 +143,11 @@ Note: Phase 9 (Frontend) can be developed in parallel with Phases 3-8, building
**PRD Sections:** 8 (Observability Integration)
**Gitea Epics:** #6 (Observability Integration), #13 (Exchange Replay — gating only)
**Depends on:** Phase 3 (server already deployed per tenant), Phase 2 (license for feature gating)
**Produces:** Tenants see their cameleer3-server UI embedded in the SaaS shell. API gateway routes to tenant server. MOAT features gated by license tier.
**Produces:** Tenants see their cameleer-server UI embedded in the SaaS shell. API gateway routes to tenant server. MOAT features gated by license tier.
**Key deliverables:**
- Ingress routing rules: `/t/{tenant}/api/*` → tenant's cameleer3-server
- cameleer3-server "managed mode" configuration (trust SaaS JWT, report metrics)
- Ingress routing rules: `/t/{tenant}/api/*` → tenant's cameleer-server
- cameleer-server "managed mode" configuration (trust SaaS JWT, report metrics)
- Bootstrap token generation API
- MOAT feature gating via license (topology=all, lineage=limited/full, correlation=mid+, debugger=high+, replay=high+)
- Server UI embedding approach (iframe or reverse proxy with path rewriting)
@@ -211,7 +211,7 @@ Note: Phase 9 (Frontend) can be developed in parallel with Phases 3-8, building
- SaaS shell (navigation, tenant switcher, user menu)
- Dashboard (platform overview)
- Apps list + App deployment page (upload, config, secrets, status, logs, versions)
- Observability section (embedded cameleer3-server UI)
- Observability section (embedded cameleer-server UI)
- Team management pages
- Settings pages (tenant config, SSO/OIDC, vault connections)
- Billing pages (usage, invoices, plan management)

View File

@@ -2006,7 +2006,7 @@ available throughout request lifecycle."
**Files:**
- Create: `src/main/java/net/siegeln/cameleer/saas/config/ForwardAuthController.java`
This endpoint is called by Traefik's ForwardAuth middleware to validate requests routed to non-platform services (e.g., cameleer3-server). It validates the JWT, resolves the tenant, and returns tenant context headers.
This endpoint is called by Traefik's ForwardAuth middleware to validate requests routed to non-platform services (e.g., cameleer-server). It validates the JWT, resolves the tenant, and returns tenant context headers.
- [ ] **Step 1: Create ForwardAuthController**
@@ -2455,8 +2455,8 @@ services:
networks:
- cameleer
cameleer3-server:
image: ${CAMELEER3_SERVER_IMAGE:-gitea.siegeln.net/cameleer/cameleer3-server}:${VERSION:-latest}
cameleer-server:
image: ${CAMELEER_SERVER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server}:${VERSION:-latest}
restart: unless-stopped
depends_on:
postgres:
@@ -2539,9 +2539,9 @@ git add docker-compose.yml docker-compose.dev.yml traefik.yml docker/init-databa
git commit -m "feat: add Docker Compose production stack with Traefik + Logto
7-container stack: Traefik (reverse proxy), PostgreSQL (shared),
Logto (identity), cameleer-saas (control plane), cameleer3-server
Logto (identity), cameleer-saas (control plane), cameleer-server
(observability), ClickHouse (traces). ForwardAuth middleware for
tenant-aware routing to cameleer3-server."
tenant-aware routing to cameleer-server."
```
---

View File

@@ -2,7 +2,7 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Customers can upload a Camel JAR, the platform builds a container image with cameleer3 agent auto-injected, and deploys it to a logical environment with full lifecycle management.
**Goal:** Customers can upload a Camel JAR, the platform builds a container image with cameleer agent auto-injected, and deploys it to a logical environment with full lifecycle management.
**Architecture:** Environment → App → Deployment entity hierarchy. `RuntimeOrchestrator` interface with `DockerRuntimeOrchestrator` (docker-java) implementation. Async deployment pipeline with status polling. Container logs streamed to ClickHouse. Pre-built `cameleer-runtime-base` image for fast (~1-3s) customer image builds.
@@ -164,8 +164,8 @@ public class RuntimeConfig {
@Value("${cameleer.runtime.bootstrap-token:${CAMELEER_AUTH_TOKEN:}}")
private String bootstrapToken;
@Value("${cameleer.runtime.cameleer3-server-endpoint:http://cameleer3-server:8081}")
private String cameleer3ServerEndpoint;
@Value("${cameleer.runtime.cameleer-server-endpoint:http://cameleer-server:8081}")
private String cameleerServerEndpoint;
public long getMaxJarSize() { return maxJarSize; }
public String getJarStoragePath() { return jarStoragePath; }
@@ -177,7 +177,7 @@ public class RuntimeConfig {
public String getContainerMemoryLimit() { return containerMemoryLimit; }
public int getContainerCpuShares() { return containerCpuShares; }
public String getBootstrapToken() { return bootstrapToken; }
public String getCameleer3ServerEndpoint() { return cameleer3ServerEndpoint; }
public String getCameleerServerEndpoint() { return cameleerServerEndpoint; }
public long parseMemoryLimitBytes() {
var limit = containerMemoryLimit.trim().toLowerCase();
@@ -270,7 +270,7 @@ Append to the existing `cameleer:` section in `src/main/resources/application.ym
container-memory-limit: ${CAMELEER_CONTAINER_MEMORY_LIMIT:512m}
container-cpu-shares: ${CAMELEER_CONTAINER_CPU_SHARES:512}
bootstrap-token: ${CAMELEER_AUTH_TOKEN:}
cameleer3-server-endpoint: ${CAMELEER3_SERVER_ENDPOINT:http://cameleer3-server:8081}
cameleer-server-endpoint: ${CAMELEER_SERVER_ENDPOINT:http://cameleer-server:8081}
clickhouse:
url: ${CLICKHOUSE_URL:jdbc:clickhouse://clickhouse:8123/cameleer}
```
@@ -2788,7 +2788,7 @@ public class DeploymentService {
var envVars = Map.of(
"CAMELEER_AUTH_TOKEN", env.getBootstrapToken(),
"CAMELEER_EXPORT_TYPE", "HTTP",
"CAMELEER_EXPORT_ENDPOINT", runtimeConfig.getCameleer3ServerEndpoint(),
"CAMELEER_EXPORT_ENDPOINT", runtimeConfig.getCameleerServerEndpoint(),
"CAMELEER_APPLICATION_ID", app.getSlug(),
"CAMELEER_ENVIRONMENT_ID", env.getSlug(),
"CAMELEER_DISPLAY_NAME", containerName);
@@ -3418,7 +3418,7 @@ volumes:
Add to the cameleer-saas service environment:
```yaml
CAMELEER_AUTH_TOKEN: ${CAMELEER_AUTH_TOKEN:-default-bootstrap-token}
CAMELEER3_SERVER_ENDPOINT: http://cameleer3-server:8081
CAMELEER_SERVER_ENDPOINT: http://cameleer-server:8081
CLICKHOUSE_URL: jdbc:clickhouse://clickhouse:8123/cameleer
```
@@ -3427,7 +3427,7 @@ Add to the cameleer-saas service volumes:
- jardata:/data/jars
```
Add `CAMELEER_AUTH_TOKEN` to the cameleer3-server service environment:
Add `CAMELEER_AUTH_TOKEN` to the cameleer-server service environment:
```yaml
CAMELEER_AUTH_TOKEN: ${CAMELEER_AUTH_TOKEN:-default-bootstrap-token}
```
@@ -3448,7 +3448,7 @@ FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# Agent JAR is copied during CI build from Gitea Maven registry
# ARG AGENT_JAR=cameleer3-agent-1.0-SNAPSHOT-shaded.jar
# ARG AGENT_JAR=cameleer-agent-1.0-SNAPSHOT-shaded.jar
COPY agent.jar /app/agent.jar
ENTRYPOINT exec java \

View File

@@ -2,9 +2,9 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Complete the deploy → hit endpoint → see traces loop. Serve the existing cameleer3-server dashboard, add agent connectivity verification, enable optional inbound HTTP routing for customer apps, and wire up observability data health checks.
**Goal:** Complete the deploy → hit endpoint → see traces loop. Serve the existing cameleer-server dashboard, add agent connectivity verification, enable optional inbound HTTP routing for customer apps, and wire up observability data health checks.
**Architecture:** Wiring phase — cameleer3-server already has full observability. Phase 4 adds Traefik routing for the dashboard + customer app endpoints, new API endpoints in cameleer-saas for agent-status and observability-status, and configures `CAMELEER_TENANT_ID` on the server.
**Architecture:** Wiring phase — cameleer-server already has full observability. Phase 4 adds Traefik routing for the dashboard + customer app endpoints, new API endpoints in cameleer-saas for agent-status and observability-status, and configures `CAMELEER_TENANT_ID` on the server.
**Tech Stack:** Spring Boot 3.4.3, docker-java 3.4.1, ClickHouse JDBC, Traefik v3 labels, Spring RestClient
@@ -14,7 +14,7 @@
### New Files
- `src/main/java/net/siegeln/cameleer/saas/observability/AgentStatusService.java` — Queries cameleer3-server for agent registration
- `src/main/java/net/siegeln/cameleer/saas/observability/AgentStatusService.java` — Queries cameleer-server for agent registration
- `src/main/java/net/siegeln/cameleer/saas/observability/AgentStatusController.java` — Agent status + observability status endpoints
- `src/main/java/net/siegeln/cameleer/saas/observability/dto/AgentStatusResponse.java` — Response DTO
- `src/main/java/net/siegeln/cameleer/saas/observability/dto/ObservabilityStatusResponse.java` — Response DTO
@@ -359,7 +359,7 @@ class AgentStatusServiceTest {
@BeforeEach
void setUp() {
when(runtimeConfig.getCameleer3ServerEndpoint()).thenReturn("http://cameleer3-server:8081");
when(runtimeConfig.getCameleerServerEndpoint()).thenReturn("http://cameleer-server:8081");
agentStatusService = new AgentStatusService(appRepository, environmentRepository, runtimeConfig);
}
@@ -439,7 +439,7 @@ public class AgentStatusService {
this.environmentRepository = environmentRepository;
this.runtimeConfig = runtimeConfig;
this.restClient = RestClient.builder()
.baseUrl(runtimeConfig.getCameleer3ServerEndpoint())
.baseUrl(runtimeConfig.getCameleerServerEndpoint())
.build();
}
@@ -475,7 +475,7 @@ public class AgentStatusService {
return new AgentStatusResponse(false, "NOT_REGISTERED", null,
List.of(), app.getSlug(), env.getSlug());
} catch (Exception e) {
log.warn("Failed to query agent status from cameleer3-server: {}", e.getMessage());
log.warn("Failed to query agent status from cameleer-server: {}", e.getMessage());
return new AgentStatusResponse(false, "UNKNOWN", null,
List.of(), app.getSlug(), env.getSlug());
}
@@ -651,28 +651,28 @@ public class ConnectivityHealthCheck {
@EventListener(ApplicationReadyEvent.class)
public void verifyConnectivity() {
checkCameleer3Server();
checkCameleerServer();
}
private void checkCameleer3Server() {
private void checkCameleerServer() {
try {
var client = RestClient.builder()
.baseUrl(runtimeConfig.getCameleer3ServerEndpoint())
.baseUrl(runtimeConfig.getCameleerServerEndpoint())
.build();
var response = client.get()
.uri("/actuator/health")
.retrieve()
.toBodilessEntity();
if (response.getStatusCode().is2xxSuccessful()) {
log.info("cameleer3-server connectivity: OK ({})",
runtimeConfig.getCameleer3ServerEndpoint());
log.info("cameleer-server connectivity: OK ({})",
runtimeConfig.getCameleerServerEndpoint());
} else {
log.warn("cameleer3-server connectivity: HTTP {} ({})",
response.getStatusCode(), runtimeConfig.getCameleer3ServerEndpoint());
log.warn("cameleer-server connectivity: HTTP {} ({})",
response.getStatusCode(), runtimeConfig.getCameleerServerEndpoint());
}
} catch (Exception e) {
log.warn("cameleer3-server connectivity: FAILED ({}) - {}",
runtimeConfig.getCameleer3ServerEndpoint(), e.getMessage());
log.warn("cameleer-server connectivity: FAILED ({}) - {}",
runtimeConfig.getCameleerServerEndpoint(), e.getMessage());
}
}
}
@@ -686,7 +686,7 @@ Run: `mvn compile -B -q`
```bash
git add src/main/java/net/siegeln/cameleer/saas/observability/ConnectivityHealthCheck.java
git commit -m "feat: add cameleer3-server startup connectivity check"
git commit -m "feat: add cameleer-server startup connectivity check"
```
---
@@ -700,7 +700,7 @@ git commit -m "feat: add cameleer3-server startup connectivity check"
- [ ] **Step 1: Update docker-compose.yml — add dashboard route and CAMELEER_TENANT_ID**
In the `cameleer3-server` service:
In the `cameleer-server` service:
Add to environment section:
```yaml
@@ -774,7 +774,7 @@ git commit -m "docs: update HOWTO with observability dashboard, routing, and age
| Spec Requirement | Task |
|---|---|
| Serve cameleer3-server dashboard via Traefik | Task 7 (dashboard Traefik labels) |
| Serve cameleer-server dashboard via Traefik | Task 7 (dashboard Traefik labels) |
| CAMELEER_TENANT_ID configuration | Task 7 (docker-compose env) |
| Agent connectivity verification endpoint | Task 4 (AgentStatusService + Controller) |
| Observability data health endpoint | Task 4 (ObservabilityStatusResponse) |

View File

@@ -4,7 +4,7 @@
**Goal:** Build a React SPA for managing tenants, environments, apps, and deployments. All backend APIs exist — this is the UI layer.
**Architecture:** React 19 + Vite + React Router + Zustand + TanStack Query + @cameleer/design-system. Sidebar layout matching cameleer3-server SPA. Shared Logto OIDC session. RBAC on all actions. Lives in `ui/` directory, built into Spring Boot static resources.
**Architecture:** React 19 + Vite + React Router + Zustand + TanStack Query + @cameleer/design-system. Sidebar layout matching cameleer-server SPA. Shared Logto OIDC session. RBAC on all actions. Lives in `ui/` directory, built into Spring Boot static resources.
**Tech Stack:** React 19, Vite 8, TypeScript, React Router 7, Zustand, TanStack React Query, @cameleer/design-system 0.1.31, Lucide React
@@ -332,7 +332,7 @@ git commit -m "feat: scaffold React SPA with Vite, design system, and TypeScript
- [ ] **Step 1: Create auth-store.ts**
Zustand store for auth state. Same localStorage keys as cameleer3-server SPA for SSO.
Zustand store for auth state. Same localStorage keys as cameleer-server SPA for SSO.
```typescript
import { create } from 'zustand';
@@ -1145,7 +1145,7 @@ git commit -m "feat: add SPA controller, Traefik route, CI frontend build, and H
|---|---|
| Project scaffolding (Vite, React, TS, design system) | Task 1 |
| TypeScript API types | Task 1 |
| Auth store (Zustand, same keys as cameleer3-server) | Task 2 |
| Auth store (Zustand, same keys as cameleer-server) | Task 2 |
| Login / Logto OIDC redirect / callback | Task 2 |
| Protected route | Task 2 |
| API client with auth middleware | Task 3 |

View File

@@ -2,35 +2,35 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace the incoherent three-system auth in cameleer-saas with Logto-centric architecture, and add OIDC resource server support to cameleer3-server for M2M.
**Goal:** Replace the incoherent three-system auth in cameleer-saas with Logto-centric architecture, and add OIDC resource server support to cameleer-server for M2M.
**Architecture:** Logto is the single identity provider for all humans. Spring OAuth2 Resource Server validates Logto JWTs in both the SaaS platform and cameleer3-server. Agents authenticate with per-environment API keys exchanged for server-issued JWTs. Ed25519 command signing is unchanged. Zero trust: every service validates tokens independently via JWKS.
**Architecture:** Logto is the single identity provider for all humans. Spring OAuth2 Resource Server validates Logto JWTs in both the SaaS platform and cameleer-server. Agents authenticate with per-environment API keys exchanged for server-issued JWTs. Ed25519 command signing is unchanged. Zero trust: every service validates tokens independently via JWKS.
**Tech Stack:** Spring Boot 3.4, Spring Security OAuth2 Resource Server, Nimbus JOSE+JWT, Logto, React + @logto/react, Zustand, PostgreSQL, Flyway
**Spec:** `docs/superpowers/specs/2026-04-05-auth-overhaul-design.md`
**Repos:**
- cameleer3-server: `C:\Users\Hendrik\Documents\projects\cameleer3-server` (Phase 1)
- cameleer-server: `C:\Users\Hendrik\Documents\projects\cameleer-server` (Phase 1)
- cameleer-saas: `C:\Users\Hendrik\Documents\projects\cameleer-saas` (Phases 2-3)
- cameleer3 (agent): NO CHANGES
- cameleer (agent): NO CHANGES
---
## Phase 1: cameleer3-server — OIDC Resource Server Support
## Phase 1: cameleer-server — OIDC Resource Server Support
All Phase 1 work is in `C:\Users\Hendrik\Documents\projects\cameleer3-server`.
All Phase 1 work is in `C:\Users\Hendrik\Documents\projects\cameleer-server`.
### Task 1: Add OAuth2 Resource Server dependency and config properties
**Files:**
- Modify: `cameleer3-server-app/pom.xml`
- Modify: `cameleer3-server-app/src/main/resources/application.yml`
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java`
- Modify: `cameleer-server-app/pom.xml`
- Modify: `cameleer-server-app/src/main/resources/application.yml`
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityProperties.java`
- [ ] **Step 1: Add dependency to pom.xml**
In `cameleer3-server-app/pom.xml`, add after the `spring-boot-starter-security` dependency (around line 88):
In `cameleer-server-app/pom.xml`, add after the `spring-boot-starter-security` dependency (around line 88):
```xml
<dependency>
@@ -41,7 +41,7 @@ In `cameleer3-server-app/pom.xml`, add after the `spring-boot-starter-security`
- [ ] **Step 2: Add OIDC properties to application.yml**
In `cameleer3-server-app/src/main/resources/application.yml`, add two new properties under the `security:` block (after line 52):
In `cameleer-server-app/src/main/resources/application.yml`, add two new properties under the `security:` block (after line 52):
```yaml
oidc-issuer-uri: ${CAMELEER_OIDC_ISSUER_URI:}
@@ -50,7 +50,7 @@ In `cameleer3-server-app/src/main/resources/application.yml`, add two new proper
- [ ] **Step 3: Add fields to SecurityProperties.java**
In `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java`, add after the `jwtSecret` field (line 19):
In `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityProperties.java`, add after the `jwtSecret` field (line 19):
```java
private String oidcIssuerUri;
@@ -64,13 +64,13 @@ public void setOidcAudience(String oidcAudience) { this.oidcAudience = oidcAudie
- [ ] **Step 4: Verify build compiles**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && ./mvnw compile -pl cameleer3-server-app -q`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && ./mvnw compile -pl cameleer-server-app -q`
Expected: BUILD SUCCESS
- [ ] **Step 5: Commit**
```bash
git add cameleer3-server-app/pom.xml cameleer3-server-app/src/main/resources/application.yml cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityProperties.java
git add cameleer-server-app/pom.xml cameleer-server-app/src/main/resources/application.yml cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityProperties.java
git commit -m "feat: add oauth2-resource-server dependency and OIDC config properties"
```
@@ -79,14 +79,14 @@ git commit -m "feat: add oauth2-resource-server dependency and OIDC config prope
### Task 2: Add conditional OIDC JwtDecoder bean
**Files:**
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java`
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityBeanConfig.java`
- [ ] **Step 1: Write the failing test**
Create `cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/OidcJwtDecoderBeanTest.java`:
Create `cameleer-server-app/src/test/java/com/cameleer/server/app/security/OidcJwtDecoderBeanTest.java`:
```java
package com.cameleer3.server.app.security;
package com.cameleer.server.app.security;
import org.junit.jupiter.api.Test;
import org.springframework.security.oauth2.jwt.JwtDecoder;
@@ -123,12 +123,12 @@ class OidcJwtDecoderBeanTest {
- [ ] **Step 2: Run test to verify it fails**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && ./mvnw test -pl cameleer3-server-app -Dtest=OidcJwtDecoderBeanTest -q`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && ./mvnw test -pl cameleer-server-app -Dtest=OidcJwtDecoderBeanTest -q`
Expected: FAIL — method `oidcJwtDecoder` does not exist
- [ ] **Step 3: Add the oidcJwtDecoder method to SecurityBeanConfig**
In `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java`, add these imports at the top:
In `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityBeanConfig.java`, add these imports at the top:
```java
import com.nimbusds.jose.JWSAlgorithm;
@@ -216,13 +216,13 @@ Update the test to match: the test calls `config.oidcJwtDecoder(properties)` dir
- [ ] **Step 5: Run test to verify it passes**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && ./mvnw test -pl cameleer3-server-app -Dtest=OidcJwtDecoderBeanTest -q`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && ./mvnw test -pl cameleer-server-app -Dtest=OidcJwtDecoderBeanTest -q`
Expected: PASS
- [ ] **Step 6: Commit**
```bash
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/OidcJwtDecoderBeanTest.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityBeanConfig.java cameleer-server-app/src/test/java/com/cameleer/server/app/security/OidcJwtDecoderBeanTest.java
git commit -m "feat: add conditional OIDC JwtDecoder factory for Logto token validation"
```
@@ -231,18 +231,18 @@ git commit -m "feat: add conditional OIDC JwtDecoder factory for Logto token val
### Task 3: Update JwtAuthenticationFilter with OIDC fallback
**Files:**
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java`
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java`
- [ ] **Step 1: Write the failing test**
Create `cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtAuthenticationFilterOidcTest.java`:
Create `cameleer-server-app/src/test/java/com/cameleer/server/app/security/JwtAuthenticationFilterOidcTest.java`:
```java
package com.cameleer3.server.app.security;
package com.cameleer.server.app.security;
import com.cameleer3.server.core.agent.AgentRegistryService;
import com.cameleer3.server.core.security.InvalidTokenException;
import com.cameleer3.server.core.security.JwtService;
import com.cameleer.server.core.agent.AgentRegistryService;
import com.cameleer.server.core.security.InvalidTokenException;
import com.cameleer.server.core.security.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import org.junit.jupiter.api.BeforeEach;
@@ -369,19 +369,19 @@ class JwtAuthenticationFilterOidcTest {
- [ ] **Step 2: Run test to verify it fails**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && ./mvnw test -pl cameleer3-server-app -Dtest=JwtAuthenticationFilterOidcTest -q`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && ./mvnw test -pl cameleer-server-app -Dtest=JwtAuthenticationFilterOidcTest -q`
Expected: FAIL — constructor doesn't accept 3 args
- [ ] **Step 3: Update JwtAuthenticationFilter**
Replace `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java` with:
Replace `cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java` with:
```java
package com.cameleer3.server.app.security;
package com.cameleer.server.app.security;
import com.cameleer3.server.core.agent.AgentRegistryService;
import com.cameleer3.server.core.security.JwtService;
import com.cameleer3.server.core.security.JwtService.JwtValidationResult;
import com.cameleer.server.core.agent.AgentRegistryService;
import com.cameleer.server.core.security.JwtService;
import com.cameleer.server.core.security.JwtService.JwtValidationResult;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@@ -508,13 +508,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
- [ ] **Step 4: Run tests**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && ./mvnw test -pl cameleer3-server-app -Dtest=JwtAuthenticationFilterOidcTest -q`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && ./mvnw test -pl cameleer-server-app -Dtest=JwtAuthenticationFilterOidcTest -q`
Expected: PASS (all 4 tests)
- [ ] **Step 5: Commit**
```bash
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/JwtAuthenticationFilterOidcTest.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java cameleer-server-app/src/test/java/com/cameleer/server/app/security/JwtAuthenticationFilterOidcTest.java
git commit -m "feat: add OIDC token fallback to JwtAuthenticationFilter"
```
@@ -523,8 +523,8 @@ git commit -m "feat: add OIDC token fallback to JwtAuthenticationFilter"
### Task 4: Wire OIDC decoder into SecurityConfig
**Files:**
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java`
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java`
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java`
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityBeanConfig.java`
- [ ] **Step 1: Add OIDC decoder bean creation to SecurityBeanConfig**
@@ -595,13 +595,13 @@ import org.springframework.security.oauth2.jwt.JwtDecoder;
- [ ] **Step 3: Run existing tests**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && ./mvnw test -pl cameleer3-server-app -q`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && ./mvnw test -pl cameleer-server-app -q`
Expected: All existing tests PASS (no OIDC env vars set, decoder is null, filter behaves as before)
- [ ] **Step 4: Commit**
```bash
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityBeanConfig.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityBeanConfig.java
git commit -m "feat: wire optional OIDC JwtDecoder into security filter chain"
```
@@ -1685,9 +1685,9 @@ In `docker-compose.yml`, remove these two labels from `cameleer-saas` (lines 122
- traefik.http.services.forwardauth.loadbalancer.server.port=8080
```
- [ ] **Step 2: Remove ForwardAuth middleware from cameleer3-server**
- [ ] **Step 2: Remove ForwardAuth middleware from cameleer-server**
In `docker-compose.yml`, remove the forward-auth middleware labels from `cameleer3-server` (lines 158-159):
In `docker-compose.yml`, remove the forward-auth middleware labels from `cameleer-server` (lines 158-159):
```yaml
- traefik.http.routers.observe.middlewares=forward-auth
@@ -1719,7 +1719,7 @@ In `cameleer-saas` environment, remove:
CAMELEER_AUTH_TOKEN: ${CAMELEER_AUTH_TOKEN:-default-bootstrap-token}
```
In `cameleer3-server` environment, add:
In `cameleer-server` environment, add:
```yaml
CAMELEER_OIDC_ISSUER_URI: ${LOGTO_ISSUER_URI:-http://logto:3001/oidc}
CAMELEER_OIDC_AUDIENCE: ${CAMELEER_OIDC_AUDIENCE:-https://api.cameleer.local}

View File

@@ -8,41 +8,41 @@
**Tech Stack:** Java 17, Spring Boot 3.4.3, PostgreSQL 16, Flyway, JUnit 5, Testcontainers, AssertJ
**Repo:** `C:\Users\Hendrik\Documents\projects\cameleer3-server`
**Repo:** `C:\Users\Hendrik\Documents\projects\cameleer-server`
---
## File Map
### New Files
- `cameleer3-server-app/src/main/resources/db/migration/V2__claim_mapping.sql`
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingRule.java`
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingRepository.java`
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingService.java`
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/AssignmentOrigin.java`
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresClaimMappingRepository.java`
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClaimMappingAdminController.java`
- `cameleer3-server-app/src/test/java/com/cameleer3/server/core/rbac/ClaimMappingServiceTest.java`
- `cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/ClaimMappingAdminControllerIT.java`
- `cameleer3-server-app/src/test/java/com/cameleer3/server/app/security/OidcOnlyModeIT.java`
- `cameleer-server-app/src/main/resources/db/migration/V2__claim_mapping.sql`
- `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingRule.java`
- `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingRepository.java`
- `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingService.java`
- `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/AssignmentOrigin.java`
- `cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresClaimMappingRepository.java`
- `cameleer-server-app/src/main/java/com/cameleer/server/app/controller/ClaimMappingAdminController.java`
- `cameleer-server-app/src/test/java/com/cameleer/server/core/rbac/ClaimMappingServiceTest.java`
- `cameleer-server-app/src/test/java/com/cameleer/server/app/controller/ClaimMappingAdminControllerIT.java`
- `cameleer-server-app/src/test/java/com/cameleer/server/app/security/OidcOnlyModeIT.java`
### Modified Files
- `cameleer3-server-app/src/main/resources/db/migration/V1__init.sql` — no changes (immutable)
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/rbac/RbacServiceImpl.java` — add origin-aware query methods
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresUserRepository.java` — add origin-aware queries
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java` — replace syncOidcRoles with claim mapping
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java` — disable internal token path in OIDC-only mode
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java` — conditional endpoint registration
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/UserAdminController.java` — disable in OIDC-only mode
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/AgentRegistryBeanConfig.java` — wire ClaimMappingService
- `cameleer3-server-app/src/main/resources/application.yml` — no new properties needed (OIDC config already exists)
- `cameleer-server-app/src/main/resources/db/migration/V1__init.sql` — no changes (immutable)
- `cameleer-server-app/src/main/java/com/cameleer/server/app/rbac/RbacServiceImpl.java` — add origin-aware query methods
- `cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresUserRepository.java` — add origin-aware queries
- `cameleer-server-app/src/main/java/com/cameleer/server/app/security/OidcAuthController.java` — replace syncOidcRoles with claim mapping
- `cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java` — disable internal token path in OIDC-only mode
- `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java` — conditional endpoint registration
- `cameleer-server-app/src/main/java/com/cameleer/server/app/controller/UserAdminController.java` — disable in OIDC-only mode
- `cameleer-server-app/src/main/java/com/cameleer/server/app/config/AgentRegistryBeanConfig.java` — wire ClaimMappingService
- `cameleer-server-app/src/main/resources/application.yml` — no new properties needed (OIDC config already exists)
---
### Task 1: Database Migration — Add Origin Tracking and Claim Mapping Rules
**Files:**
- Create: `cameleer3-server-app/src/main/resources/db/migration/V2__claim_mapping.sql`
- Create: `cameleer-server-app/src/main/resources/db/migration/V2__claim_mapping.sql`
- [ ] **Step 1: Write the migration**
@@ -90,14 +90,14 @@ CREATE INDEX idx_user_groups_origin ON user_groups(user_id, origin);
- [ ] **Step 2: Run migration to verify**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn flyway:migrate -pl cameleer3-server-app -Dflyway.url=jdbc:postgresql://localhost:5432/cameleer3 -Dflyway.user=cameleer -Dflyway.password=cameleer_dev`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn flyway:migrate -pl cameleer-server-app -Dflyway.url=jdbc:postgresql://localhost:5432/cameleer -Dflyway.user=cameleer -Dflyway.password=cameleer_dev`
If no local PostgreSQL, verify syntax by running the existing test suite which uses Testcontainers.
- [ ] **Step 3: Commit**
```bash
git add cameleer3-server-app/src/main/resources/db/migration/V2__claim_mapping.sql
git add cameleer-server-app/src/main/resources/db/migration/V2__claim_mapping.sql
git commit -m "feat: add claim mapping rules table and origin tracking to RBAC assignments"
```
@@ -106,14 +106,14 @@ git commit -m "feat: add claim mapping rules table and origin tracking to RBAC a
### Task 2: Core Domain — ClaimMappingRule, AssignmentOrigin, Repository Interface
**Files:**
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/AssignmentOrigin.java`
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingRule.java`
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingRepository.java`
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/AssignmentOrigin.java`
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingRule.java`
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingRepository.java`
- [ ] **Step 1: Create AssignmentOrigin enum**
```java
package com.cameleer3.server.core.rbac;
package com.cameleer.server.core.rbac;
public enum AssignmentOrigin {
direct, managed
@@ -123,7 +123,7 @@ public enum AssignmentOrigin {
- [ ] **Step 2: Create ClaimMappingRule record**
```java
package com.cameleer3.server.core.rbac;
package com.cameleer.server.core.rbac;
import java.time.Instant;
import java.util.UUID;
@@ -146,7 +146,7 @@ public record ClaimMappingRule(
- [ ] **Step 3: Create ClaimMappingRepository interface**
```java
package com.cameleer3.server.core.rbac;
package com.cameleer.server.core.rbac;
import java.util.List;
import java.util.Optional;
@@ -164,9 +164,9 @@ public interface ClaimMappingRepository {
- [ ] **Step 4: Commit**
```bash
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/AssignmentOrigin.java
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingRule.java
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingRepository.java
git add cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/AssignmentOrigin.java
git add cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingRule.java
git add cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingRepository.java
git commit -m "feat: add ClaimMappingRule domain model and repository interface"
```
@@ -175,13 +175,13 @@ git commit -m "feat: add ClaimMappingRule domain model and repository interface"
### Task 3: Core Domain — ClaimMappingService
**Files:**
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingService.java`
- Create: `cameleer3-server-app/src/test/java/com/cameleer3/server/core/rbac/ClaimMappingServiceTest.java`
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingService.java`
- Create: `cameleer-server-app/src/test/java/com/cameleer/server/core/rbac/ClaimMappingServiceTest.java`
- [ ] **Step 1: Write tests for ClaimMappingService**
```java
package com.cameleer3.server.core.rbac;
package com.cameleer.server.core.rbac;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -300,13 +300,13 @@ class ClaimMappingServiceTest {
- [ ] **Step 2: Run tests to verify they fail**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest=ClaimMappingServiceTest -Dsurefire.failIfNoSpecifiedTests=false`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app -Dtest=ClaimMappingServiceTest -Dsurefire.failIfNoSpecifiedTests=false`
Expected: Compilation error — ClaimMappingService does not exist yet.
- [ ] **Step 3: Implement ClaimMappingService**
```java
package com.cameleer3.server.core.rbac;
package com.cameleer.server.core.rbac;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -377,14 +377,14 @@ public class ClaimMappingService {
- [ ] **Step 4: Run tests to verify they pass**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest=ClaimMappingServiceTest`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app -Dtest=ClaimMappingServiceTest`
Expected: All 7 tests PASS.
- [ ] **Step 5: Commit**
```bash
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/ClaimMappingService.java
git add cameleer3-server-app/src/test/java/com/cameleer3/server/core/rbac/ClaimMappingServiceTest.java
git add cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/ClaimMappingService.java
git add cameleer-server-app/src/test/java/com/cameleer/server/core/rbac/ClaimMappingServiceTest.java
git commit -m "feat: implement ClaimMappingService with equals/contains/regex matching"
```
@@ -393,15 +393,15 @@ git commit -m "feat: implement ClaimMappingService with equals/contains/regex ma
### Task 4: PostgreSQL Repository — ClaimMappingRepository Implementation
**Files:**
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresClaimMappingRepository.java`
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresClaimMappingRepository.java`
- [ ] **Step 1: Implement PostgresClaimMappingRepository**
```java
package com.cameleer3.server.app.storage;
package com.cameleer.server.app.storage;
import com.cameleer3.server.core.rbac.ClaimMappingRepository;
import com.cameleer3.server.core.rbac.ClaimMappingRule;
import com.cameleer.server.core.rbac.ClaimMappingRepository;
import com.cameleer.server.core.rbac.ClaimMappingRule;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;
@@ -479,7 +479,7 @@ public class PostgresClaimMappingRepository implements ClaimMappingRepository {
- [ ] **Step 2: Wire the bean in AgentRegistryBeanConfig (or a new RbacBeanConfig)**
Add to `cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/AgentRegistryBeanConfig.java` (or create a new `RbacBeanConfig.java`):
Add to `cameleer-server-app/src/main/java/com/cameleer/server/app/config/AgentRegistryBeanConfig.java` (or create a new `RbacBeanConfig.java`):
```java
@Bean
@@ -496,8 +496,8 @@ public ClaimMappingService claimMappingService() {
- [ ] **Step 3: Commit**
```bash
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresClaimMappingRepository.java
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/AgentRegistryBeanConfig.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresClaimMappingRepository.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/config/AgentRegistryBeanConfig.java
git commit -m "feat: implement PostgresClaimMappingRepository and wire beans"
```
@@ -506,11 +506,11 @@ git commit -m "feat: implement PostgresClaimMappingRepository and wire beans"
### Task 5: Modify RbacServiceImpl — Origin-Aware Assignments
**Files:**
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/rbac/RbacServiceImpl.java`
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/rbac/RbacServiceImpl.java`
- [ ] **Step 1: Add managed assignment methods to RbacService interface**
In `cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/RbacService.java`, add:
In `cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/RbacService.java`, add:
```java
void clearManagedAssignments(String userId);
@@ -592,14 +592,14 @@ public List<RoleSummary> getDirectRolesForUser(String userId) {
- [ ] **Step 5: Run existing tests**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app`
Expected: All existing tests still pass (migration adds columns with defaults).
- [ ] **Step 6: Commit**
```bash
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/rbac/RbacService.java
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/rbac/RbacServiceImpl.java
git add cameleer-server-core/src/main/java/com/cameleer/server/core/rbac/RbacService.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/rbac/RbacServiceImpl.java
git commit -m "feat: add origin-aware managed/direct assignment methods to RbacService"
```
@@ -608,7 +608,7 @@ git commit -m "feat: add origin-aware managed/direct assignment methods to RbacS
### Task 6: Modify OidcAuthController — Replace syncOidcRoles with Claim Mapping
**Files:**
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java`
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/OidcAuthController.java`
- [ ] **Step 1: Inject ClaimMappingService and ClaimMappingRepository**
@@ -676,13 +676,13 @@ Note: `extractAllClaims` needs to be added to `OidcTokenExchanger` — it return
- [ ] **Step 4: Run existing tests**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app`
Expected: PASS (OIDC tests may need adjustment if they test syncOidcRoles directly).
- [ ] **Step 5: Commit**
```bash
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/security/OidcAuthController.java
git commit -m "feat: replace syncOidcRoles with claim mapping evaluation on OIDC login"
```
@@ -691,8 +691,8 @@ git commit -m "feat: replace syncOidcRoles with claim mapping evaluation on OIDC
### Task 7: OIDC-Only Mode — Disable Local Auth When OIDC Configured
**Files:**
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java`
- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java`
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java`
- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java`
- [ ] **Step 1: Add isOidcEnabled() helper to SecurityConfig**
@@ -760,15 +760,15 @@ public ResponseEntity<?> resetPassword(@PathVariable String userId, @RequestBody
- [ ] **Step 5: Run full test suite**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app`
Expected: PASS.
- [ ] **Step 6: Commit**
```bash
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/SecurityConfig.java
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/JwtAuthenticationFilter.java
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/UserAdminController.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/security/JwtAuthenticationFilter.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/controller/UserAdminController.java
git commit -m "feat: disable local auth when OIDC is configured (resource server mode)"
```
@@ -777,15 +777,15 @@ git commit -m "feat: disable local auth when OIDC is configured (resource server
### Task 8: Claim Mapping Admin Controller
**Files:**
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClaimMappingAdminController.java`
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/controller/ClaimMappingAdminController.java`
- [ ] **Step 1: Implement the controller**
```java
package com.cameleer3.server.app.controller;
package com.cameleer.server.app.controller;
import com.cameleer3.server.core.rbac.ClaimMappingRepository;
import com.cameleer3.server.core.rbac.ClaimMappingRule;
import com.cameleer.server.core.rbac.ClaimMappingRepository;
import com.cameleer.server.core.rbac.ClaimMappingRule;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
@@ -867,13 +867,13 @@ In `SecurityConfig.filterChain()`, the `/api/v1/admin/**` path already requires
- [ ] **Step 3: Run full test suite**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app`
Expected: PASS.
- [ ] **Step 4: Commit**
```bash
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ClaimMappingAdminController.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/controller/ClaimMappingAdminController.java
git commit -m "feat: add ClaimMappingAdminController for CRUD on mapping rules"
```
@@ -882,14 +882,14 @@ git commit -m "feat: add ClaimMappingAdminController for CRUD on mapping rules"
### Task 9: Integration Test — Claim Mapping End-to-End
**Files:**
- Create: `cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/ClaimMappingAdminControllerIT.java`
- Create: `cameleer-server-app/src/test/java/com/cameleer/server/app/controller/ClaimMappingAdminControllerIT.java`
- [ ] **Step 1: Write integration test**
```java
package com.cameleer3.server.app.controller;
package com.cameleer.server.app.controller;
import com.cameleer3.server.app.AbstractPostgresIT;
import com.cameleer.server.app.AbstractPostgresIT;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
@@ -954,13 +954,13 @@ class ClaimMappingAdminControllerIT extends AbstractPostgresIT {
- [ ] **Step 2: Run integration tests**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest=ClaimMappingAdminControllerIT`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app -Dtest=ClaimMappingAdminControllerIT`
Expected: PASS.
- [ ] **Step 3: Commit**
```bash
git add cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/ClaimMappingAdminControllerIT.java
git add cameleer-server-app/src/test/java/com/cameleer/server/app/controller/ClaimMappingAdminControllerIT.java
git commit -m "test: add integration tests for claim mapping admin API"
```
@@ -970,12 +970,12 @@ git commit -m "test: add integration tests for claim mapping admin API"
- [ ] **Step 1: Run all tests**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn clean verify`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn clean verify`
Expected: All tests PASS. Build succeeds.
- [ ] **Step 2: Verify migration applies cleanly on fresh database**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest=AbstractPostgresIT`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app -Dtest=AbstractPostgresIT`
Expected: Testcontainers starts fresh PostgreSQL, Flyway applies V1 + V2, context loads.
- [ ] **Step 3: Commit any remaining fixes**

View File

@@ -8,37 +8,37 @@
**Tech Stack:** Java 17, Spring Boot 3.4.3, Ed25519 (JDK built-in), Nimbus JOSE JWT, JUnit 5, AssertJ
**Repo:** `C:\Users\Hendrik\Documents\projects\cameleer3-server`
**Repo:** `C:\Users\Hendrik\Documents\projects\cameleer-server`
---
## File Map
### New Files
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseInfo.java`
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseValidator.java`
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseGate.java`
- `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/Feature.java`
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/LicenseBeanConfig.java`
- `cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LicenseAdminController.java`
- `cameleer3-server-app/src/test/java/com/cameleer3/server/core/license/LicenseValidatorTest.java`
- `cameleer3-server-app/src/test/java/com/cameleer3/server/core/license/LicenseGateTest.java`
- `cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseInfo.java`
- `cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseValidator.java`
- `cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseGate.java`
- `cameleer-server-core/src/main/java/com/cameleer/server/core/license/Feature.java`
- `cameleer-server-app/src/main/java/com/cameleer/server/app/config/LicenseBeanConfig.java`
- `cameleer-server-app/src/main/java/com/cameleer/server/app/controller/LicenseAdminController.java`
- `cameleer-server-app/src/test/java/com/cameleer/server/core/license/LicenseValidatorTest.java`
- `cameleer-server-app/src/test/java/com/cameleer/server/core/license/LicenseGateTest.java`
### Modified Files
- `cameleer3-server-app/src/main/resources/application.yml` — add license config properties
- `cameleer-server-app/src/main/resources/application.yml` — add license config properties
---
### Task 1: Core Domain — LicenseInfo, Feature Enum
**Files:**
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/Feature.java`
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseInfo.java`
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/license/Feature.java`
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseInfo.java`
- [ ] **Step 1: Create Feature enum**
```java
package com.cameleer3.server.core.license;
package com.cameleer.server.core.license;
public enum Feature {
topology,
@@ -52,7 +52,7 @@ public enum Feature {
- [ ] **Step 2: Create LicenseInfo record**
```java
package com.cameleer3.server.core.license;
package com.cameleer.server.core.license;
import java.time.Instant;
import java.util.Map;
@@ -87,8 +87,8 @@ public record LicenseInfo(
- [ ] **Step 3: Commit**
```bash
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/Feature.java
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseInfo.java
git add cameleer-server-core/src/main/java/com/cameleer/server/core/license/Feature.java
git add cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseInfo.java
git commit -m "feat: add LicenseInfo and Feature domain model"
```
@@ -97,13 +97,13 @@ git commit -m "feat: add LicenseInfo and Feature domain model"
### Task 2: LicenseValidator — Ed25519 JWT Verification
**Files:**
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseValidator.java`
- Create: `cameleer3-server-app/src/test/java/com/cameleer3/server/core/license/LicenseValidatorTest.java`
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseValidator.java`
- Create: `cameleer-server-app/src/test/java/com/cameleer/server/core/license/LicenseValidatorTest.java`
- [ ] **Step 1: Write tests**
```java
package com.cameleer3.server.core.license;
package com.cameleer.server.core.license;
import org.junit.jupiter.api.Test;
@@ -194,13 +194,13 @@ class LicenseValidatorTest {
- [ ] **Step 2: Run tests to verify they fail**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest=LicenseValidatorTest -Dsurefire.failIfNoSpecifiedTests=false`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app -Dtest=LicenseValidatorTest -Dsurefire.failIfNoSpecifiedTests=false`
Expected: Compilation error — LicenseValidator does not exist.
- [ ] **Step 3: Implement LicenseValidator**
```java
package com.cameleer3.server.core.license;
package com.cameleer.server.core.license;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -298,14 +298,14 @@ public class LicenseValidator {
- [ ] **Step 4: Run tests**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest=LicenseValidatorTest`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app -Dtest=LicenseValidatorTest`
Expected: All 3 tests PASS.
- [ ] **Step 5: Commit**
```bash
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseValidator.java
git add cameleer3-server-app/src/test/java/com/cameleer3/server/core/license/LicenseValidatorTest.java
git add cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseValidator.java
git add cameleer-server-app/src/test/java/com/cameleer/server/core/license/LicenseValidatorTest.java
git commit -m "feat: implement LicenseValidator with Ed25519 signature verification"
```
@@ -314,13 +314,13 @@ git commit -m "feat: implement LicenseValidator with Ed25519 signature verificat
### Task 3: LicenseGate — Feature Check Service
**Files:**
- Create: `cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseGate.java`
- Create: `cameleer3-server-app/src/test/java/com/cameleer3/server/core/license/LicenseGateTest.java`
- Create: `cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseGate.java`
- Create: `cameleer-server-app/src/test/java/com/cameleer/server/core/license/LicenseGateTest.java`
- [ ] **Step 1: Write tests**
```java
package com.cameleer3.server.core.license;
package com.cameleer.server.core.license;
import org.junit.jupiter.api.Test;
@@ -366,7 +366,7 @@ class LicenseGateTest {
- [ ] **Step 2: Implement LicenseGate**
```java
package com.cameleer3.server.core.license;
package com.cameleer.server.core.license;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -405,14 +405,14 @@ public class LicenseGate {
- [ ] **Step 3: Run tests**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app -Dtest=LicenseGateTest`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app -Dtest=LicenseGateTest`
Expected: PASS.
- [ ] **Step 4: Commit**
```bash
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/license/LicenseGate.java
git add cameleer3-server-app/src/test/java/com/cameleer3/server/core/license/LicenseGateTest.java
git add cameleer-server-core/src/main/java/com/cameleer/server/core/license/LicenseGate.java
git add cameleer-server-app/src/test/java/com/cameleer/server/core/license/LicenseGateTest.java
git commit -m "feat: implement LicenseGate for feature checking"
```
@@ -421,8 +421,8 @@ git commit -m "feat: implement LicenseGate for feature checking"
### Task 4: License Loading — Bean Config and Startup
**Files:**
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/LicenseBeanConfig.java`
- Modify: `cameleer3-server-app/src/main/resources/application.yml`
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/config/LicenseBeanConfig.java`
- Modify: `cameleer-server-app/src/main/resources/application.yml`
- [ ] **Step 1: Add license config properties to application.yml**
@@ -436,11 +436,11 @@ license:
- [ ] **Step 2: Implement LicenseBeanConfig**
```java
package com.cameleer3.server.app.config;
package com.cameleer.server.app.config;
import com.cameleer3.server.core.license.LicenseGate;
import com.cameleer3.server.core.license.LicenseInfo;
import com.cameleer3.server.core.license.LicenseValidator;
import com.cameleer.server.core.license.LicenseGate;
import com.cameleer.server.core.license.LicenseInfo;
import com.cameleer.server.core.license.LicenseValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
@@ -509,8 +509,8 @@ public class LicenseBeanConfig {
- [ ] **Step 3: Commit**
```bash
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/LicenseBeanConfig.java
git add cameleer3-server-app/src/main/resources/application.yml
git add cameleer-server-app/src/main/java/com/cameleer/server/app/config/LicenseBeanConfig.java
git add cameleer-server-app/src/main/resources/application.yml
git commit -m "feat: add license loading at startup from env var or file"
```
@@ -519,16 +519,16 @@ git commit -m "feat: add license loading at startup from env var or file"
### Task 5: License Admin API — Runtime License Update
**Files:**
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LicenseAdminController.java`
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/controller/LicenseAdminController.java`
- [ ] **Step 1: Implement controller**
```java
package com.cameleer3.server.app.controller;
package com.cameleer.server.app.controller;
import com.cameleer3.server.core.license.LicenseGate;
import com.cameleer3.server.core.license.LicenseInfo;
import com.cameleer3.server.core.license.LicenseValidator;
import com.cameleer.server.core.license.LicenseGate;
import com.cameleer.server.core.license.LicenseInfo;
import com.cameleer.server.core.license.LicenseValidator;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Value;
@@ -581,13 +581,13 @@ public class LicenseAdminController {
- [ ] **Step 2: Run full test suite**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn clean verify`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn clean verify`
Expected: PASS.
- [ ] **Step 3: Commit**
```bash
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/LicenseAdminController.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/controller/LicenseAdminController.java
git commit -m "feat: add license admin API for runtime license updates"
```
@@ -611,5 +611,5 @@ public ResponseEntity<?> listDebugSessions() {
- [ ] **Step 2: Final verification**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn clean verify`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn clean verify`
Expected: All tests PASS.

View File

@@ -1,6 +1,6 @@
# Plan 3: Runtime Management in the Server
> **Status: COMPLETED** — Verified 2026-04-09. All runtime management fully ported to cameleer3-server with enhancements beyond the original plan.
> **Status: COMPLETED** — Verified 2026-04-09. All runtime management fully ported to cameleer-server with enhancements beyond the original plan.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
@@ -10,7 +10,7 @@
**Tech Stack:** Java 17, Spring Boot 3.4.3, docker-java (zerodep transport), PostgreSQL 16, Flyway, JUnit 5, Testcontainers
**Repo:** `C:\Users\Hendrik\Documents\projects\cameleer3-server`
**Repo:** `C:\Users\Hendrik\Documents\projects\cameleer-server`
**Source reference:** Code ported from `C:\Users\Hendrik\Documents\projects\cameleer-saas` (environment, app, deployment, runtime packages)
@@ -18,10 +18,10 @@
## File Map
### New Files — Core Module (`cameleer3-server-core`)
### New Files — Core Module (`cameleer-server-core`)
```
src/main/java/com/cameleer3/server/core/runtime/
src/main/java/com/cameleer/server/core/runtime/
├── Environment.java Record: id, slug, displayName, status, createdAt
├── EnvironmentStatus.java Enum: ACTIVE, SUSPENDED
├── EnvironmentRepository.java Interface: CRUD + findBySlug
@@ -42,10 +42,10 @@ src/main/java/com/cameleer3/server/core/runtime/
└── RoutingMode.java Enum: path, subdomain
```
### New Files — App Module (`cameleer3-server-app`)
### New Files — App Module (`cameleer-server-app`)
```
src/main/java/com/cameleer3/server/app/runtime/
src/main/java/com/cameleer/server/app/runtime/
├── DockerRuntimeOrchestrator.java Docker implementation using docker-java
├── DisabledRuntimeOrchestrator.java No-op implementation (observability-only mode)
├── RuntimeOrchestratorAutoConfig.java @Configuration: auto-detects Docker vs K8s vs disabled
@@ -53,13 +53,13 @@ src/main/java/com/cameleer3/server/app/runtime/
├── JarStorageService.java File-system JAR storage with versioning
└── ContainerLogCollector.java Collects Docker container stdout/stderr
src/main/java/com/cameleer3/server/app/storage/
src/main/java/com/cameleer/server/app/storage/
├── PostgresEnvironmentRepository.java
├── PostgresAppRepository.java
├── PostgresAppVersionRepository.java
└── PostgresDeploymentRepository.java
src/main/java/com/cameleer3/server/app/controller/
src/main/java/com/cameleer/server/app/controller/
├── EnvironmentAdminController.java CRUD endpoints under /api/v1/admin/environments
├── AppController.java App + version CRUD + JAR upload
└── DeploymentController.java Deploy, stop, restart, promote, logs
@@ -70,7 +70,7 @@ src/main/resources/db/migration/
### Modified Files
- `pom.xml` (parent) — add docker-java dependency
- `cameleer3-server-app/pom.xml` — add docker-java dependency
- `cameleer-server-app/pom.xml` — add docker-java dependency
- `application.yml` — add runtime config properties
---
@@ -78,7 +78,7 @@ src/main/resources/db/migration/
### Task 1: Add docker-java Dependency
**Files:**
- Modify: `cameleer3-server-app/pom.xml`
- Modify: `cameleer-server-app/pom.xml`
- [x] **Step 1: Add docker-java dependency**
@@ -97,13 +97,13 @@ src/main/resources/db/migration/
- [x] **Step 2: Verify build**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn compile -pl cameleer3-server-app`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn compile -pl cameleer-server-app`
Expected: BUILD SUCCESS.
- [x] **Step 3: Commit**
```bash
git add cameleer3-server-app/pom.xml
git add cameleer-server-app/pom.xml
git commit -m "chore: add docker-java dependency for runtime orchestration"
```
@@ -112,7 +112,7 @@ git commit -m "chore: add docker-java dependency for runtime orchestration"
### Task 2: Database Migration — Runtime Management Tables
**Files:**
- Create: `cameleer3-server-app/src/main/resources/db/migration/V3__runtime_management.sql`
- Create: `cameleer-server-app/src/main/resources/db/migration/V3__runtime_management.sql`
- [x] **Step 1: Write migration**
@@ -176,7 +176,7 @@ INSERT INTO environments (slug, display_name) VALUES ('default', 'Default');
- [x] **Step 2: Commit**
```bash
git add cameleer3-server-app/src/main/resources/db/migration/V3__runtime_management.sql
git add cameleer-server-app/src/main/resources/db/migration/V3__runtime_management.sql
git commit -m "feat: add runtime management database schema (environments, apps, versions, deployments)"
```
@@ -185,36 +185,36 @@ git commit -m "feat: add runtime management database schema (environments, apps,
### Task 3: Core Domain — Environment, App, AppVersion, Deployment Records
**Files:**
- Create all records in `cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/`
- Create all records in `cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/`
- [x] **Step 1: Create all domain records**
```java
// Environment.java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
import java.time.Instant;
import java.util.UUID;
public record Environment(UUID id, String slug, String displayName, EnvironmentStatus status, Instant createdAt) {}
// EnvironmentStatus.java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
public enum EnvironmentStatus { ACTIVE, SUSPENDED }
// App.java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
import java.time.Instant;
import java.util.UUID;
public record App(UUID id, UUID environmentId, String slug, String displayName, Instant createdAt) {}
// AppVersion.java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
import java.time.Instant;
import java.util.UUID;
public record AppVersion(UUID id, UUID appId, int version, String jarPath, String jarChecksum,
String jarFilename, Long jarSizeBytes, Instant uploadedAt) {}
// Deployment.java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
import java.time.Instant;
import java.util.UUID;
public record Deployment(UUID id, UUID appId, UUID appVersionId, UUID environmentId,
@@ -227,18 +227,18 @@ public record Deployment(UUID id, UUID appId, UUID appVersionId, UUID environmen
}
// DeploymentStatus.java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
public enum DeploymentStatus { STARTING, RUNNING, FAILED, STOPPED }
// RoutingMode.java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
public enum RoutingMode { path, subdomain }
```
- [x] **Step 2: Commit**
```bash
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/
git add cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/
git commit -m "feat: add runtime management domain records"
```
@@ -253,7 +253,7 @@ git commit -m "feat: add runtime management domain records"
```java
// EnvironmentRepository.java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
import java.util.*;
public interface EnvironmentRepository {
List<Environment> findAll();
@@ -266,7 +266,7 @@ public interface EnvironmentRepository {
}
// AppRepository.java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
import java.util.*;
public interface AppRepository {
List<App> findByEnvironmentId(UUID environmentId);
@@ -277,7 +277,7 @@ public interface AppRepository {
}
// AppVersionRepository.java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
import java.util.*;
public interface AppVersionRepository {
List<AppVersion> findByAppId(UUID appId);
@@ -287,7 +287,7 @@ public interface AppVersionRepository {
}
// DeploymentRepository.java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
import java.util.*;
public interface DeploymentRepository {
List<Deployment> findByAppId(UUID appId);
@@ -305,7 +305,7 @@ public interface DeploymentRepository {
```java
// RuntimeOrchestrator.java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
import java.util.stream.Stream;
@@ -319,7 +319,7 @@ public interface RuntimeOrchestrator {
}
// ContainerRequest.java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
import java.util.Map;
public record ContainerRequest(
String containerName,
@@ -334,7 +334,7 @@ public record ContainerRequest(
) {}
// ContainerStatus.java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
public record ContainerStatus(String state, boolean running, int exitCode, String error) {
public static ContainerStatus notFound() {
return new ContainerStatus("not_found", false, -1, "Container not found");
@@ -345,7 +345,7 @@ public record ContainerStatus(String state, boolean running, int exitCode, Strin
- [x] **Step 3: Commit**
```bash
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/
git add cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/
git commit -m "feat: add runtime repository interfaces and RuntimeOrchestrator"
```
@@ -359,7 +359,7 @@ git commit -m "feat: add runtime repository interfaces and RuntimeOrchestrator"
- [x] **Step 1: Create EnvironmentService**
```java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
import java.util.List;
import java.util.UUID;
@@ -395,7 +395,7 @@ public class EnvironmentService {
- [x] **Step 2: Create AppService**
```java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -478,7 +478,7 @@ public class AppService {
- [x] **Step 3: Create DeploymentService**
```java
package com.cameleer3.server.core.runtime;
package com.cameleer.server.core.runtime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -536,7 +536,7 @@ public class DeploymentService {
- [x] **Step 4: Commit**
```bash
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/runtime/
git add cameleer-server-core/src/main/java/com/cameleer/server/core/runtime/
git commit -m "feat: add EnvironmentService, AppService, DeploymentService"
```
@@ -598,14 +598,14 @@ public class RuntimeBeanConfig {
- [x] **Step 3: Run tests**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn test -pl cameleer3-server-app`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn test -pl cameleer-server-app`
Expected: PASS (Flyway applies V3 migration, context loads).
- [x] **Step 4: Commit**
```bash
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/Postgres*Repository.java
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/RuntimeBeanConfig.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/storage/Postgres*Repository.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/config/RuntimeBeanConfig.java
git commit -m "feat: implement PostgreSQL repositories for runtime management"
```
@@ -614,16 +614,16 @@ git commit -m "feat: implement PostgreSQL repositories for runtime management"
### Task 7: Docker Runtime Orchestrator
**Files:**
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DockerRuntimeOrchestrator.java`
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DisabledRuntimeOrchestrator.java`
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/RuntimeOrchestratorAutoConfig.java`
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/DockerRuntimeOrchestrator.java`
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/DisabledRuntimeOrchestrator.java`
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/RuntimeOrchestratorAutoConfig.java`
- [x] **Step 1: Implement DisabledRuntimeOrchestrator**
```java
package com.cameleer3.server.app.runtime;
package com.cameleer.server.app.runtime;
import com.cameleer3.server.core.runtime.*;
import com.cameleer.server.core.runtime.*;
import java.util.stream.Stream;
public class DisabledRuntimeOrchestrator implements RuntimeOrchestrator {
@@ -685,9 +685,9 @@ public String startContainer(ContainerRequest request) {
- [x] **Step 3: Implement RuntimeOrchestratorAutoConfig**
```java
package com.cameleer3.server.app.runtime;
package com.cameleer.server.app.runtime;
import com.cameleer3.server.core.runtime.RuntimeOrchestrator;
import com.cameleer.server.core.runtime.RuntimeOrchestrator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
@@ -718,7 +718,7 @@ public class RuntimeOrchestratorAutoConfig {
- [x] **Step 4: Commit**
```bash
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/
git add cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/
git commit -m "feat: implement DockerRuntimeOrchestrator with volume-mount JAR deployment"
```
@@ -727,14 +727,14 @@ git commit -m "feat: implement DockerRuntimeOrchestrator with volume-mount JAR d
### Task 8: DeploymentExecutor — Async Deployment Pipeline
**Files:**
- Create: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java`
- Create: `cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/DeploymentExecutor.java`
- [x] **Step 1: Implement async deployment pipeline**
```java
package com.cameleer3.server.app.runtime;
package com.cameleer.server.app.runtime;
import com.cameleer3.server.core.runtime.*;
import com.cameleer.server.core.runtime.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
@@ -841,7 +841,7 @@ public TaskExecutor deploymentTaskExecutor() {
- [x] **Step 3: Commit**
```bash
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/runtime/DeploymentExecutor.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/runtime/DeploymentExecutor.java
git commit -m "feat: implement async DeploymentExecutor pipeline"
```
@@ -907,9 +907,9 @@ Add to `SecurityConfig.filterChain()`:
- [x] **Step 5: Commit**
```bash
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/EnvironmentAdminController.java
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AppController.java
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/DeploymentController.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/controller/EnvironmentAdminController.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AppController.java
git add cameleer-server-app/src/main/java/com/cameleer/server/app/controller/DeploymentController.java
git commit -m "feat: add REST controllers for environment, app, and deployment management"
```
@@ -918,7 +918,7 @@ git commit -m "feat: add REST controllers for environment, app, and deployment m
### Task 10: Configuration and Application Properties
**Files:**
- Modify: `cameleer3-server-app/src/main/resources/application.yml`
- Modify: `cameleer-server-app/src/main/resources/application.yml`
- [x] **Step 1: Add runtime config properties**
@@ -939,13 +939,13 @@ cameleer:
- [x] **Step 2: Run full test suite**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn clean verify`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn clean verify`
Expected: PASS.
- [x] **Step 3: Commit**
```bash
git add cameleer3-server-app/src/main/resources/application.yml
git add cameleer-server-app/src/main/resources/application.yml
git commit -m "feat: add runtime management configuration properties"
```
@@ -968,7 +968,7 @@ Test deployment creation (with `DisabledRuntimeOrchestrator` — verifies the de
- [x] **Step 4: Commit**
```bash
git add cameleer3-server-app/src/test/java/com/cameleer3/server/app/controller/
git add cameleer-server-app/src/test/java/com/cameleer/server/app/controller/
git commit -m "test: add integration tests for runtime management API"
```
@@ -978,7 +978,7 @@ git commit -m "test: add integration tests for runtime management API"
- [x] **Step 1: Run full build**
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer3-server && mvn clean verify`
Run: `cd /c/Users/Hendrik/Documents/projects/cameleer-server && mvn clean verify`
Expected: All tests PASS.
- [x] **Step 2: Verify schema applies cleanly**

View File

@@ -10,7 +10,7 @@
**Repo:** `C:\Users\Hendrik\Documents\projects\cameleer-saas`
**Prerequisite:** Plans 1-3 must be implemented in cameleer3-server first.
**Prerequisite:** Plans 1-3 must be implemented in cameleer-server first.
---
@@ -212,7 +212,7 @@ git commit -m "feat: remove migrated environment/app/deployment/runtime code fro
```sql
-- V010__drop_migrated_tables.sql
-- Drop tables that have been migrated to cameleer3-server
-- Drop tables that have been migrated to cameleer-server
DROP TABLE IF EXISTS deployments CASCADE;
DROP TABLE IF EXISTS apps CASCADE;
@@ -242,7 +242,7 @@ group_add:
- "0"
```
The Docker socket mount now belongs to the `cameleer3-server` service instead.
The Docker socket mount now belongs to the `cameleer-server` service instead.
- [ ] **Step 2: Remove docker-java dependency from pom.xml**
@@ -328,7 +328,7 @@ git commit -m "feat: expand ServerApiClient with license push and health check m
- [ ] **Step 1: Create integration contract document**
Create `docs/SAAS-INTEGRATION.md` in the cameleer3-server repo documenting:
Create `docs/SAAS-INTEGRATION.md` in the cameleer-server repo documenting:
- Which server API endpoints the SaaS calls
- Required auth (M2M token with `server:admin` scope)
- License injection mechanism (`POST /api/v1/admin/license`)
@@ -339,7 +339,7 @@ Create `docs/SAAS-INTEGRATION.md` in the cameleer3-server repo documenting:
- [ ] **Step 2: Commit**
```bash
cd /c/Users/Hendrik/Documents/projects/cameleer3-server
cd /c/Users/Hendrik/Documents/projects/cameleer-server
git add docs/SAAS-INTEGRATION.md
git commit -m "docs: add SaaS integration contract documentation"
```

View File

@@ -2,7 +2,7 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Redesign the cameleer-saas platform from a read-only viewer into a vendor management plane that provisions per-tenant cameleer3-server instances, with vendor CRUD and customer self-service.
**Goal:** Redesign the cameleer-saas platform from a read-only viewer into a vendor management plane that provisions per-tenant cameleer-server instances, with vendor CRUD and customer self-service.
**Architecture:** Two-persona split (vendor console at `/vendor/*`, tenant portal at `/tenant/*`). Pluggable `TenantProvisioner` interface with Docker implementation. Backend orchestrates provisioning + Logto + licensing in a single create-tenant flow. Frontend adapts sidebar/routes by persona.
@@ -410,13 +410,13 @@ Append to `application.yml`:
```yaml
cameleer:
provisioning:
server-image: ${CAMELEER_SERVER_IMAGE:gitea.siegeln.net/cameleer/cameleer3-server:latest}
server-ui-image: ${CAMELEER_SERVER_UI_IMAGE:gitea.siegeln.net/cameleer/cameleer3-server-ui:latest}
server-image: ${CAMELEER_SERVER_IMAGE:gitea.siegeln.net/cameleer/cameleer-server:latest}
server-ui-image: ${CAMELEER_SERVER_UI_IMAGE:gitea.siegeln.net/cameleer/cameleer-server-ui:latest}
network-name: ${CAMELEER_NETWORK:cameleer-saas_cameleer}
traefik-network: ${CAMELEER_TRAEFIK_NETWORK:cameleer-traefik}
public-host: ${PUBLIC_HOST:localhost}
public-protocol: ${PUBLIC_PROTOCOL:https}
datasource-url: ${CAMELEER_SERVER_DB_URL:jdbc:postgresql://postgres:5432/cameleer3}
datasource-url: ${CAMELEER_SERVER_DB_URL:jdbc:postgresql://postgres:5432/cameleer}
oidc-issuer-uri: ${PUBLIC_PROTOCOL:https}://${PUBLIC_HOST:localhost}/oidc
oidc-jwk-set-uri: http://logto:3001/oidc/jwks
cors-origins: ${PUBLIC_PROTOCOL:https}://${PUBLIC_HOST:localhost}
@@ -1877,7 +1877,7 @@ import { LayoutDashboard, ShieldCheck, Server, Users, Settings, KeyRound, Buildi
import { useAuth } from '../auth/useAuth';
import { useScopes } from '../auth/useScopes';
import { useOrgStore } from '../auth/useOrganization';
import logo from '@cameleer/design-system/assets/cameleer3-logo.svg';
import logo from '@cameleer/design-system/assets/cameleer-logo.svg';
export function Layout() {
const navigate = useNavigate();
@@ -2940,8 +2940,8 @@ This gives the SaaS container access to the Docker daemon for provisioning.
Add to the `cameleer-saas` environment section:
```yaml
CAMELEER_SERVER_IMAGE: gitea.siegeln.net/cameleer/cameleer3-server:${VERSION:-latest}
CAMELEER_SERVER_UI_IMAGE: gitea.siegeln.net/cameleer/cameleer3-server-ui:${VERSION:-latest}
CAMELEER_SERVER_IMAGE: gitea.siegeln.net/cameleer/cameleer-server:${VERSION:-latest}
CAMELEER_SERVER_UI_IMAGE: gitea.siegeln.net/cameleer/cameleer-server-ui:${VERSION:-latest}
CAMELEER_NETWORK: cameleer-saas_cameleer
CAMELEER_TRAEFIK_NETWORK: cameleer-traefik
```

View File

@@ -581,7 +581,7 @@ In `ui/sign-in/src/SignInPage.tsx`, find the logo text (line ~61):
// BEFORE:
<div className={styles.logo}>
<img src={cameleerLogo} alt="" className={styles.logoImg} />
cameleer3
cameleer
</div>
// AFTER:

View File

@@ -0,0 +1,210 @@
# Fleet Health at a Glance Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add agent count, environment count, and agent limit columns to the vendor tenant list so the vendor can see fleet utilization at a glance.
**Architecture:** Extend the existing `VendorTenantSummary` record with three int fields. The list endpoint fetches counts from each active tenant's server via existing M2M API methods (`getAgentCount`, `getEnvironmentCount`), parallelized with `CompletableFuture`. Frontend adds two columns (Agents, Envs) to the DataTable.
**Tech Stack:** Java 21, Spring Boot, CompletableFuture, React, TypeScript, @cameleer/design-system DataTable
---
### Task 1: Extend backend — VendorTenantSummary + parallel fetch
**Files:**
- Modify: `src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantController.java`
- [ ] **Step 1: Extend the VendorTenantSummary record**
In `VendorTenantController.java`, replace the record at lines 39-48:
```java
public record VendorTenantSummary(
UUID id,
String name,
String slug,
String tier,
String status,
String serverState,
String licenseExpiry,
String provisionError,
int agentCount,
int environmentCount,
int agentLimit
) {}
```
- [ ] **Step 2: Update the listAll() endpoint to fetch counts in parallel**
Replace the `listAll()` method at lines 60-77:
```java
@GetMapping
public ResponseEntity<List<VendorTenantSummary>> listAll() {
var tenants = vendorTenantService.listAll();
// Parallel health fetch for active tenants
var futures = tenants.stream().map(tenant -> java.util.concurrent.CompletableFuture.supplyAsync(() -> {
ServerStatus status = vendorTenantService.getServerStatus(tenant);
String licenseExpiry = vendorTenantService
.getLicenseForTenant(tenant.getId())
.map(l -> l.getExpiresAt() != null ? l.getExpiresAt().toString() : null)
.orElse(null);
int agentCount = 0;
int environmentCount = 0;
int agentLimit = -1;
String endpoint = tenant.getServerEndpoint();
boolean isActive = "ACTIVE".equals(tenant.getStatus().name());
if (isActive && endpoint != null && !endpoint.isBlank() && "RUNNING".equals(status.state().name())) {
var serverApi = vendorTenantService.getServerApiClient();
agentCount = serverApi.getAgentCount(endpoint);
environmentCount = serverApi.getEnvironmentCount(endpoint);
}
var license = vendorTenantService.getLicenseForTenant(tenant.getId());
if (license.isPresent() && license.get().getLimits() != null) {
var limits = license.get().getLimits();
if (limits.containsKey("agents")) {
agentLimit = ((Number) limits.get("agents")).intValue();
}
}
return new VendorTenantSummary(
tenant.getId(), tenant.getName(), tenant.getSlug(),
tenant.getTier().name(), tenant.getStatus().name(),
status.state().name(), licenseExpiry, tenant.getProvisionError(),
agentCount, environmentCount, agentLimit
);
})).toList();
List<VendorTenantSummary> summaries = futures.stream()
.map(java.util.concurrent.CompletableFuture::join)
.toList();
return ResponseEntity.ok(summaries);
}
```
- [ ] **Step 3: Expose ServerApiClient from VendorTenantService**
Add a getter in `src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java`:
```java
public ServerApiClient getServerApiClient() {
return serverApiClient;
}
```
(The `serverApiClient` field already exists in VendorTenantService — check around line 30.)
- [ ] **Step 4: Verify compilation**
Run: `./mvnw compile -pl . -q`
Expected: BUILD SUCCESS
- [ ] **Step 5: Commit**
```bash
git add src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantController.java \
src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java
git commit -m "feat: add agent/env counts to vendor tenant list endpoint"
```
---
### Task 2: Update frontend types and columns
**Files:**
- Modify: `ui/src/types/api.ts`
- Modify: `ui/src/pages/vendor/VendorTenantsPage.tsx`
- [ ] **Step 1: Add fields to VendorTenantSummary TypeScript type**
In `ui/src/types/api.ts`, update the `VendorTenantSummary` interface:
```typescript
export interface VendorTenantSummary {
id: string;
name: string;
slug: string;
tier: string;
status: string;
serverState: string;
licenseExpiry: string | null;
provisionError: string | null;
agentCount: number;
environmentCount: number;
agentLimit: number;
}
```
- [ ] **Step 2: Add Agents and Envs columns to VendorTenantsPage**
In `ui/src/pages/vendor/VendorTenantsPage.tsx`, add a helper function after `statusColor`:
```typescript
function formatUsage(used: number, limit: number): string {
return limit < 0 ? `${used} / ∞` : `${used} / ${limit}`;
}
```
Then add two column entries in the `columns` array, after the `serverState` column (after line 54) and before the `licenseExpiry` column:
```typescript
{
key: 'agentCount',
header: 'Agents',
render: (_v, row) => (
<span style={{ fontFamily: 'monospace', fontSize: '0.875rem' }}>
{formatUsage(row.agentCount, row.agentLimit)}
</span>
),
},
{
key: 'environmentCount',
header: 'Envs',
render: (_v, row) => (
<span style={{ fontFamily: 'monospace', fontSize: '0.875rem' }}>
{row.environmentCount}
</span>
),
},
```
- [ ] **Step 3: Build the UI**
Run: `cd ui && npm run build`
Expected: Build succeeds with no errors.
- [ ] **Step 4: Commit**
```bash
git add ui/src/types/api.ts ui/src/pages/vendor/VendorTenantsPage.tsx
git commit -m "feat: show agent/env counts in vendor tenant list"
```
---
### Task 3: Verify end-to-end
- [ ] **Step 1: Run backend tests**
Run: `./mvnw test -pl . -q`
Expected: All tests pass. (Existing tests use mocks, the new parallel fetch doesn't break them since it only affects the controller's list mapping.)
- [ ] **Step 2: Verify in browser**
Navigate to the vendor tenant list. Confirm:
- "Agents" column shows "0 / ∞" (or actual count if agents are connected)
- "Envs" column shows "1" (or actual count)
- PROVISIONING/SUSPENDED tenants show "0" for both
- 30s auto-refresh still works
- [ ] **Step 3: Final commit and push**
```bash
git push
```

View File

@@ -1572,8 +1572,8 @@ VENDOR_PASS=${VENDOR_PASS:-}
DOCKER_SOCKET=${DOCKER_SOCKET}
# Provisioning images
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=${REGISTRY}/cameleer3-server:${VERSION}
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=${REGISTRY}/cameleer3-server-ui:${VERSION}
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=${REGISTRY}/cameleer-server:${VERSION}
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=${REGISTRY}/cameleer-server-ui:${VERSION}
EOF
log_info "Generated .env"
@@ -1793,8 +1793,8 @@ EOF
CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
CAMELEER_SAAS_PROVISIONING_NETWORKNAME: ${COMPOSE_PROJECT_NAME:-cameleer-saas}_cameleer
CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK: cameleer-traefik
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERIMAGE:-gitea.siegeln.net/cameleer/cameleer3-server:latest}
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE:-gitea.siegeln.net/cameleer/cameleer3-server-ui:latest}
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERIMAGE:-gitea.siegeln.net/cameleer/cameleer-server:latest}
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE:-gitea.siegeln.net/cameleer/cameleer-server-ui:latest}
labels:
- traefik.enable=true
- traefik.http.routers.saas.rule=PathPrefix(`/platform`)
@@ -2109,7 +2109,7 @@ EOF
| `logto` | OIDC identity provider + bootstrap |
| `cameleer-saas` | SaaS platform (Spring Boot + React) |
Per-tenant `cameleer3-server` and `cameleer3-server-ui` containers are provisioned dynamically when tenants are created.
Per-tenant `cameleer-server` and `cameleer-server-ui` containers are provisioned dynamically when tenants are created.
## Networking
@@ -2656,7 +2656,7 @@ Tasks 8-16 ────── can run in parallel with Phase 1
## Follow-up (out of scope)
- Bake `docker/server-ui-entrypoint.sh` into the `cameleer3-server-ui` image (separate repo)
- Bake `docker/server-ui-entrypoint.sh` into the `cameleer-server-ui` image (separate repo)
- Set up `install.cameleer.io` distribution endpoint
- Create release automation (tag → publish installer scripts to distribution endpoint)
- Add `docker-compose.dev.yml` overlay generation for the installer's expert mode

View File

@@ -0,0 +1,961 @@
# Externalize Docker Compose Templates — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace inline docker-compose generation in installer scripts with static template files, reducing duplication and enabling user customization.
**Architecture:** Static YAML templates in `installer/templates/` are copied to the install directory. The installer writes `.env` (including `COMPOSE_FILE` to select which templates are active) and runs `docker compose up -d`. Conditional features (TLS, monitoring) are handled via compose file layering and `.env` variables instead of heredoc injection.
**Tech Stack:** Docker Compose v2, YAML, Bash, PowerShell
**Spec:** `docs/superpowers/specs/2026-04-15-externalize-compose-templates-design.md`
---
### Task 1: Create `docker-compose.yml` (infra base template)
**Files:**
- Create: `installer/templates/docker-compose.yml`
This is the shared infrastructure base — always loaded regardless of deployment mode.
- [ ] **Step 1: Create the infra base template**
```yaml
# Cameleer Infrastructure
# Shared base — always loaded. Mode-specific services in separate compose files.
services:
cameleer-traefik:
image: ${TRAEFIK_IMAGE:-gitea.siegeln.net/cameleer/cameleer-traefik}:${VERSION:-latest}
restart: unless-stopped
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
- "${LOGTO_CONSOLE_BIND:-127.0.0.1}:${LOGTO_CONSOLE_PORT:-3002}:3002"
environment:
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
CERT_FILE: ${CERT_FILE:-}
KEY_FILE: ${KEY_FILE:-}
CA_FILE: ${CA_FILE:-}
volumes:
- cameleer-certs:/certs
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro
labels:
- "prometheus.io/scrape=true"
- "prometheus.io/port=8082"
- "prometheus.io/path=/metrics"
networks:
- cameleer
- cameleer-traefik
- monitoring
cameleer-postgres:
image: ${POSTGRES_IMAGE:-gitea.siegeln.net/cameleer/cameleer-postgres}:${VERSION:-latest}
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-cameleer_saas}
POSTGRES_USER: ${POSTGRES_USER:-cameleer}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in .env}
volumes:
- cameleer-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d $${POSTGRES_DB:-cameleer_saas}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- cameleer
- monitoring
cameleer-clickhouse:
image: ${CLICKHOUSE_IMAGE:-gitea.siegeln.net/cameleer/cameleer-clickhouse}:${VERSION:-latest}
restart: unless-stopped
environment:
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?CLICKHOUSE_PASSWORD must be set in .env}
volumes:
- cameleer-chdata:/var/lib/clickhouse
healthcheck:
test: ["CMD-SHELL", "clickhouse-client --password $${CLICKHOUSE_PASSWORD} --query 'SELECT 1'"]
interval: 10s
timeout: 5s
retries: 3
labels:
- "prometheus.io/scrape=true"
- "prometheus.io/port=9363"
- "prometheus.io/path=/metrics"
networks:
- cameleer
- monitoring
volumes:
cameleer-pgdata:
cameleer-chdata:
cameleer-certs:
networks:
cameleer:
driver: bridge
cameleer-traefik:
name: cameleer-traefik
driver: bridge
monitoring:
name: cameleer-monitoring-noop
```
Key changes from the generated version:
- Logto console port always present with `LOGTO_CONSOLE_BIND` controlling exposure
- Prometheus labels unconditional on traefik and clickhouse
- `monitoring` network defined as local noop bridge
- All services join `monitoring` network
- `POSTGRES_DB` uses `${POSTGRES_DB:-cameleer_saas}` (parameterized — standalone overrides via `.env`)
- Password variables use `:?` fail-if-unset
Note: The SaaS mode uses `cameleer-postgres` (custom multi-DB image) while standalone uses `postgres:16-alpine`. The `POSTGRES_IMAGE` variable already handles this — the infra base uses `${POSTGRES_IMAGE:-...}` and standalone `.env` sets `POSTGRES_IMAGE=postgres:16-alpine`.
- [ ] **Step 2: Verify YAML is valid**
Run: `python -c "import yaml; yaml.safe_load(open('installer/templates/docker-compose.yml'))"`
Expected: No output (valid YAML). If python/yaml not available, use `docker compose -f installer/templates/docker-compose.yml config --quiet` (will fail on unset vars, but validates structure).
- [ ] **Step 3: Commit**
```bash
git add installer/templates/docker-compose.yml
git commit -m "feat(installer): add infra base docker-compose template"
```
---
### Task 2: Create `docker-compose.saas.yml` (SaaS mode template)
**Files:**
- Create: `installer/templates/docker-compose.saas.yml`
SaaS-specific services: Logto identity provider and cameleer-saas management plane.
- [ ] **Step 1: Create the SaaS template**
```yaml
# Cameleer SaaS — Logto + management plane
# Loaded in SaaS deployment mode
services:
cameleer-logto:
image: ${LOGTO_IMAGE:-gitea.siegeln.net/cameleer/cameleer-logto}:${VERSION:-latest}
restart: unless-stopped
depends_on:
cameleer-postgres:
condition: service_healthy
environment:
DB_URL: postgres://${POSTGRES_USER:-cameleer}:${POSTGRES_PASSWORD}@cameleer-postgres:5432/logto
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
ADMIN_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}
TRUST_PROXY_HEADER: 1
NODE_TLS_REJECT_UNAUTHORIZED: "${NODE_TLS_REJECT:-0}"
LOGTO_ENDPOINT: http://cameleer-logto:3001
LOGTO_ADMIN_ENDPOINT: http://cameleer-logto:3002
LOGTO_PUBLIC_ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
PUBLIC_HOST: ${PUBLIC_HOST:-localhost}
PUBLIC_PROTOCOL: ${PUBLIC_PROTOCOL:-https}
PG_HOST: cameleer-postgres
PG_USER: ${POSTGRES_USER:-cameleer}
PG_PASSWORD: ${POSTGRES_PASSWORD}
PG_DB_SAAS: cameleer_saas
SAAS_ADMIN_USER: ${SAAS_ADMIN_USER:-admin}
SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:?SAAS_ADMIN_PASS must be set in .env}
healthcheck:
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3001/oidc/.well-known/openid-configuration', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\" && test -f /data/logto-bootstrap.json"]
interval: 10s
timeout: 5s
retries: 60
start_period: 30s
labels:
- traefik.enable=true
- traefik.http.routers.cameleer-logto.rule=PathPrefix(`/`)
- traefik.http.routers.cameleer-logto.priority=1
- traefik.http.routers.cameleer-logto.entrypoints=websecure
- traefik.http.routers.cameleer-logto.tls=true
- traefik.http.routers.cameleer-logto.service=cameleer-logto
- traefik.http.routers.cameleer-logto.middlewares=cameleer-logto-cors
- "traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowOriginList=${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}:${LOGTO_CONSOLE_PORT:-3002}"
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowMethods=GET,POST,PUT,PATCH,DELETE,OPTIONS
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowHeaders=Authorization,Content-Type
- traefik.http.middlewares.cameleer-logto-cors.headers.accessControlAllowCredentials=true
- traefik.http.services.cameleer-logto.loadbalancer.server.port=3001
- traefik.http.routers.cameleer-logto-console.rule=PathPrefix(`/`)
- traefik.http.routers.cameleer-logto-console.entrypoints=admin-console
- traefik.http.routers.cameleer-logto-console.tls=true
- traefik.http.routers.cameleer-logto-console.service=cameleer-logto-console
- traefik.http.services.cameleer-logto-console.loadbalancer.server.port=3002
volumes:
- cameleer-bootstrapdata:/data
networks:
- cameleer
- monitoring
cameleer-saas:
image: ${CAMELEER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-saas}:${VERSION:-latest}
restart: unless-stopped
depends_on:
cameleer-logto:
condition: service_healthy
environment:
# SaaS database
SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/cameleer_saas
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
# Identity (Logto)
CAMELEER_SAAS_IDENTITY_LOGTOENDPOINT: http://cameleer-logto:3001
CAMELEER_SAAS_IDENTITY_LOGTOPUBLICENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
# Provisioning — passed to per-tenant server containers
CAMELEER_SAAS_PROVISIONING_PUBLICHOST: ${PUBLIC_HOST:-localhost}
CAMELEER_SAAS_PROVISIONING_PUBLICPROTOCOL: ${PUBLIC_PROTOCOL:-https}
CAMELEER_SAAS_PROVISIONING_NETWORKNAME: ${COMPOSE_PROJECT_NAME:-cameleer-saas}_cameleer
CAMELEER_SAAS_PROVISIONING_TRAEFIKNETWORK: cameleer-traefik
CAMELEER_SAAS_PROVISIONING_DATASOURCEUSERNAME: ${POSTGRES_USER:-cameleer}
CAMELEER_SAAS_PROVISIONING_DATASOURCEPASSWORD: ${POSTGRES_PASSWORD}
CAMELEER_SAAS_PROVISIONING_CLICKHOUSEPASSWORD: ${CLICKHOUSE_PASSWORD}
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERIMAGE:-gitea.siegeln.net/cameleer/cameleer-server:latest}
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE: ${CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE:-gitea.siegeln.net/cameleer/cameleer-server-ui:latest}
labels:
- traefik.enable=true
- traefik.http.routers.saas.rule=PathPrefix(`/platform`)
- traefik.http.routers.saas.entrypoints=websecure
- traefik.http.routers.saas.tls=true
- traefik.http.services.saas.loadbalancer.server.port=8080
- "prometheus.io/scrape=true"
- "prometheus.io/port=8080"
- "prometheus.io/path=/platform/actuator/prometheus"
volumes:
- cameleer-bootstrapdata:/data/bootstrap:ro
- cameleer-certs:/certs
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
group_add:
- "${DOCKER_GID:-0}"
networks:
- cameleer
- monitoring
volumes:
cameleer-bootstrapdata:
networks:
monitoring:
name: cameleer-monitoring-noop
```
Key changes:
- Logto console traefik labels always included (harmless when port is localhost-only)
- Prometheus labels on cameleer-saas always included
- `DOCKER_GID` read from `.env` via `${DOCKER_GID:-0}` instead of inline `stat`
- Both services join `monitoring` network
- `monitoring` network redefined as noop bridge (compose merges with base definition)
- [ ] **Step 2: Commit**
```bash
git add installer/templates/docker-compose.saas.yml
git commit -m "feat(installer): add SaaS docker-compose template"
```
---
### Task 3: Create `docker-compose.server.yml` (standalone mode template)
**Files:**
- Create: `installer/templates/docker-compose.server.yml`
- Create: `installer/templates/traefik-dynamic.yml`
Standalone-specific services: cameleer-server + server-ui. Also includes the traefik dynamic config that standalone mode needs (overrides the baked-in SaaS redirect).
- [ ] **Step 1: Create the standalone template**
```yaml
# Cameleer Server (standalone)
# Loaded in standalone deployment mode
services:
cameleer-traefik:
volumes:
- ./traefik-dynamic.yml:/etc/traefik/dynamic.yml:ro
cameleer-postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-cameleer}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-cameleer} -d $${POSTGRES_DB:-cameleer}"]
cameleer-server:
image: ${SERVER_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server}:${VERSION:-latest}
container_name: cameleer-server
restart: unless-stopped
depends_on:
cameleer-postgres:
condition: service_healthy
environment:
CAMELEER_SERVER_TENANT_ID: default
SPRING_DATASOURCE_URL: jdbc:postgresql://cameleer-postgres:5432/${POSTGRES_DB:-cameleer}?currentSchema=tenant_default
SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-cameleer}
SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
CAMELEER_SERVER_CLICKHOUSE_URL: jdbc:clickhouse://cameleer-clickhouse:8123/cameleer
CAMELEER_SERVER_CLICKHOUSE_USERNAME: default
CAMELEER_SERVER_CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN: ${BOOTSTRAP_TOKEN:?BOOTSTRAP_TOKEN must be set in .env}
CAMELEER_SERVER_SECURITY_UIUSER: ${SERVER_ADMIN_USER:-admin}
CAMELEER_SERVER_SECURITY_UIPASSWORD: ${SERVER_ADMIN_PASS:?SERVER_ADMIN_PASS must be set in .env}
CAMELEER_SERVER_SECURITY_CORSALLOWEDORIGINS: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
CAMELEER_SERVER_RUNTIME_ENABLED: "true"
CAMELEER_SERVER_RUNTIME_SERVERURL: http://cameleer-server:8081
CAMELEER_SERVER_RUNTIME_ROUTINGDOMAIN: ${PUBLIC_HOST:-localhost}
CAMELEER_SERVER_RUNTIME_ROUTINGMODE: path
CAMELEER_SERVER_RUNTIME_JARSTORAGEPATH: /data/jars
CAMELEER_SERVER_RUNTIME_DOCKERNETWORK: cameleer-apps
CAMELEER_SERVER_RUNTIME_JARDOCKERVOLUME: cameleer-jars
CAMELEER_SERVER_RUNTIME_BASEIMAGE: gitea.siegeln.net/cameleer/cameleer-runtime-base:${VERSION:-latest}
labels:
- traefik.enable=true
- traefik.http.routers.server-api.rule=PathPrefix(`/api`)
- traefik.http.routers.server-api.entrypoints=websecure
- traefik.http.routers.server-api.tls=true
- traefik.http.services.server-api.loadbalancer.server.port=8081
- traefik.docker.network=cameleer-traefik
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:8081/api/v1/health || exit 1"]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
volumes:
- jars:/data/jars
- cameleer-certs:/certs:ro
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock
group_add:
- "${DOCKER_GID:-0}"
networks:
- cameleer
- cameleer-traefik
- cameleer-apps
- monitoring
cameleer-server-ui:
image: ${SERVER_UI_IMAGE:-gitea.siegeln.net/cameleer/cameleer-server-ui}:${VERSION:-latest}
restart: unless-stopped
depends_on:
cameleer-server:
condition: service_healthy
environment:
CAMELEER_API_URL: http://cameleer-server:8081
BASE_PATH: ""
labels:
- traefik.enable=true
- traefik.http.routers.ui.rule=PathPrefix(`/`)
- traefik.http.routers.ui.priority=1
- traefik.http.routers.ui.entrypoints=websecure
- traefik.http.routers.ui.tls=true
- traefik.http.services.ui.loadbalancer.server.port=80
- traefik.docker.network=cameleer-traefik
networks:
- cameleer-traefik
- monitoring
volumes:
jars:
networks:
cameleer-apps:
name: cameleer-apps
driver: bridge
monitoring:
name: cameleer-monitoring-noop
```
Key design decisions:
- `cameleer-traefik` and `cameleer-postgres` entries are **overrides** — compose merges them with the base. The postgres image switches to `postgres:16-alpine` and the healthcheck uses `${POSTGRES_DB:-cameleer}` instead of hardcoded `cameleer_saas`. Traefik gets the `traefik-dynamic.yml` volume mount.
- `DOCKER_GID` from `.env` via `${DOCKER_GID:-0}`
- `BOOTSTRAP_TOKEN` uses `:?` fail-if-unset
- Both server and server-ui join `monitoring` network
- [ ] **Step 2: Create the traefik dynamic config template**
```yaml
tls:
stores:
default:
defaultCertificate:
certFile: /certs/cert.pem
keyFile: /certs/key.pem
```
This file is only relevant in standalone mode (overrides the baked-in SaaS `/` -> `/platform/` redirect in the traefik image).
- [ ] **Step 3: Commit**
```bash
git add installer/templates/docker-compose.server.yml installer/templates/traefik-dynamic.yml
git commit -m "feat(installer): add standalone docker-compose and traefik templates"
```
---
### Task 4: Create overlay templates (TLS + monitoring)
**Files:**
- Create: `installer/templates/docker-compose.tls.yml`
- Create: `installer/templates/docker-compose.monitoring.yml`
- [ ] **Step 1: Create the TLS overlay**
```yaml
# Custom TLS certificates overlay
# Adds user-supplied certificate volume to traefik
services:
cameleer-traefik:
volumes:
- ./certs:/user-certs:ro
```
- [ ] **Step 2: Create the monitoring overlay**
```yaml
# External monitoring network overlay
# Overrides the noop monitoring bridge with a real external network
networks:
monitoring:
external: true
name: ${MONITORING_NETWORK:?MONITORING_NETWORK must be set in .env}
```
This is the key to the monitoring pattern: the base compose files define `monitoring` as a local noop bridge and all services join it. When this overlay is included in `COMPOSE_FILE`, compose merges the network definition — overriding it to point at the real external monitoring network. No per-service entries needed.
- [ ] **Step 3: Commit**
```bash
git add installer/templates/docker-compose.tls.yml installer/templates/docker-compose.monitoring.yml
git commit -m "feat(installer): add TLS and monitoring overlay templates"
```
---
### Task 5: Create `.env.example`
**Files:**
- Create: `installer/templates/.env.example`
- [ ] **Step 1: Create the documented variable reference**
```bash
# Cameleer Configuration
# Copy this file to .env and fill in the values.
# The installer generates .env automatically — this file is for reference.
# ============================================================
# Compose file assembly (set by installer)
# ============================================================
# SaaS: docker-compose.yml:docker-compose.saas.yml
# Standalone: docker-compose.yml:docker-compose.server.yml
# Add :docker-compose.tls.yml for custom TLS certificates
# Add :docker-compose.monitoring.yml for external monitoring network
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml
# ============================================================
# Image version
# ============================================================
VERSION=latest
# ============================================================
# Public access
# ============================================================
PUBLIC_HOST=localhost
PUBLIC_PROTOCOL=https
# ============================================================
# Ports
# ============================================================
HTTP_PORT=80
HTTPS_PORT=443
# Set to 0.0.0.0 to expose Logto admin console externally (default: localhost only)
# LOGTO_CONSOLE_BIND=0.0.0.0
LOGTO_CONSOLE_PORT=3002
# ============================================================
# PostgreSQL
# ============================================================
POSTGRES_USER=cameleer
POSTGRES_PASSWORD=CHANGE_ME
# SaaS: cameleer_saas, Standalone: cameleer
POSTGRES_DB=cameleer_saas
# ============================================================
# ClickHouse
# ============================================================
CLICKHOUSE_PASSWORD=CHANGE_ME
# ============================================================
# Admin credentials (SaaS mode)
# ============================================================
SAAS_ADMIN_USER=admin
SAAS_ADMIN_PASS=CHANGE_ME
# ============================================================
# Admin credentials (standalone mode)
# ============================================================
# SERVER_ADMIN_USER=admin
# SERVER_ADMIN_PASS=CHANGE_ME
# BOOTSTRAP_TOKEN=CHANGE_ME
# ============================================================
# TLS
# ============================================================
# Set to 1 to reject unauthorized TLS certificates (production)
NODE_TLS_REJECT=0
# Custom TLS certificate paths (inside container, set by installer)
# CERT_FILE=/user-certs/cert.pem
# KEY_FILE=/user-certs/key.pem
# CA_FILE=/user-certs/ca.pem
# ============================================================
# Docker
# ============================================================
DOCKER_SOCKET=/var/run/docker.sock
# GID of the docker socket — detected by installer, used for container group_add
DOCKER_GID=0
# ============================================================
# Provisioning images (SaaS mode only)
# ============================================================
# CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer-server:latest
# CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer-server-ui:latest
# ============================================================
# Monitoring (optional)
# ============================================================
# External Docker network name for Prometheus scraping.
# Only needed when docker-compose.monitoring.yml is in COMPOSE_FILE.
# MONITORING_NETWORK=prometheus
```
- [ ] **Step 2: Commit**
```bash
git add installer/templates/.env.example
git commit -m "feat(installer): add .env.example with documented variables"
```
---
### Task 6: Update `install.sh` — replace compose generation with template copying
**Files:**
- Modify: `installer/install.sh:574-672` (generate_env_file — add COMPOSE_FILE and LOGTO_CONSOLE_BIND)
- Modify: `installer/install.sh:674-1135` (replace generate_compose_file + generate_compose_file_standalone with copy_templates)
- Modify: `installer/install.sh:1728-1731` (reinstall cleanup — delete template files)
- Modify: `installer/install.sh:1696-1710` (upgrade path — copy templates instead of generate)
- Modify: `installer/install.sh:1790-1791` (main — call copy_templates instead of generate_compose_file)
- [ ] **Step 1: Replace `generate_compose_file` and `generate_compose_file_standalone` with `copy_templates`**
Delete both functions (`generate_compose_file` at line 674 and `generate_compose_file_standalone` at line 934) and replace with:
```bash
copy_templates() {
local src
src="$(cd "$(dirname "$0")" && pwd)/templates"
# Base infra — always copied
cp "$src/docker-compose.yml" "$INSTALL_DIR/docker-compose.yml"
cp "$src/.env.example" "$INSTALL_DIR/.env.example"
# Mode-specific
if [ "$DEPLOYMENT_MODE" = "standalone" ]; then
cp "$src/docker-compose.server.yml" "$INSTALL_DIR/docker-compose.server.yml"
cp "$src/traefik-dynamic.yml" "$INSTALL_DIR/traefik-dynamic.yml"
else
cp "$src/docker-compose.saas.yml" "$INSTALL_DIR/docker-compose.saas.yml"
fi
# Optional overlays
if [ "$TLS_MODE" = "custom" ]; then
cp "$src/docker-compose.tls.yml" "$INSTALL_DIR/docker-compose.tls.yml"
fi
if [ -n "$MONITORING_NETWORK" ]; then
cp "$src/docker-compose.monitoring.yml" "$INSTALL_DIR/docker-compose.monitoring.yml"
fi
log_info "Copied docker-compose templates to $INSTALL_DIR"
}
```
- [ ] **Step 2: Update `generate_env_file` to include `COMPOSE_FILE`, `LOGTO_CONSOLE_BIND`, and `DOCKER_GID`**
In the standalone `.env` block (line 577-614), add after the `DOCKER_GID` line:
```bash
# Compose file assembly
COMPOSE_FILE=docker-compose.yml:docker-compose.server.yml$([ "$TLS_MODE" = "custom" ] && echo ":docker-compose.tls.yml")$([ -n "$MONITORING_NETWORK" ] && echo ":docker-compose.monitoring.yml")
EOF
```
In the SaaS `.env` block (line 617-668), add `LOGTO_CONSOLE_BIND` and `COMPOSE_FILE`. After the `LOGTO_CONSOLE_PORT` line:
```bash
LOGTO_CONSOLE_BIND=$([ "$LOGTO_CONSOLE_EXPOSED" = "true" ] && echo "0.0.0.0" || echo "127.0.0.1")
```
And at the end of the SaaS block, add the `COMPOSE_FILE` line:
```bash
# Compose file assembly
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml$([ "$TLS_MODE" = "custom" ] && echo ":docker-compose.tls.yml")$([ -n "$MONITORING_NETWORK" ] && echo ":docker-compose.monitoring.yml")
```
Also add the `MONITORING_NETWORK` variable to `.env` when set:
```bash
if [ -n "$MONITORING_NETWORK" ]; then
echo "" >> "$f"
echo "# Monitoring" >> "$f"
echo "MONITORING_NETWORK=${MONITORING_NETWORK}" >> "$f"
fi
```
- [ ] **Step 3: Update `main()` — replace `generate_compose_file` call with `copy_templates`**
At line 1791, change:
```bash
generate_compose_file
```
to:
```bash
copy_templates
```
- [ ] **Step 4: Update `handle_rerun` upgrade path**
At line 1703, change:
```bash
generate_compose_file
```
to:
```bash
copy_templates
```
- [ ] **Step 5: Update reinstall cleanup to remove template files**
At lines 1728-1731, update the `rm -f` list to include all possible template files:
```bash
rm -f "$INSTALL_DIR/.env" "$INSTALL_DIR/.env.bak" "$INSTALL_DIR/.env.example" \
"$INSTALL_DIR/docker-compose.yml" "$INSTALL_DIR/docker-compose.saas.yml" \
"$INSTALL_DIR/docker-compose.server.yml" "$INSTALL_DIR/docker-compose.tls.yml" \
"$INSTALL_DIR/docker-compose.monitoring.yml" "$INSTALL_DIR/traefik-dynamic.yml" \
"$INSTALL_DIR/cameleer.conf" "$INSTALL_DIR/credentials.txt" \
"$INSTALL_DIR/INSTALL.md"
```
- [ ] **Step 6: Commit**
```bash
git add installer/install.sh
git commit -m "refactor(installer): replace sh compose generation with template copying"
```
---
### Task 7: Update `install.ps1` — replace compose generation with template copying
**Files:**
- Modify: `installer/install.ps1:574-666` (Generate-EnvFile — add COMPOSE_FILE and LOGTO_CONSOLE_BIND)
- Modify: `installer/install.ps1:671-1105` (replace Generate-ComposeFile + Generate-ComposeFileStandalone with Copy-Templates)
- Modify: `installer/install.ps1:1706-1723` (upgrade path)
- Modify: `installer/install.ps1:1746` (reinstall cleanup)
- Modify: `installer/install.ps1:1797-1798` (Main — call Copy-Templates)
- [ ] **Step 1: Replace `Generate-ComposeFile` and `Generate-ComposeFileStandalone` with `Copy-Templates`**
Delete both functions and replace with:
```powershell
function Copy-Templates {
$c = $script:cfg
$src = Join-Path $PSScriptRoot 'templates'
# Base infra — always copied
Copy-Item (Join-Path $src 'docker-compose.yml') (Join-Path $c.InstallDir 'docker-compose.yml') -Force
Copy-Item (Join-Path $src '.env.example') (Join-Path $c.InstallDir '.env.example') -Force
# Mode-specific
if ($c.DeploymentMode -eq 'standalone') {
Copy-Item (Join-Path $src 'docker-compose.server.yml') (Join-Path $c.InstallDir 'docker-compose.server.yml') -Force
Copy-Item (Join-Path $src 'traefik-dynamic.yml') (Join-Path $c.InstallDir 'traefik-dynamic.yml') -Force
} else {
Copy-Item (Join-Path $src 'docker-compose.saas.yml') (Join-Path $c.InstallDir 'docker-compose.saas.yml') -Force
}
# Optional overlays
if ($c.TlsMode -eq 'custom') {
Copy-Item (Join-Path $src 'docker-compose.tls.yml') (Join-Path $c.InstallDir 'docker-compose.tls.yml') -Force
}
if ($c.MonitoringNetwork) {
Copy-Item (Join-Path $src 'docker-compose.monitoring.yml') (Join-Path $c.InstallDir 'docker-compose.monitoring.yml') -Force
}
Log-Info "Copied docker-compose templates to $($c.InstallDir)"
}
```
- [ ] **Step 2: Update `Generate-EnvFile` to include `COMPOSE_FILE`, `LOGTO_CONSOLE_BIND`, and `MONITORING_NETWORK`**
In the standalone `.env` content block, add after `DOCKER_GID`:
```powershell
$composeFile = 'docker-compose.yml:docker-compose.server.yml'
if ($c.TlsMode -eq 'custom') { $composeFile += ':docker-compose.tls.yml' }
if ($c.MonitoringNetwork) { $composeFile += ':docker-compose.monitoring.yml' }
```
Then append to `$content`:
```powershell
$content += "`n`n# Compose file assembly`nCOMPOSE_FILE=$composeFile"
if ($c.MonitoringNetwork) {
$content += "`n`n# Monitoring`nMONITORING_NETWORK=$($c.MonitoringNetwork)"
}
```
In the SaaS `.env` content block, add `LOGTO_CONSOLE_BIND` after `LOGTO_CONSOLE_PORT`:
```powershell
$consoleBind = if ($c.LogtoConsoleExposed -eq 'true') { '0.0.0.0' } else { '127.0.0.1' }
```
Add to the content string: `LOGTO_CONSOLE_BIND=$consoleBind`
Build `COMPOSE_FILE`:
```powershell
$composeFile = 'docker-compose.yml:docker-compose.saas.yml'
if ($c.TlsMode -eq 'custom') { $composeFile += ':docker-compose.tls.yml' }
if ($c.MonitoringNetwork) { $composeFile += ':docker-compose.monitoring.yml' }
```
And append to `$content`:
```powershell
$content += "`n`n# Compose file assembly`nCOMPOSE_FILE=$composeFile"
if ($c.MonitoringNetwork) {
$content += "`n`n# Monitoring`nMONITORING_NETWORK=$($c.MonitoringNetwork)"
}
```
- [ ] **Step 3: Update `Main` — replace `Generate-ComposeFile` call with `Copy-Templates`**
At line 1798, change:
```powershell
Generate-ComposeFile
```
to:
```powershell
Copy-Templates
```
- [ ] **Step 4: Update `Handle-Rerun` upgrade path**
At line 1716, change:
```powershell
Generate-ComposeFile
```
to:
```powershell
Copy-Templates
```
- [ ] **Step 5: Update reinstall cleanup to remove template files**
At line 1746, update the filename list:
```powershell
foreach ($fname in @('.env','.env.bak','.env.example','docker-compose.yml','docker-compose.saas.yml','docker-compose.server.yml','docker-compose.tls.yml','docker-compose.monitoring.yml','traefik-dynamic.yml','cameleer.conf','credentials.txt','INSTALL.md')) {
```
- [ ] **Step 6: Commit**
```bash
git add installer/install.ps1
git commit -m "refactor(installer): replace ps1 compose generation with template copying"
```
---
### Task 8: Update existing generated install and clean up
**Files:**
- Modify: `installer/cameleer/docker-compose.yml` (replace with template copy for dev environment)
- [ ] **Step 1: Remove the old generated docker-compose.yml from the cameleer/ directory**
The `installer/cameleer/` directory contains a previously generated install. The `docker-compose.yml` there is now stale — it was generated by the old inline method. Since this is a dev environment output, remove it (it will be recreated by running the installer with the new template approach).
```bash
git rm installer/cameleer/docker-compose.yml
```
- [ ] **Step 2: Add `installer/cameleer/` to `.gitignore` if not already there**
The install output directory should not be tracked. Check if `.gitignore` already covers it. If not, add:
```
installer/cameleer/
```
This prevents generated `.env`, `credentials.txt`, and compose files from being committed.
- [ ] **Step 3: Commit**
```bash
git add -A installer/cameleer/ .gitignore
git commit -m "chore(installer): remove generated install output, add to gitignore"
```
---
### Task 9: Verify the templates produce equivalent output
**Files:** (no changes — verification only)
- [ ] **Step 1: Compare template output against the old generated compose**
Create a temporary `.env` file and run `docker compose config` to render the resolved compose. Compare against the old generated output:
```bash
cd installer/cameleer
# Back up old generated file for comparison
cp docker-compose.yml docker-compose.old.yml 2>/dev/null || true
# Create a test .env that exercises the SaaS path
cat > /tmp/test-saas.env << 'EOF'
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml
VERSION=latest
PUBLIC_HOST=test.example.com
PUBLIC_PROTOCOL=https
HTTP_PORT=80
HTTPS_PORT=443
LOGTO_CONSOLE_PORT=3002
LOGTO_CONSOLE_BIND=0.0.0.0
POSTGRES_USER=cameleer
POSTGRES_PASSWORD=testpass
POSTGRES_DB=cameleer_saas
CLICKHOUSE_PASSWORD=testpass
SAAS_ADMIN_USER=admin
SAAS_ADMIN_PASS=testpass
NODE_TLS_REJECT=0
DOCKER_SOCKET=/var/run/docker.sock
DOCKER_GID=0
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer-server:latest
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer-server-ui:latest
EOF
# Render the new templates
cd ../templates
docker compose --env-file /tmp/test-saas.env config
```
Expected: A fully resolved compose with all 5 services (traefik, postgres, clickhouse, logto, saas), correct environment variables, and the monitoring noop network.
- [ ] **Step 2: Test standalone mode rendering**
```bash
cat > /tmp/test-standalone.env << 'EOF'
COMPOSE_FILE=docker-compose.yml:docker-compose.server.yml
VERSION=latest
PUBLIC_HOST=test.example.com
PUBLIC_PROTOCOL=https
HTTP_PORT=80
HTTPS_PORT=443
POSTGRES_IMAGE=postgres:16-alpine
POSTGRES_USER=cameleer
POSTGRES_PASSWORD=testpass
POSTGRES_DB=cameleer
CLICKHOUSE_PASSWORD=testpass
SERVER_ADMIN_USER=admin
SERVER_ADMIN_PASS=testpass
BOOTSTRAP_TOKEN=testtoken
DOCKER_SOCKET=/var/run/docker.sock
DOCKER_GID=0
EOF
cd ../templates
docker compose --env-file /tmp/test-standalone.env config
```
Expected: 5 services (traefik, postgres with `postgres:16-alpine` image, clickhouse, server, server-ui). Postgres `POSTGRES_DB` should be `cameleer`. Server should have all env vars resolved.
- [ ] **Step 3: Test with TLS + monitoring overlays**
```bash
cat > /tmp/test-full.env << 'EOF'
COMPOSE_FILE=docker-compose.yml:docker-compose.saas.yml:docker-compose.tls.yml:docker-compose.monitoring.yml
VERSION=latest
PUBLIC_HOST=test.example.com
PUBLIC_PROTOCOL=https
HTTP_PORT=80
HTTPS_PORT=443
LOGTO_CONSOLE_PORT=3002
LOGTO_CONSOLE_BIND=0.0.0.0
POSTGRES_USER=cameleer
POSTGRES_PASSWORD=testpass
POSTGRES_DB=cameleer_saas
CLICKHOUSE_PASSWORD=testpass
SAAS_ADMIN_USER=admin
SAAS_ADMIN_PASS=testpass
NODE_TLS_REJECT=0
DOCKER_SOCKET=/var/run/docker.sock
DOCKER_GID=0
MONITORING_NETWORK=prometheus
CAMELEER_SAAS_PROVISIONING_SERVERIMAGE=gitea.siegeln.net/cameleer/cameleer-server:latest
CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE=gitea.siegeln.net/cameleer/cameleer-server-ui:latest
EOF
cd ../templates
docker compose --env-file /tmp/test-full.env config
```
Expected: Same as SaaS mode but with `./certs:/user-certs:ro` volume on traefik and the `monitoring` network declared as `external: true` with name `prometheus`.
- [ ] **Step 4: Clean up temp files**
```bash
rm -f /tmp/test-saas.env /tmp/test-standalone.env /tmp/test-full.env
```
- [ ] **Step 5: Commit verification results as a note (optional)**
No code changes — this task is verification only. If all checks pass, proceed to the final commit.
---
### Task 10: Final commit — update CLAUDE.md deployment modes table
**Files:**
- Modify: `CLAUDE.md` (update Deployment Modes section to reference template files)
- [ ] **Step 1: Update the deployment modes documentation**
In the "Deployment Modes (installer)" section of CLAUDE.md, add a note about the template-based approach:
After the deployment modes table, add:
```markdown
The installer uses static docker-compose templates in `installer/templates/`. Templates are copied to the install directory and composed via `COMPOSE_FILE` in `.env`:
- `docker-compose.yml` — shared infrastructure (traefik, postgres, clickhouse)
- `docker-compose.saas.yml` — SaaS mode (logto, cameleer-saas)
- `docker-compose.server.yml` — standalone mode (server, server-ui)
- `docker-compose.tls.yml` — overlay: custom TLS cert volume
- `docker-compose.monitoring.yml` — overlay: external monitoring network
```
- [ ] **Step 2: Commit**
```bash
git add CLAUDE.md
git commit -m "docs: update CLAUDE.md with template-based installer architecture"
```

View File

@@ -0,0 +1,464 @@
# Per-Tenant PostgreSQL Isolation Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Give each tenant its own PostgreSQL user and schema so tenant servers can only access their own data at the database level.
**Architecture:** During provisioning, create a dedicated PG user (`tenant_<slug>`) with a matching schema. Pass per-tenant credentials and `currentSchema`/`ApplicationName` JDBC parameters to the server container. On delete, drop both schema and user. Existing tenants without `dbPassword` fall back to shared credentials for backwards compatibility.
**Tech Stack:** Java 21, Spring Boot 3.4, Flyway, PostgreSQL 16, Docker Java API
**Spec:** `docs/superpowers/specs/2026-04-15-per-tenant-pg-isolation-design.md`
---
### Task 1: Flyway Migration — add `db_password` column
**Files:**
- Create: `src/main/resources/db/migration/V015__add_tenant_db_password.sql`
- [ ] **Step 1: Create migration file**
```sql
ALTER TABLE tenants ADD COLUMN db_password VARCHAR(255);
```
- [ ] **Step 2: Verify migration applies**
Run: `mvn flyway:info -pl .` or start the app and check logs for `V015__add_tenant_db_password` in Flyway output.
- [ ] **Step 3: Commit**
```bash
git add src/main/resources/db/migration/V015__add_tenant_db_password.sql
git commit -m "feat: add db_password column to tenants table (V015)"
```
---
### Task 2: TenantEntity — add `dbPassword` field
**Files:**
- Modify: `src/main/java/net/siegeln/cameleer/saas/tenant/TenantEntity.java`
- [ ] **Step 1: Add field and accessors**
After the `provisionError` field (line 59), add:
```java
@Column(name = "db_password")
private String dbPassword;
```
After the `setProvisionError` method (line 102), add:
```java
public String getDbPassword() { return dbPassword; }
public void setDbPassword(String dbPassword) { this.dbPassword = dbPassword; }
```
- [ ] **Step 2: Commit**
```bash
git add src/main/java/net/siegeln/cameleer/saas/tenant/TenantEntity.java
git commit -m "feat: add dbPassword field to TenantEntity"
```
---
### Task 3: Create `TenantDatabaseService`
**Files:**
- Create: `src/main/java/net/siegeln/cameleer/saas/provisioning/TenantDatabaseService.java`
- [ ] **Step 1: Implement the service**
```java
package net.siegeln.cameleer.saas.provisioning;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
/**
* Creates and drops per-tenant PostgreSQL users and schemas
* on the shared cameleer database for DB-level tenant isolation.
*/
@Service
public class TenantDatabaseService {
private static final Logger log = LoggerFactory.getLogger(TenantDatabaseService.class);
private final ProvisioningProperties props;
public TenantDatabaseService(ProvisioningProperties props) {
this.props = props;
}
/**
* Create a dedicated PG user and schema for a tenant.
* Idempotent — skips if user/schema already exist.
*/
public void createTenantDatabase(String slug, String password) {
validateSlug(slug);
String url = props.datasourceUrl();
if (url == null || url.isBlank()) {
log.warn("No datasource URL configured — skipping tenant DB setup");
return;
}
String user = "tenant_" + slug;
String schema = "tenant_" + slug;
try (Connection conn = DriverManager.getConnection(url, props.datasourceUsername(), props.datasourcePassword());
Statement stmt = conn.createStatement()) {
// Create user if not exists
boolean userExists;
try (ResultSet rs = stmt.executeQuery(
"SELECT 1 FROM pg_roles WHERE rolname = '" + user + "'")) {
userExists = rs.next();
}
if (!userExists) {
stmt.execute("CREATE USER \"" + user + "\" WITH PASSWORD '" + escapePassword(password) + "'");
log.info("Created PostgreSQL user: {}", user);
} else {
// Update password on re-provision
stmt.execute("ALTER USER \"" + user + "\" WITH PASSWORD '" + escapePassword(password) + "'");
log.info("Updated password for existing PostgreSQL user: {}", user);
}
// Create schema if not exists
boolean schemaExists;
try (ResultSet rs = stmt.executeQuery(
"SELECT 1 FROM information_schema.schemata WHERE schema_name = '" + schema + "'")) {
schemaExists = rs.next();
}
if (!schemaExists) {
stmt.execute("CREATE SCHEMA \"" + schema + "\" AUTHORIZATION \"" + user + "\"");
log.info("Created PostgreSQL schema: {}", schema);
} else {
// Ensure ownership is correct
stmt.execute("ALTER SCHEMA \"" + schema + "\" OWNER TO \"" + user + "\"");
log.info("Schema {} already exists — ensured ownership", schema);
}
// Revoke access to public schema
stmt.execute("REVOKE ALL ON SCHEMA public FROM \"" + user + "\"");
} catch (Exception e) {
throw new RuntimeException("Failed to create tenant database for '" + slug + "': " + e.getMessage(), e);
}
}
/**
* Drop tenant schema (CASCADE) and user. Idempotent.
*/
public void dropTenantDatabase(String slug) {
validateSlug(slug);
String url = props.datasourceUrl();
if (url == null || url.isBlank()) {
log.warn("No datasource URL configured — skipping tenant DB cleanup");
return;
}
String user = "tenant_" + slug;
String schema = "tenant_" + slug;
try (Connection conn = DriverManager.getConnection(url, props.datasourceUsername(), props.datasourcePassword());
Statement stmt = conn.createStatement()) {
stmt.execute("DROP SCHEMA IF EXISTS \"" + schema + "\" CASCADE");
log.info("Dropped PostgreSQL schema: {}", schema);
stmt.execute("DROP USER IF EXISTS \"" + user + "\"");
log.info("Dropped PostgreSQL user: {}", user);
} catch (Exception e) {
log.warn("Failed to drop tenant database for '{}': {}", slug, e.getMessage());
}
}
private void validateSlug(String slug) {
if (slug == null || !slug.matches("^[a-z0-9-]+$")) {
throw new IllegalArgumentException("Invalid tenant slug: " + slug);
}
}
private String escapePassword(String password) {
return password.replace("'", "''");
}
}
```
- [ ] **Step 2: Commit**
```bash
git add src/main/java/net/siegeln/cameleer/saas/provisioning/TenantDatabaseService.java
git commit -m "feat: add TenantDatabaseService for per-tenant PG user+schema"
```
---
### Task 4: Add `dbPassword` to `TenantProvisionRequest`
**Files:**
- Modify: `src/main/java/net/siegeln/cameleer/saas/provisioning/TenantProvisionRequest.java`
- [ ] **Step 1: Add field to record**
Replace the entire record with:
```java
package net.siegeln.cameleer.saas.provisioning;
import java.util.UUID;
public record TenantProvisionRequest(
UUID tenantId,
String slug,
String tier,
String licenseToken,
String dbPassword
) {}
```
- [ ] **Step 2: Commit**
```bash
git add src/main/java/net/siegeln/cameleer/saas/provisioning/TenantProvisionRequest.java
git commit -m "feat: add dbPassword to TenantProvisionRequest"
```
---
### Task 5: Update `DockerTenantProvisioner` — per-tenant JDBC URL
**Files:**
- Modify: `src/main/java/net/siegeln/cameleer/saas/provisioning/DockerTenantProvisioner.java:197-200`
- [ ] **Step 1: Replace shared credentials with per-tenant credentials**
In `createServerContainer()` (line 197-200), replace:
```java
var env = new java.util.ArrayList<>(List.of(
"SPRING_DATASOURCE_URL=" + props.datasourceUrl(),
"SPRING_DATASOURCE_USERNAME=" + props.datasourceUsername(),
"SPRING_DATASOURCE_PASSWORD=" + props.datasourcePassword(),
```
With:
```java
// Per-tenant DB isolation: dedicated user+schema when dbPassword is set,
// shared credentials for backwards compatibility with pre-isolation tenants.
String dsUrl;
String dsUser;
String dsPass;
if (req.dbPassword() != null) {
dsUrl = props.datasourceUrl() + "?currentSchema=tenant_" + slug + "&ApplicationName=tenant_" + slug;
dsUser = "tenant_" + slug;
dsPass = req.dbPassword();
} else {
dsUrl = props.datasourceUrl();
dsUser = props.datasourceUsername();
dsPass = props.datasourcePassword();
}
var env = new java.util.ArrayList<>(List.of(
"SPRING_DATASOURCE_URL=" + dsUrl,
"SPRING_DATASOURCE_USERNAME=" + dsUser,
"SPRING_DATASOURCE_PASSWORD=" + dsPass,
```
- [ ] **Step 2: Commit**
```bash
git add src/main/java/net/siegeln/cameleer/saas/provisioning/DockerTenantProvisioner.java
git commit -m "feat: construct per-tenant JDBC URL with currentSchema and ApplicationName"
```
---
### Task 6: Update `VendorTenantService` — provisioning and delete flows
**Files:**
- Modify: `src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java`
- [ ] **Step 1: Inject `TenantDatabaseService`**
Add to the constructor and field declarations:
```java
private final TenantDatabaseService tenantDatabaseService;
```
Add to the constructor parameter list and assignment. (Follow the existing pattern of other injected services.)
- [ ] **Step 2: Update `provisionAsync()` — create DB before containers**
In `provisionAsync()` (around line 120), add DB creation before the provision call. Replace:
```java
var provisionRequest = new TenantProvisionRequest(tenantId, slug, tier, licenseToken);
ProvisionResult result = tenantProvisioner.provision(provisionRequest);
```
With:
```java
// Create per-tenant PG user + schema
String dbPassword = UUID.randomUUID().toString().replace("-", "")
+ UUID.randomUUID().toString().replace("-", "").substring(0, 8);
try {
tenantDatabaseService.createTenantDatabase(slug, dbPassword);
} catch (Exception e) {
log.error("Failed to create tenant database for {}: {}", slug, e.getMessage(), e);
tenantRepository.findById(tenantId).ifPresent(t -> {
t.setProvisionError("Database setup failed: " + e.getMessage());
tenantRepository.save(t);
});
return;
}
// Store DB password on entity
TenantEntity tenantForDb = tenantRepository.findById(tenantId).orElse(null);
if (tenantForDb == null) {
log.error("Tenant {} disappeared during provisioning", slug);
return;
}
tenantForDb.setDbPassword(dbPassword);
tenantRepository.save(tenantForDb);
var provisionRequest = new TenantProvisionRequest(tenantId, slug, tier, licenseToken, dbPassword);
ProvisionResult result = tenantProvisioner.provision(provisionRequest);
```
- [ ] **Step 3: Update the existing `TenantProvisionRequest` constructor call in upgrade flow**
Search for any other `new TenantProvisionRequest(...)` calls. The `upgradeServer` method (or re-provision after upgrade) also creates a provision request. Update it to pass `dbPassword` from the entity:
```java
TenantEntity tenant = ...;
var provisionRequest = new TenantProvisionRequest(
tenant.getId(), tenant.getSlug(), tenant.getTier().name(),
licenseToken, tenant.getDbPassword());
```
If the tenant has `dbPassword == null` (pre-existing), this is fine — Task 5 handles the null fallback.
- [ ] **Step 4: Update `delete()` — use TenantDatabaseService**
In `delete()` (around line 306), replace:
```java
// Erase tenant data from server databases (GDPR)
dataCleanupService.cleanup(tenant.getSlug());
```
With:
```java
// Drop per-tenant PG schema + user
tenantDatabaseService.dropTenantDatabase(tenant.getSlug());
// Erase ClickHouse data (GDPR)
dataCleanupService.cleanupClickHouse(tenant.getSlug());
```
- [ ] **Step 5: Commit**
```bash
git add src/main/java/net/siegeln/cameleer/saas/vendor/VendorTenantService.java
git commit -m "feat: create per-tenant PG database during provisioning, drop on delete"
```
---
### Task 7: Refactor `TenantDataCleanupService` — ClickHouse only
**Files:**
- Modify: `src/main/java/net/siegeln/cameleer/saas/provisioning/TenantDataCleanupService.java`
- [ ] **Step 1: Remove PG logic, rename public method**
Remove the `dropPostgresSchema()` method and the `cleanup()` method. Replace with a single public method:
```java
/**
* Deletes tenant data from ClickHouse tables (GDPR data erasure).
* PostgreSQL cleanup is handled by TenantDatabaseService.
*/
public void cleanupClickHouse(String slug) {
deleteClickHouseData(slug);
}
```
Remove the `dropPostgresSchema()` private method entirely. Keep `deleteClickHouseData()` unchanged.
- [ ] **Step 2: Commit**
```bash
git add src/main/java/net/siegeln/cameleer/saas/provisioning/TenantDataCleanupService.java
git commit -m "refactor: move PG cleanup to TenantDatabaseService, keep only ClickHouse"
```
---
### Task 8: Verify end-to-end
- [ ] **Step 1: Build**
```bash
mvn compile -pl .
```
Verify no compilation errors.
- [ ] **Step 2: Deploy and test tenant creation**
Deploy the updated SaaS image. Create a new tenant via the UI. Verify in PostgreSQL:
```sql
-- Should show the new tenant user
SELECT rolname FROM pg_roles WHERE rolname LIKE 'tenant_%';
-- Should show the new tenant schema
SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'tenant_%';
```
- [ ] **Step 3: Verify server container env vars**
```bash
docker inspect cameleer-server-<slug> | grep -E "DATASOURCE|currentSchema|ApplicationName"
```
Expected: URL contains `?currentSchema=tenant_<slug>&ApplicationName=tenant_<slug>`, username is `tenant_<slug>`.
- [ ] **Step 4: Verify Infrastructure page**
Navigate to Vendor > Infrastructure. The PostgreSQL card should now show the tenant schema with size/tables/rows.
- [ ] **Step 5: Test tenant deletion**
Delete the tenant. Verify:
```sql
-- User should be gone
SELECT rolname FROM pg_roles WHERE rolname LIKE 'tenant_%';
-- Schema should be gone
SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'tenant_%';
```
- [ ] **Step 6: Commit all remaining changes**
```bash
git add -A
git commit -m "feat: per-tenant PostgreSQL isolation — complete implementation"
```

View File

@@ -3,7 +3,7 @@
**Date:** 2026-03-29
**Status:** Draft — Awaiting Review
**Author:** Boardroom simulation (Strategist, Skeptic, Architect, Growth Hacker)
**Gitea Issues:** cameleer/cameleer3 #57-#72 (label: MOAT)
**Gitea Issues:** cameleer/cameleer #57-#72 (label: MOAT)
## Executive Summary
@@ -32,14 +32,14 @@ Week 8-14: Live Route Debugger (agent + server + UI)
- #59 — Cross-Service Trace Correlation + Topology Map
**Debugger sub-issues:**
- #60 — Protocol: Debug session command types (`cameleer3-common`)
- #60 — Protocol: Debug session command types (`cameleer-common`)
- #61 — Agent: DebugSessionManager + breakpoint InterceptStrategy integration
- #62 — Agent: ExchangeStateSerializer + synthetic direct route wrapper
- #63 — Server: DebugSessionService + WebSocket + REST API
- #70 — UI: Debug session frontend components
**Lineage sub-issues:**
- #64 — Protocol: Lineage command types (`cameleer3-common`)
- #64 — Protocol: Lineage command types (`cameleer-common`)
- #65 — Agent: LineageManager + capture mode integration
- #66 — Server: LineageService + DiffEngine + REST API
- #71 — UI: Lineage timeline + diff viewer components
@@ -69,14 +69,14 @@ Browser (SaaS UI)
WebSocket <--------------------------------------+
| |
v |
cameleer3-server |
cameleer-server |
| POST /api/v1/debug/sessions |
| POST /api/v1/debug/sessions/{id}/step |
| POST /api/v1/debug/sessions/{id}/resume |
| DELETE /api/v1/debug/sessions/{id} |
| |
v |
SSE Command Channel --> cameleer3 agent |
SSE Command Channel --> cameleer agent |
| | |
| "start-debug" | |
| command v |
@@ -101,7 +101,7 @@ SSE Command Channel --> cameleer3 agent |
| Continue to next processor
```
### 1.3 Protocol Additions (cameleer3-common)
### 1.3 Protocol Additions (cameleer-common)
#### New SSE Commands
@@ -160,11 +160,11 @@ SSE Command Channel --> cameleer3 agent |
}
```
### 1.4 Agent Implementation (cameleer3-agent)
### 1.4 Agent Implementation (cameleer-agent)
#### DebugSessionManager
- Location: `com.cameleer3.agent.debug.DebugSessionManager`
- Location: `com.cameleer.agent.debug.DebugSessionManager`
- Stores active sessions: `ConcurrentHashMap<sessionId, DebugSession>`
- Enforces max concurrent sessions (default 3, configurable via `cameleer.debug.maxSessions`)
- Allocates **dedicated Thread** per session (NOT from Camel thread pool)
@@ -213,7 +213,7 @@ For non-direct routes (timer, jms, http, file):
3. Debug exchange enters via `ProducerTemplate.send()`
4. Remove temporary route on session completion
### 1.5 Server Implementation (cameleer3-server)
### 1.5 Server Implementation (cameleer-server)
#### REST Endpoints
@@ -308,7 +308,7 @@ Capture the full transformation history of a message flowing through a route. At
### 2.2 Architecture
```
cameleer3 agent
cameleer agent
|
| On lineage-enabled exchange:
| Before processor: capture INPUT
@@ -319,7 +319,7 @@ cameleer3 agent
POST /api/v1/data/executions (processors carry full snapshots)
|
v
cameleer3-server
cameleer-server
|
| LineageService:
| > Flatten processor tree to ordered list
@@ -334,7 +334,7 @@ GET /api/v1/executions/{id}/lineage
Browser: LineageTimeline + DiffViewer
```
### 2.3 Protocol Additions (cameleer3-common)
### 2.3 Protocol Additions (cameleer-common)
#### New SSE Commands
@@ -370,11 +370,11 @@ Browser: LineageTimeline + DiffViewer
| `EXPRESSION` | Any exchange matching a Simple/JsonPath predicate |
| `NEXT_N` | Next N exchanges on the route (countdown) |
### 2.4 Agent Implementation (cameleer3-agent)
### 2.4 Agent Implementation (cameleer-agent)
#### LineageManager
- Location: `com.cameleer3.agent.lineage.LineageManager`
- Location: `com.cameleer.agent.lineage.LineageManager`
- Stores active configs: `ConcurrentHashMap<lineageId, LineageConfig>`
- Tracks capture count per lineageId: auto-disables at `maxCaptures`
- Duration timeout via `ScheduledExecutorService`: auto-disables after expiry
@@ -412,7 +412,7 @@ cameleer.lineage.maxBodySize=65536 # 64KB for lineage captures (vs 4KB normal
cameleer.lineage.enabled=true # master switch
```
### 2.5 Server Implementation (cameleer3-server)
### 2.5 Server Implementation (cameleer-server)
#### LineageService
@@ -548,7 +548,7 @@ New (added):
| Direct/SEDA | URI prefix `direct:`, `seda:`, `vm:` | Exchange property (in-process) |
| File/FTP | URI prefix `file:`, `ftp:` | Not propagated (async) |
### 3.3 Agent Implementation (cameleer3-agent)
### 3.3 Agent Implementation (cameleer-agent)
#### Outgoing Propagation (InterceptStrategy)
@@ -597,7 +597,7 @@ execution.setHopIndex(...); // depth in distributed trace
- Parse failure: log warning, continue without context (no exchange failure)
- Only inject on outgoing processors, never on FROM consumers
### 3.4 Server Implementation: Trace Assembly (cameleer3-server)
### 3.4 Server Implementation: Trace Assembly (cameleer-server)
#### CorrelationService
@@ -665,7 +665,7 @@ CREATE INDEX idx_executions_parent_span
- **Fan-out:** parallel multicast creates multiple children from same processor
- **Circular calls:** detected via hopIndex (max depth 20)
### 3.5 Server Implementation: Topology Graph (cameleer3-server)
### 3.5 Server Implementation: Topology Graph (cameleer-server)
#### DependencyGraphService
@@ -799,11 +799,11 @@ Reserve `sourceTenantHash` in TraceContext for future use:
| Work | Repo | Issue |
|------|------|-------|
| Service topology materialized view | cameleer3-server | #69 |
| Topology REST API | cameleer3-server | #69 |
| ServiceTopologyGraph.tsx | cameleer3-server + saas | #72 |
| WebSocket infrastructure (for debugger) | cameleer3-server | #63 |
| TraceContext DTO in cameleer3-common | cameleer3 | #67 |
| Service topology materialized view | cameleer-server | #69 |
| Topology REST API | cameleer-server | #69 |
| ServiceTopologyGraph.tsx | cameleer-server + saas | #72 |
| WebSocket infrastructure (for debugger) | cameleer-server | #63 |
| TraceContext DTO in cameleer-common | cameleer | #67 |
**Ship:** Topology graph visible from existing data. Zero agent changes. Immediate visual payoff.
@@ -811,10 +811,10 @@ Reserve `sourceTenantHash` in TraceContext for future use:
| Work | Repo | Issue |
|------|------|-------|
| Lineage protocol DTOs | cameleer3-common | #64 |
| LineageManager + capture integration | cameleer3-agent | #65 |
| LineageService + DiffEngine | cameleer3-server | #66 |
| Lineage UI components | cameleer3-server + saas | #71 |
| Lineage protocol DTOs | cameleer-common | #64 |
| LineageManager + capture integration | cameleer-agent | #65 |
| LineageService + DiffEngine | cameleer-server | #66 |
| Lineage UI components | cameleer-server + saas | #71 |
**Ship:** Payload flow lineage independently usable.
@@ -822,10 +822,10 @@ Reserve `sourceTenantHash` in TraceContext for future use:
| Work | Repo | Issue |
|------|------|-------|
| Trace context header propagation | cameleer3-agent | #67 |
| Executions table migration (new columns) | cameleer3-server | #68 |
| CorrelationService + trace assembly | cameleer3-server | #68 |
| DistributedTraceView + TraceSearch UI | cameleer3-server + saas | #72 |
| Trace context header propagation | cameleer-agent | #67 |
| Executions table migration (new columns) | cameleer-server | #68 |
| CorrelationService + trace assembly | cameleer-server | #68 |
| DistributedTraceView + TraceSearch UI | cameleer-server + saas | #72 |
**Ship:** Distributed traces + topology — full correlation story.
@@ -833,11 +833,11 @@ Reserve `sourceTenantHash` in TraceContext for future use:
| Work | Repo | Issue |
|------|------|-------|
| Debug protocol DTOs | cameleer3-common | #60 |
| DebugSessionManager + InterceptStrategy | cameleer3-agent | #61 |
| ExchangeStateSerializer + synthetic wrapper | cameleer3-agent | #62 |
| DebugSessionService + WS + REST | cameleer3-server | #63 |
| Debug UI components | cameleer3-server + saas | #70 |
| Debug protocol DTOs | cameleer-common | #60 |
| DebugSessionManager + InterceptStrategy | cameleer-agent | #61 |
| ExchangeStateSerializer + synthetic wrapper | cameleer-agent | #62 |
| DebugSessionService + WS + REST | cameleer-server | #63 |
| Debug UI components | cameleer-server + saas | #70 |
**Ship:** Full browser-based route debugger with integration to lineage and correlation.

View File

@@ -10,12 +10,12 @@
## 1. Product Definition
**Cameleer SaaS** is a Camel application runtime platform with built-in observability. Customers deploy Apache Camel applications and get zero-configuration tracing, topology mapping, payload lineage, distributed correlation, live debugging, and exchange replay — powered by the cameleer3 agent (auto-injected) and cameleer3-server (managed per tenant).
**Cameleer SaaS** is a Camel application runtime platform with built-in observability. Customers deploy Apache Camel applications and get zero-configuration tracing, topology mapping, payload lineage, distributed correlation, live debugging, and exchange replay — powered by the cameleer agent (auto-injected) and cameleer-server (managed per tenant).
### Three Pillars
1. **Runtime** — Deploy and run Camel applications with automatic agent injection
2. **Observability** — Per-tenant cameleer3-server (traces, topology, lineage, correlation, debugger, replay)
2. **Observability** — Per-tenant cameleer-server (traces, topology, lineage, correlation, debugger, replay)
3. **Management** — Auth, billing, teams, provisioning, secrets, environments
### Two Deployment Modes
@@ -27,8 +27,8 @@
| Component | Role | Changes Required |
|-----------|------|------------------|
| cameleer3 (agent) | Zero-code Camel instrumentation, auto-injected into customer JARs | MOAT features (lineage, correlation, debugger, replay) |
| cameleer3-server | Per-tenant observability backend | Managed mode (trust SaaS JWT), license module, MOAT features |
| cameleer (agent) | Zero-code Camel instrumentation, auto-injected into customer JARs | MOAT features (lineage, correlation, debugger, replay) |
| cameleer-server | Per-tenant observability backend | Managed mode (trust SaaS JWT), license module, MOAT features |
| cameleer-saas (this repo) | SaaS management platform — control plane | New: everything in this document |
| design-system | Shared React component library | Used by both SaaS shell and server UI |
@@ -81,7 +81,7 @@ Single Spring Boot application with well-bounded internal modules. K8s ingress h
```
[Browser] → [Ingress (Traefik/Envoy)] → [SaaS Platform (modular Spring Boot)]
↓ (tenant routes) ↓ (provisioning)
[Tenant cameleer3-server] [Flux CD → K8s]
[Tenant cameleer-server] [Flux CD → K8s]
```
### Component Map
@@ -114,7 +114,7 @@ Single Spring Boot application with well-bounded internal modules. K8s ingress h
│ (PostgreSQL) │ │ API │ │ │
│ - tenants │ └────────┘ │ ┌─────────────────────┐ │
│ - users │ │ │ tenant-a namespace │ │
│ - teams │ ┌─────┐ │ │ ├─ cameleer3-server │ │
│ - teams │ ┌─────┐ │ │ ├─ cameleer-server │ │
│ - audit log │ │Flux │ │ │ ├─ camel-app-1 │ │
│ - licenses │ │ CD │ │ │ ├─ camel-app-2 │ │
└──────────────┘ └──┬──┘ │ │ └─ NetworkPolicies │ │
@@ -144,7 +144,7 @@ Same management platform routes to dedicated cluster(s) per customer. Dedicated
| Management Platform backend | Spring Boot 3, Java 21 |
| Management Platform frontend | React, @cameleer/design-system |
| Platform database | PostgreSQL |
| Tenant observability | cameleer3-server (Spring Boot), PostgreSQL, OpenSearch |
| Tenant observability | cameleer-server (Spring Boot), PostgreSQL, OpenSearch |
| GitOps | Flux CD |
| K8s distribution | Talos (production), k3s (dev) |
| Ingress | Traefik or Envoy |
@@ -192,7 +192,7 @@ Stores all SaaS control plane data — completely separate from tenant observabi
### Tenant Data (Shared PostgreSQL)
Each tenant's cameleer3-server uses its own PostgreSQL schema on the shared instance (dedicated instance for high/business). This is the existing cameleer3-server data model — unchanged:
Each tenant's cameleer-server uses its own PostgreSQL schema on the shared instance (dedicated instance for high/business). This is the existing cameleer-server data model — unchanged:
- Route executions, processor traces, metrics
- Route graph topology
@@ -215,12 +215,12 @@ Completely separate: Prometheus TSDB for metrics, Loki for logs.
### Architecture
The SaaS management platform is the single identity plane. It owns authentication and authorization. Per-tenant cameleer3-server instances trust SaaS-issued tokens.
The SaaS management platform is the single identity plane. It owns authentication and authorization. Per-tenant cameleer-server instances trust SaaS-issued tokens.
- Spring Security OAuth2 for OIDC federation with customer IdPs
- Ed25519 JWT signing (consistent with existing cameleer3-server pattern)
- Ed25519 JWT signing (consistent with existing cameleer-server pattern)
- Tokens carry: tenant ID, user ID, roles, feature entitlements
- cameleer3-server validates SaaS-issued JWTs in managed mode
- cameleer-server validates SaaS-issued JWTs in managed mode
- Standalone mode retains its own auth for air-gapped deployments
### RBAC Model
@@ -252,7 +252,7 @@ Customer signs up + payment
→ Create tenant record + Stripe customer/subscription
→ Generate signed license token (Ed25519)
→ Create Flux HelmRelease CR
→ Flux reconciles: namespace, ResourceQuota, NetworkPolicies, cameleer3-server
→ Flux reconciles: namespace, ResourceQuota, NetworkPolicies, cameleer-server
→ Provision PostgreSQL schema + per-tenant credentials
→ Provision OpenSearch index template + per-tenant credentials
→ Readiness check: server healthy, DB migrated, auth working
@@ -297,7 +297,7 @@ Full Cluster API automation deferred to future release.
### JAR Upload → Immutable Image
1. **Validation** — File type check, size limit per tier, SHA-256 checksum, Trivy security scan, secret detection (reject JARs with embedded credentials)
2. **Image Build** — Templated Dockerfile: distroless JRE base + customer JAR + cameleer3-agent.jar + `-javaagent` flag + agent pre-configured for tenant server. Image tagged: `registry/{tenant}/{app}:v{N}-{sha256short}`. Signed with cosign. SBOM attached.
2. **Image Build** — Templated Dockerfile: distroless JRE base + customer JAR + cameleer-agent.jar + `-javaagent` flag + agent pre-configured for tenant server. Image tagged: `registry/{tenant}/{app}:v{N}-{sha256short}`. Signed with cosign. SBOM attached.
3. **Registry Push** — Per-tenant repository in platform container registry
4. **Deploy** — K8s Deployment in tenant namespace with resource limits, secrets mounted, config injected, NetworkPolicy applied, liveness/readiness probes
@@ -350,7 +350,7 @@ Central UI for managing each deployed application:
### Architecture
Each tenant gets a dedicated cameleer3-server instance:
Each tenant gets a dedicated cameleer-server instance:
- Shared tiers: deployed in tenant's namespace
- Dedicated tiers: deployed in tenant's cluster
@@ -359,7 +359,7 @@ The SaaS API gateway routes `/t/{tenant}/api/*` to the correct server instance.
### Agent Connection
- Agent bootstrap tokens generated by the SaaS platform
- Agents connect directly to their tenant's cameleer3-server instance
- Agents connect directly to their tenant's cameleer-server instance
- Agent auto-injected into customer Camel apps deployed on the platform
- External agents (customer-hosted Camel apps) can also connect using bootstrap tokens
@@ -448,7 +448,7 @@ K8s NetworkPolicies per tenant namespace:
- **Allow:** tenant namespace → shared PostgreSQL/OpenSearch (authenticated per-tenant credentials)
- **Allow:** tenant namespace → public internet (Camel app external connectivity)
- **Allow:** SaaS platform namespace → all tenant namespaces (management access)
- **Allow:** tenant Camel apps → tenant cameleer3-server (intra-namespace)
- **Allow:** tenant Camel apps → tenant cameleer-server (intra-namespace)
### Zero-Trust Tenant Boundary
@@ -546,7 +546,7 @@ Completely separate from tenant observability data.
- TLS certificate expiry < 14 days
- Metering pipeline stale > 1 hour
- Disk usage > 80% on any PV
- Tenant cameleer3-server unhealthy > 5 minutes
- Tenant cameleer-server unhealthy > 5 minutes
- OOMKill on any tenant workload
### Dashboards
@@ -577,7 +577,7 @@ K8s Metrics → Metrics Collector → Usage Aggregator (hourly) → Stripe Usage
|-----------|------|--------|
| CPU | core·hours | K8s metrics (namespace aggregate) |
| RAM | GB·hours | K8s metrics (namespace aggregate) |
| Data volume | GB ingested | cameleer3-server reports |
| Data volume | GB ingested | cameleer-server reports |
- Aggregated per tenant, per hour, stored in platform DB before Stripe submission
- Idempotent aggregation (safe to re-run)
@@ -613,7 +613,7 @@ K8s Metrics → Metrics Collector → Usage Aggregator (hourly) → Stripe Usage
| **App → Status** | Pod health, resource usage, agent connection, events |
| **App → Logs** | Live stdout/stderr stream |
| **App → Versions** | Image history, promotion log, rollback |
| **Observe** | Embedded cameleer3-server UI (topology, traces, lineage, correlation, debugger, replay) |
| **Observe** | Embedded cameleer-server UI (topology, traces, lineage, correlation, debugger, replay) |
| **Team** | Users, roles, invites |
| **Settings** | Tenant config, SSO/OIDC, vault connections |
| **Billing** | Usage, invoices, plan management |
@@ -621,7 +621,7 @@ K8s Metrics → Metrics Collector → Usage Aggregator (hourly) → Stripe Usage
### Design
- SaaS shell built with `@cameleer/design-system`
- cameleer3-server React UI embedded (same design system, visual consistency)
- cameleer-server React UI embedded (same design system, visual consistency)
- Responsive but desktop-primary (observability tooling is a desktop workflow)
---
@@ -681,4 +681,4 @@ K8s Metrics → Metrics Collector → Usage Aggregator (hourly) → Stripe Usage
| 12 | Platform Operations & Self-Monitoring | epic, ops |
| 13 | MOAT: Exchange Replay | epic, observability |
MOAT features (Debugger, Lineage, Correlation) tracked in cameleer/cameleer3 #57#72.
MOAT features (Debugger, Lineage, Correlation) tracked in cameleer/cameleer #57#72.

View File

@@ -27,7 +27,7 @@ Key constraints:
| **Identity & Auth** | **Logto** | MPL-2.0 | Lightest IdP (2 containers, ~0.5-1 GB). Orgs, RBAC, M2M tokens, OIDC/SSO federation all in OSS. Replaces ~3-4 months of custom auth build (OIDC, SSO, teams, invites, MFA, password reset, custom roles). |
| **Reverse Proxy** | **Traefik** | MIT | Native Docker provider (labels) and K8s provider (IngressRoute CRDs). Same mental model in both environments. Already on the k3s cluster. ForwardAuth middleware for tenant-aware routing. Auto-HTTPS via Let's Encrypt. ~256 MB RAM. |
| **Database** | **PostgreSQL** | PostgreSQL License | Already chosen. Platform data + Logto data (separate schemas). |
| **Trace/Metrics Storage** | **ClickHouse** | Apache-2.0 | Replaced OpenSearch in the cameleer3-server stack. Columnar OLAP, excellent for time-series observability data. |
| **Trace/Metrics Storage** | **ClickHouse** | Apache-2.0 | Replaced OpenSearch in the cameleer-server stack. Columnar OLAP, excellent for time-series observability data. |
| **Schema Migrations** | **Flyway** | Apache-2.0 | Already in place. |
| **Billing (subscriptions)** | **Stripe** | N/A (API) | Start with Stripe Checkout for fixed-tier subscriptions. No custom billing infrastructure day 1. |
| **Billing (usage metering)** | **Lago** (deferred) | AGPL-3.0 | Purpose-built for event-based metering. 8 containers — deploy only when usage-based pricing launches. Design event model with Lago's API shape in mind from day 1. Integrate via API only (keeps AGPL safe). |
@@ -42,14 +42,14 @@ Key constraints:
| Subsystem | Why Build |
|---|---|
| **License signing & validation** | Ed25519 signed JWT with tier, features, limits, expiry. Dual mode: online API check + offline signed file. No off-the-shelf tool does this. Core IP. |
| **Agent bootstrap tokens** | Tightly coupled to the cameleer3 agent protocol (PROTOCOL.md). Custom Ed25519 tokens for agent registration. |
| **Agent bootstrap tokens** | Tightly coupled to the cameleer agent protocol (PROTOCOL.md). Custom Ed25519 tokens for agent registration. |
| **Tenant lifecycle** | CRUD, configuration, status management. Core business logic. User management (invites, teams, roles) is delegated to Logto's organization model. |
| **Runtime orchestration** | The core of the "managed Camel runtime" product. `RuntimeOrchestrator` interface with Docker and K8s implementations. No off-the-shelf tool does "managed Camel runtime with agent injection." |
| **Image build pipeline** | Templated Dockerfile: JRE + cameleer3-agent.jar + customer JAR + `-javaagent` flag. Simple but custom. |
| **Image build pipeline** | Templated Dockerfile: JRE + cameleer-agent.jar + customer JAR + `-javaagent` flag. Simple but custom. |
| **Feature gating** | Tier-based feature gating logic. Which features are available at which tier. Business logic. |
| **Billing integration** | Stripe API calls, subscription lifecycle, webhook handling. Thin integration layer. |
| **Observability proxy** | Routing authenticated requests to tenant-specific cameleer3-server instances. |
| **MOAT features** | Debugger, Lineage, Correlation — the defensible product. Built in cameleer3 agent + server. |
| **Observability proxy** | Routing authenticated requests to tenant-specific cameleer-server instances. |
| **MOAT features** | Debugger, Lineage, Correlation — the defensible product. Built in cameleer agent + server. |
### SKIP / DEFER
@@ -74,7 +74,7 @@ Key constraints:
+--------+---------------------+------------------------+
| |
+--------v--------+ +---------v-----------+
| cameleer-saas | | cameleer3-server |
| cameleer-saas | | cameleer-server |
| (Spring Boot) | | (observability) |
| Control plane | | Per-tenant instance |
+---+-------+-----+ +----------+----------+
@@ -99,10 +99,10 @@ API request:
-> Traefik forwards to upstream service
Machine auth (agent bootstrap):
cameleer3-agent -> cameleer-saas /api/agent/register
cameleer-agent -> cameleer-saas /api/agent/register
-> Validates bootstrap token (Ed25519)
-> Issues agent session token
-> Agent connects to cameleer3-server
-> Agent connects to cameleer-server
```
Logto handles all user-facing identity. The cameleer-saas app handles machine-to-machine auth (agent tokens, license tokens) using Ed25519.
@@ -137,9 +137,9 @@ Customer uploads JAR
-> Validation (file type, size, SHA-256, security scan)
-> Templated Dockerfile generation:
FROM eclipse-temurin:21-jre-alpine
COPY cameleer3-agent.jar /opt/agent/
COPY cameleer-agent.jar /opt/agent/
COPY customer-app.jar /opt/app/
ENTRYPOINT ["java", "-javaagent:/opt/agent/cameleer3-agent.jar", "-jar", "/opt/app/customer-app.jar"]
ENTRYPOINT ["java", "-javaagent:/opt/agent/cameleer-agent.jar", "-jar", "/opt/app/customer-app.jar"]
-> Build:
Docker mode: docker build via docker-java (local image cache)
K8s mode: Kaniko Job -> push to registry
@@ -152,7 +152,7 @@ Customer uploads JAR
- **Schema-per-tenant** in PostgreSQL for platform data isolation.
- **Logto organizations** map 1:1 to tenants. Logto handles user-tenant membership.
- **ClickHouse** data partitioned by tenant_id.
- **cameleer3-server** instances are per-tenant (separate containers/pods).
- **cameleer-server** instances are per-tenant (separate containers/pods).
- **K8s bonus:** Namespace-per-tenant for network isolation, resource quotas.
### Environment Model
@@ -232,8 +232,8 @@ services:
- traefik.enable=true
- traefik.http.routers.auth.rule=PathPrefix(`/auth`)
cameleer3-server:
image: gitea.siegeln.net/cameleer/cameleer3-server:${VERSION}
cameleer-server:
image: gitea.siegeln.net/cameleer/cameleer-server:${VERSION}
environment:
- CLICKHOUSE_URL=jdbc:clickhouse://clickhouse:8123/cameleer
labels:
@@ -312,9 +312,9 @@ volumes:
### Phase 4: Observability Pipeline
**Goal:** Customer can see traces, metrics, and route topology for deployed apps.
- Connect cameleer3-server to customer app containers
- Connect cameleer-server to customer app containers
- ClickHouse tenant-scoped data partitioning
- Observability API proxy (tenant-aware routing to cameleer3-server)
- Observability API proxy (tenant-aware routing to cameleer-server)
- Basic topology graph endpoint
- Agent ↔ server connectivity verification
@@ -367,13 +367,13 @@ volumes:
1. Upload a sample Camel JAR via API
2. Platform builds container image
3. Deploy to "dev" environment
4. Container starts with cameleer3 agent attached
4. Container starts with cameleer agent attached
5. App is reachable via Traefik routing
6. Logs are accessible via API
7. Deploy same image to "prod" with different config
### Phase 4 Verification
1. Running Camel app sends traces to cameleer3-server
1. Running Camel app sends traces to cameleer-server
2. Traces visible in ClickHouse with correct tenant_id
3. Topology graph shows route structure
4. Different tenant cannot see another tenant's data
@@ -393,7 +393,7 @@ docker compose up -d
# Create tenant + user via API/Logto
# Upload sample Camel JAR
# Deploy to environment
# Verify agent connects to cameleer3-server
# Verify agent connects to cameleer-server
# Verify traces in ClickHouse
# Verify observability API returns data
```

View File

@@ -7,7 +7,7 @@
## Context
Phase 2 delivered multi-tenancy, identity (Logto OIDC), and license management. The platform can create tenants and issue licenses, but there is nothing to run yet. Phase 3 is the core product differentiator: customers upload a Camel JAR, the platform builds an immutable container image with the cameleer3 agent auto-injected, and deploys it to a logical environment. This is "managed Camel runtime" — similar to Coolify or MuleSoft CloudHub, but purpose-built for Apache Camel with deep observability.
Phase 2 delivered multi-tenancy, identity (Logto OIDC), and license management. The platform can create tenants and issue licenses, but there is nothing to run yet. Phase 3 is the core product differentiator: customers upload a Camel JAR, the platform builds an immutable container image with the cameleer agent auto-injected, and deploys it to a logical environment. This is "managed Camel runtime" — similar to Coolify or MuleSoft CloudHub, but purpose-built for Apache Camel with deep observability.
Docker-first. The `KubernetesRuntimeOrchestrator` is deferred to Phase 5.
@@ -23,10 +23,10 @@ Docker-first. The `KubernetesRuntimeOrchestrator` is deferred to Phase 5.
| Deployment model | Async with polling | Image builds are inherently slow. Deploy returns immediately with deployment ID. Client polls for status. |
| Entity hierarchy | Environment → App → Deployment | User thinks "I'm in dev, deploy my app." Environment is the workspace context. |
| Environment provisioning | Hybrid auto + manual | Every tenant gets a `default` environment on creation. Additional environments created manually, tier limit enforced. |
| Cross-environment isolation | Logical (not network) | Docker single-tenant mode — customer owns the stack. Data separated by `environmentId` in cameleer3-server. Network isolation is a K8s Phase 5 concern. |
| Container networking | Shared `cameleer` bridge network | Customer containers join the existing network. Agent reaches cameleer3-server at `http://cameleer3-server:8081`. |
| Cross-environment isolation | Logical (not network) | Docker single-tenant mode — customer owns the stack. Data separated by `environmentId` in cameleer-server. Network isolation is a K8s Phase 5 concern. |
| Container networking | Shared `cameleer` bridge network | Customer containers join the existing network. Agent reaches cameleer-server at `http://cameleer-server:8081`. |
| Container naming | `{tenant-slug}-{env-slug}-{app-slug}` | Human-readable, unique, identifies tenant+environment+app at a glance. |
| Bootstrap tokens | Shared `CAMELEER_AUTH_TOKEN` from cameleer3-server config | Platform reads the existing token and injects it into customer containers. Environment separation via agent `environmentId` claim, not token. Per-environment tokens deferred to K8s Phase 5. |
| Bootstrap tokens | Shared `CAMELEER_AUTH_TOKEN` from cameleer-server config | Platform reads the existing token and injects it into customer containers. Environment separation via agent `environmentId` claim, not token. Per-environment tokens deferred to K8s Phase 5. |
| Health checking | Agent health endpoint (port 9464) | Guaranteed to exist, no user config needed. User-defined health endpoints deferred. |
| Inbound HTTP routing | Not in Phase 3 | Most Camel apps are consumers (queues, polls), not servers. Traefik routing for customer apps deferred to Phase 4/4.5. |
| Container logs | Captured via docker-java, written to ClickHouse | Unified log query surface from day 1. Same pattern future app logs will use. |
@@ -157,7 +157,7 @@ Uses `com.github.docker-java:docker-java` library. Connects via Docker socket (`
- Environment variables:
- `CAMELEER_AUTH_TOKEN={bootstrap-token}`
- `CAMELEER_EXPORT_TYPE=HTTP`
- `CAMELEER_EXPORT_ENDPOINT=http://cameleer3-server:8081`
- `CAMELEER_EXPORT_ENDPOINT=http://cameleer-server:8081`
- `CAMELEER_APPLICATION_ID={app-slug}`
- `CAMELEER_ENVIRONMENT_ID={env-slug}`
- `CAMELEER_DISPLAY_NAME={tenant-slug}-{env-slug}-{app-slug}`
@@ -182,7 +182,7 @@ A pre-built Docker image containing everything except the customer JAR:
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY cameleer3-agent-{version}-shaded.jar /app/agent.jar
COPY cameleer-agent-{version}-shaded.jar /app/agent.jar
ENTRYPOINT exec java \
-Dcameleer.export.type=${CAMELEER_EXPORT_TYPE:-HTTP} \
@@ -250,11 +250,11 @@ ORDER BY (tenant_id, environment_id, app_id, timestamp);
### Bootstrap Token Handling
In Docker single-tenant mode, all environments share the single cameleer3-server instance and its single `CAMELEER_AUTH_TOKEN`. The platform reads this token from its own configuration (`cameleer.runtime.bootstrap-token` / `CAMELEER_AUTH_TOKEN` env var) and injects it into every customer container. No changes to cameleer3-server are needed.
In Docker single-tenant mode, all environments share the single cameleer-server instance and its single `CAMELEER_AUTH_TOKEN`. The platform reads this token from its own configuration (`cameleer.runtime.bootstrap-token` / `CAMELEER_AUTH_TOKEN` env var) and injects it into every customer container. No changes to cameleer-server are needed.
Environment-level data separation happens at the agent registration level — the agent sends its `environmentId` claim when it registers, and cameleer3-server uses that to scope all data. The bootstrap token is the same across environments in a Docker stack.
Environment-level data separation happens at the agent registration level — the agent sends its `environmentId` claim when it registers, and cameleer-server uses that to scope all data. The bootstrap token is the same across environments in a Docker stack.
The `bootstrap_token` column on the environment entity stores the token value used for that environment's containers. In Docker mode this is the same shared value for all environments. In K8s mode (Phase 5), each environment could have its own cameleer3-server instance with a unique token, enabling true per-environment token isolation.
The `bootstrap_token` column on the environment entity stores the token value used for that environment's containers. In Docker mode this is the same shared value for all environments. In K8s mode (Phase 5), each environment could have its own cameleer-server instance with a unique token, enabling true per-environment token isolation.
## API Surface
@@ -354,7 +354,7 @@ The cameleer-saas service needs:
- JAR storage volume: `jardata:/data/jars`
- `cameleer-runtime-base` image must be available (pre-pulled or built locally)
The cameleer3-server `CAMELEER_AUTH_TOKEN` is read by cameleer-saas from shared environment config and injected into customer containers.
The cameleer-server `CAMELEER_AUTH_TOKEN` is read by cameleer-saas from shared environment config and injected into customer containers.
New volume in docker-compose.yml:
```yaml
@@ -413,7 +413,7 @@ cameleer:
3. Poll `GET /api/apps/{aid}/deployments/{did}` — status transitions: `BUILDING` → `STARTING` → `RUNNING`
4. Container visible in `docker ps` as `{tenant}-{env}-{app}`
5. Container is on the `cameleer` network
6. cameleer3 agent registers with cameleer3-server (visible in server logs)
6. cameleer agent registers with cameleer-server (visible in server logs)
7. Agent health endpoint responds on port 9464
8. Container logs appear in ClickHouse `container_logs` table
9. `GET /api/apps/{aid}/logs` returns log entries

View File

@@ -7,18 +7,18 @@
## Context
Phase 3 delivered the managed Camel runtime: customers upload a JAR, the platform builds a container with the cameleer3 agent injected, and deploys it. The agent connects to cameleer3-server and sends traces, metrics, diagrams, and logs to ClickHouse. But there is no way for the user to see this data yet, and customer apps that expose HTTP endpoints are not reachable.
Phase 3 delivered the managed Camel runtime: customers upload a JAR, the platform builds a container with the cameleer agent injected, and deploys it. The agent connects to cameleer-server and sends traces, metrics, diagrams, and logs to ClickHouse. But there is no way for the user to see this data yet, and customer apps that expose HTTP endpoints are not reachable.
Phase 4 completes the loop: deploy an app, hit its endpoint, see the traces in the dashboard.
cameleer3-server already has the complete observability stack — ClickHouse schemas with `tenant_id` partitioning, full search/stats/diagram/log REST APIs, and a React SPA dashboard. Phase 4 is a **wiring phase**, not a build-from-scratch phase.
cameleer-server already has the complete observability stack — ClickHouse schemas with `tenant_id` partitioning, full search/stats/diagram/log REST APIs, and a React SPA dashboard. Phase 4 is a **wiring phase**, not a build-from-scratch phase.
## Key Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Observability UI | Serve existing cameleer3-server React SPA via Traefik | Already built. SaaS management UI is Phase 9 — observability UI is not SaaS-specific. |
| API access | Traefik routes directly to cameleer3-server with forward-auth | No proxy layer needed. Forward-auth validates user, injects headers. Server API works as-is. |
| Observability UI | Serve existing cameleer-server React SPA via Traefik | Already built. SaaS management UI is Phase 9 — observability UI is not SaaS-specific. |
| API access | Traefik routes directly to cameleer-server with forward-auth | No proxy layer needed. Forward-auth validates user, injects headers. Server API works as-is. |
| Server changes | None | Single-tenant Docker mode works out of the box. `CAMELEER_TENANT_ID` env var already supported. |
| Agent changes | None | Agent already sends `applicationId`, `environmentId`, connects to `CAMELEER_EXPORT_ENDPOINT`. |
| Tenant ID | Set `CAMELEER_TENANT_ID` to tenant slug in Docker Compose | Tags ClickHouse data with the real tenant identity from day one. Avoids `'default'` → real-id migration later. |
@@ -27,16 +27,16 @@ cameleer3-server already has the complete observability stack — ClickHouse sch
## What's Already Working (Phase 3)
- Customer containers on the `cameleer` bridge network
- Agent configured: `CAMELEER_AUTH_TOKEN`, `CAMELEER_EXPORT_ENDPOINT=http://cameleer3-server:8081`, `CAMELEER_APPLICATION_ID`, `CAMELEER_ENVIRONMENT_ID`
- cameleer3-server writes traces/metrics/diagrams/logs to ClickHouse
- Traefik routes `/observe/*` to cameleer3-server with forward-auth middleware
- Agent configured: `CAMELEER_AUTH_TOKEN`, `CAMELEER_EXPORT_ENDPOINT=http://cameleer-server:8081`, `CAMELEER_APPLICATION_ID`, `CAMELEER_ENVIRONMENT_ID`
- cameleer-server writes traces/metrics/diagrams/logs to ClickHouse
- Traefik routes `/observe/*` to cameleer-server with forward-auth middleware
- Forward-auth endpoint at `/auth/verify` validates JWT, returns `X-Tenant-Id`, `X-User-Id`, `X-User-Email` headers
## Component 1: Serve cameleer3-server Dashboard
## Component 1: Serve cameleer-server Dashboard
### Traefik Routing
Add Traefik labels to the cameleer3-server service in `docker-compose.yml` to serve the React SPA:
Add Traefik labels to the cameleer-server service in `docker-compose.yml` to serve the React SPA:
```yaml
# Existing (Phase 3):
@@ -49,23 +49,23 @@ Add Traefik labels to the cameleer3-server service in `docker-compose.yml` to se
- traefik.http.services.dashboard.loadbalancer.server.port=8080
```
The cameleer3-server SPA is served from its own embedded web server. The SPA already calls the server's API endpoints at relative paths — the existing `/observe/*` Traefik route handles those requests with forward-auth.
The cameleer-server SPA is served from its own embedded web server. The SPA already calls the server's API endpoints at relative paths — the existing `/observe/*` Traefik route handles those requests with forward-auth.
**Note:** If the cameleer3-server SPA expects to be served from `/` rather than `/dashboard`, a Traefik StripPrefix middleware may be needed:
**Note:** If the cameleer-server SPA expects to be served from `/` rather than `/dashboard`, a Traefik StripPrefix middleware may be needed:
```yaml
- traefik.http.middlewares.dashboard-strip.stripprefix.prefixes=/dashboard
- traefik.http.routers.dashboard.middlewares=forward-auth,dashboard-strip
```
This depends on how the cameleer3-server SPA is configured (base path). To be verified during implementation.
This depends on how the cameleer-server SPA is configured (base path). To be verified during implementation.
### CAMELEER_TENANT_ID Configuration
Set `CAMELEER_TENANT_ID` on the cameleer3-server service so all ingested data is tagged with the real tenant slug:
Set `CAMELEER_TENANT_ID` on the cameleer-server service so all ingested data is tagged with the real tenant slug:
```yaml
cameleer3-server:
cameleer-server:
environment:
CAMELEER_TENANT_ID: ${CAMELEER_TENANT_SLUG:-default}
```
@@ -76,7 +76,7 @@ Add `CAMELEER_TENANT_SLUG` to `.env.example`.
## Component 2: Agent Connectivity Verification
New endpoint in cameleer-saas to check whether a deployed app's agent has successfully registered with cameleer3-server and is sending data.
New endpoint in cameleer-saas to check whether a deployed app's agent has successfully registered with cameleer-server and is sending data.
### API
@@ -100,15 +100,15 @@ public record AgentStatusResponse(
### Implementation
`AgentStatusService` in cameleer-saas calls cameleer3-server's agent registry API:
`AgentStatusService` in cameleer-saas calls cameleer-server's agent registry API:
```
GET http://cameleer3-server:8081/api/v1/agents
GET http://cameleer-server:8081/api/v1/agents
```
This returns the list of registered agents. The service filters by `applicationId` matching the app's slug and `environmentId` matching the environment's slug.
If the cameleer3-server doesn't expose a public agent listing endpoint, the alternative is to query ClickHouse directly for recent data:
If the cameleer-server doesn't expose a public agent listing endpoint, the alternative is to query ClickHouse directly for recent data:
```sql
SELECT max(timestamp) as last_seen
@@ -212,17 +212,17 @@ cameleer:
### Startup Verification
On application startup, cameleer-saas verifies that cameleer3-server is reachable:
On application startup, cameleer-saas verifies that cameleer-server is reachable:
```java
@EventListener(ApplicationReadyEvent.class)
public void verifyConnectivity() {
// HTTP GET http://cameleer3-server:8081/actuator/health
// Log result: "cameleer3-server connectivity: OK" or "FAILED: ..."
// HTTP GET http://cameleer-server:8081/actuator/health
// Log result: "cameleer-server connectivity: OK" or "FAILED: ..."
}
```
This is a best-effort check, not a hard dependency. If cameleer3-server is not yet running (e.g., starting up), the SaaS platform still starts. The check is logged for diagnostics.
This is a best-effort check, not a hard dependency. If cameleer-server is not yet running (e.g., starting up), the SaaS platform still starts. The check is logged for diagnostics.
### ClickHouse Data Verification
@@ -259,10 +259,10 @@ This requires cameleer-saas to query ClickHouse directly (the `clickHouseDataSou
## Docker Compose Changes
### cameleer3-server labels (add dashboard route)
### cameleer-server labels (add dashboard route)
```yaml
cameleer3-server:
cameleer-server:
environment:
CAMELEER_TENANT_ID: ${CAMELEER_TENANT_SLUG:-default}
labels:
@@ -304,18 +304,18 @@ cameleer:
1. Deploy a sample Camel REST app with `exposedPort: 8080`
2. `curl http://order-svc.default.acme.localhost` hits the Camel app
3. The Camel route processes the request
4. cameleer3 agent captures the trace and sends to cameleer3-server
4. cameleer agent captures the trace and sends to cameleer-server
5. `GET /api/apps/{appId}/agent-status` shows `registered: true, state: ACTIVE`
6. `GET /api/apps/{appId}/observability-status` shows `hasTraces: true`
7. Open `http://localhost/dashboard` — cameleer3-server SPA loads
7. Open `http://localhost/dashboard` — cameleer-server SPA loads
8. Traces visible in the dashboard for the deployed app
9. Route topology graph shows the Camel route structure
10. `CAMELEER_TENANT_ID` is set to the tenant slug in ClickHouse data
## What Phase 4 Does NOT Touch
- No changes to cameleer3-server code (works as-is for single-tenant Docker mode)
- No changes to the cameleer3 agent
- No new ClickHouse schemas (cameleer3-server manages its own)
- No changes to cameleer-server code (works as-is for single-tenant Docker mode)
- No changes to the cameleer agent
- No new ClickHouse schemas (cameleer-server manages its own)
- No SaaS management UI (Phase 9)
- No K8s-specific changes (Phase 5)

View File

@@ -7,19 +7,19 @@
## Context
Phases 1-4 built the complete backend: tenants, licensing, environments, app deployment with JAR upload, async deployment pipeline, container logs, agent status, observability status, and inbound HTTP routing. The cameleer3-server observability dashboard is already served at `/dashboard`. But there is no management UI — all operations require curl/API calls.
Phases 1-4 built the complete backend: tenants, licensing, environments, app deployment with JAR upload, async deployment pipeline, container logs, agent status, observability status, and inbound HTTP routing. The cameleer-server observability dashboard is already served at `/dashboard`. But there is no management UI — all operations require curl/API calls.
Phase 9 adds the SaaS management shell: a React SPA for managing tenants, environments, apps, and deployments. The observability UI is already handled by cameleer3-server — this shell covers everything else.
Phase 9 adds the SaaS management shell: a React SPA for managing tenants, environments, apps, and deployments. The observability UI is already handled by cameleer-server — this shell covers everything else.
## Key Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Location | `ui/` directory in cameleer-saas repo | Matches cameleer3-server pattern. Single build pipeline. Spring Boot serves the SPA. |
| Location | `ui/` directory in cameleer-saas repo | Matches cameleer-server pattern. Single build pipeline. Spring Boot serves the SPA. |
| Relationship to dashboard | Two separate SPAs, linked via navigation | SaaS shell at `/`, observability at `/dashboard`. Same design system = cohesive feel. No coupling. |
| Layout | Sidebar navigation | Consistent with cameleer3-server dashboard. Same AppShell pattern from design system. |
| Layout | Sidebar navigation | Consistent with cameleer-server dashboard. Same AppShell pattern from design system. |
| Auth | Shared Logto OIDC session | Same client ID, same localStorage keys. True SSO between SaaS shell and observability dashboard. |
| Tech stack | React 19 + Vite + React Router + Zustand + TanStack Query | Identical to cameleer3-server SPA. Same patterns, same libraries, same conventions. |
| Tech stack | React 19 + Vite + React Router + Zustand + TanStack Query | Identical to cameleer-server SPA. Same patterns, same libraries, same conventions. |
| Design system | `@cameleer/design-system` v0.1.31 | Shared component library. CSS Modules + design tokens. Dark theme. |
| RBAC | Frontend role-based visibility | Roles from JWT claims. Hide/disable UI for unauthorized actions. Backend enforces — frontend is UX only. |
@@ -39,7 +39,7 @@ Phase 9 adds the SaaS management shell: a React SPA for managing tenants, enviro
2. If not authenticated, redirect to Logto OIDC authorize endpoint
3. Logto callback at `/callback` — exchange code for tokens
4. Store `accessToken`, `refreshToken`, `username`, `roles` in Zustand + localStorage
5. Tokens stored with same keys as cameleer3-server SPA: `cameleer-access-token`, `cameleer-refresh-token`
5. Tokens stored with same keys as cameleer-server SPA: `cameleer-access-token`, `cameleer-refresh-token`
6. API client injects `Authorization: Bearer {token}` on all requests
7. On 401, attempt token refresh; on failure, redirect to login
@@ -128,7 +128,7 @@ Frontend RBAC implementation:
- Sidebar uses `Sidebar` + `TreeView` components from design system
- Environment → App hierarchy is collapsible
- "View Dashboard" is an external link to `/dashboard` (cameleer3-server SPA)
- "View Dashboard" is an external link to `/dashboard` (cameleer-server SPA)
- Sidebar collapses on small screens (responsive)
## API Integration
@@ -181,7 +181,7 @@ ui/
│ ├── main.tsx — React root + providers
│ ├── router.tsx — React Router config
│ ├── auth/
│ │ ├── auth-store.ts — Zustand store (same keys as cameleer3-server)
│ │ ├── auth-store.ts — Zustand store (same keys as cameleer-server)
│ │ ├── LoginPage.tsx
│ │ ├── CallbackPage.tsx
│ │ └── ProtectedRoute.tsx
@@ -302,14 +302,14 @@ import { ThemeProvider, ToastProvider, BreadcrumbProvider } from '@cameleer/desi
6. Deploy triggers async deployment, status polls and updates live
7. Agent status shows registered/connected
8. Container logs stream in LogViewer
9. "View Dashboard" link navigates to `/dashboard` (cameleer3-server SPA)
9. "View Dashboard" link navigates to `/dashboard` (cameleer-server SPA)
10. Shared auth: no re-login when switching between SPAs
11. RBAC: VIEWER cannot see deploy button, DEVELOPER cannot delete environments
12. Production build: `npm run build` + `mvn package` produces JAR with embedded SPA
## What Phase 9 Does NOT Touch
- No changes to cameleer3-server or its SPA
- No changes to cameleer-server or its SPA
- No billing UI (Phase 6)
- No team management (Logto org admin — deferred)
- No tenant settings/profile page

View File

@@ -2,7 +2,7 @@
**Date:** 2026-04-05
**Status:** Draft
**Scope:** cameleer-saas (large), cameleer3-server (small), cameleer3 agent (none)
**Scope:** cameleer-saas (large), cameleer-server (small), cameleer agent (none)
## Problem Statement
@@ -13,7 +13,7 @@ The current cameleer-saas authentication implementation has three overlapping id
1. **Logto is the single identity provider** for all human users across all components.
2. **Zero trust** — every service validates tokens independently via JWKS or its own signing key. No identity in HTTP headers. The JWT is the proof.
3. **No custom crypto** — use standard libraries and protocols (OAuth2, OIDC, JWT). No hand-rolled JWT generation or validation.
4. **Server-per-tenant** — each tenant gets their own cameleer3-server instance. The SaaS platform provisions and manages them.
4. **Server-per-tenant** — each tenant gets their own cameleer-server instance. The SaaS platform provisions and manages them.
5. **API keys for agents** — per-environment opaque secrets, exchanged for server-issued JWTs via the existing bootstrap registration flow.
6. **Self-hosted compatible** — same stack, single Logto org, single tenant. No special code paths.
@@ -52,9 +52,9 @@ The current cameleer-saas authentication implementation has three overlapping id
|-------|--------|-----------|-----------|---------|
| Logto user JWT | Logto | ES384 (asymmetric) | Any service via JWKS | SaaS UI users, server dashboard users |
| Logto M2M JWT | Logto | ES384 (asymmetric) | Any service via JWKS | SaaS platform → server API calls |
| Server internal JWT | cameleer3-server | HS256 (symmetric) | Issuing server only | Agents (after registration) |
| API key (opaque) | SaaS platform | N/A (hashed at rest) | cameleer3-server (bootstrap validator) | Agent initial registration |
| Ed25519 signature | cameleer3-server | EdDSA | Agent | Server → agent command integrity |
| Server internal JWT | cameleer-server | HS256 (symmetric) | Issuing server only | Agents (after registration) |
| API key (opaque) | SaaS platform | N/A (hashed at rest) | cameleer-server (bootstrap validator) | Agent initial registration |
| Ed25519 signature | cameleer-server | EdDSA | Agent | Server → agent command integrity |
### Authentication flows
@@ -65,19 +65,19 @@ The current cameleer-saas authentication implementation has three overlapping id
4. `organization_id` claim in JWT → resolves to internal tenant ID
5. Roles come from JWT claims (Logto org roles), not Management API calls
**Human user → cameleer3-server dashboard:**
**Human user → cameleer-server dashboard:**
1. User authenticates with Logto (OIDC flow, server configured via existing admin API)
2. Server exchanges auth code for ID token, validates via provider JWKS
3. Server issues internal HMAC JWT with mapped roles
4. Existing flow, no changes needed
**SaaS platform → cameleer3-server API (M2M):**
**SaaS platform → cameleer-server API (M2M):**
1. SaaS platform obtains Logto M2M access token (`client_credentials` grant)
2. Calls tenant server API with `Authorization: Bearer <logto-m2m-token>`
3. Server validates via Logto JWKS (new capability — see server changes below)
4. Server grants ADMIN role to valid M2M tokens
**Agent → cameleer3-server:**
**Agent → cameleer-server:**
1. Agent reads `CAMELEER_API_KEY` env var (fallback: `CAMELEER_AUTH_TOKEN` for backward compat)
2. Calls `POST /api/v1/agents/register` with `Authorization: Bearer <api-key>`
3. Server validates via `BootstrapTokenValidator` (constant-time comparison, unchanged)
@@ -96,7 +96,7 @@ The current cameleer-saas authentication implementation has three overlapping id
## Component Changes
### cameleer3 (agent) — NO CHANGES
### cameleer (agent) — NO CHANGES
The agent's authentication flow is correct as designed:
- Reads API key from environment variable
@@ -106,13 +106,13 @@ The agent's authentication flow is correct as designed:
The only optional change is renaming `CAMELEER_AUTH_TOKEN` to `CAMELEER_API_KEY` for clarity, with backward-compatible fallback. This is cosmetic and can be done at any time.
### cameleer3-server — SMALL CHANGES
### cameleer-server — SMALL CHANGES
The server needs one new capability: accepting Logto access tokens (asymmetric JWT) in addition to its own internal HMAC JWTs. This enables the SaaS platform to call server APIs using M2M tokens.
#### Change 1: Add `spring-boot-starter-oauth2-resource-server` dependency
**File:** `cameleer3-server-app/pom.xml`
**File:** `cameleer-server-app/pom.xml`
Add:
```xml
@@ -124,7 +124,7 @@ Add:
#### Change 2: Add OIDC resource server properties
**File:** `cameleer3-server-app/src/main/resources/application.yml`
**File:** `cameleer-server-app/src/main/resources/application.yml`
Add under `security:`:
```yaml
@@ -360,7 +360,7 @@ cameleer:
public-key-path: ${CAMELEER_JWT_PUBLIC_KEY_PATH:}
```
Remove the `keys/` directory mount from `docker-compose.yml`. The SaaS platform does not sign anything — Ed25519 signing lives in cameleer3-server only.
Remove the `keys/` directory mount from `docker-compose.yml`. The SaaS platform does not sign anything — Ed25519 signing lives in cameleer-server only.
#### REWRITE: `SecurityConfig.java`
@@ -680,7 +680,7 @@ Keep as-is. Serves frontend configuration. No auth changes needed.
Update to set `CAMELEER_OIDC_ISSUER_URI` and `CAMELEER_OIDC_AUDIENCE` on the tenant server:
```bash
# Add to the cameleer3-server environment in docker-compose or bootstrap output:
# Add to the cameleer-server environment in docker-compose or bootstrap output:
CAMELEER_OIDC_ISSUER_URI=http://logto:3001/oidc
CAMELEER_OIDC_AUDIENCE=https://api.cameleer.local
```
@@ -727,7 +727,7 @@ This is a new development — no production data exists. All database schemas, m
### Implementation Order
1. **Phase 1**: Update cameleer3-server (add OIDC resource server support). Deploy.
1. **Phase 1**: Update cameleer-server (add OIDC resource server support). Deploy.
2. **Phase 2**: Rewrite cameleer-saas backend (clean security config, API key management, Logto-only auth). Deploy with frontend changes atomically.
3. **Phase 3**: Update bootstrap script (set OIDC env vars on server, stop reading Logto DB directly).

View File

@@ -0,0 +1,184 @@
# Configurable Base Path for SaaS App
## Problem
Logto uses many root-level paths (`/sign-in`, `/register`, `/consent`, `/social`, `/api/interaction`, `/api/experience`, `/assets`, etc.) that conflict with the SaaS app's catch-all routing. Enumerating Logto's paths in Traefik is fragile and keeps growing.
## Solution
Move the SaaS app to a configurable base path (default: `/platform`). Logto becomes the Traefik catch-all. Zero path enumeration — any path Logto adds in the future just works.
## Routing
| Path | Target | Priority |
|------|--------|----------|
| `/platform/*` | cameleer-saas:8080 | default |
| `/server/*` | cameleer-server-ui:80 | default |
| `/*` | logto:3001 (catch-all) | 1 (lowest) |
## Configuration
```env
# .env
CONTEXT_PATH=/platform # Change to /saas, /app, etc. No rebuild needed.
```
## Implementation
### 1. Spring Boot — `application.yml`
```yaml
server:
servlet:
context-path: ${CONTEXT_PATH:/platform}
```
Spring automatically prefixes all endpoints. Controllers, SecurityConfig matchers, interceptor patterns — all relative to context-path. No changes needed in Java code.
### 2. Vite — `ui/vite.config.ts`
Build with relative base so assets work from any prefix:
```ts
build: {
outDir: 'dist',
emptyOutDir: true,
assetsDir: '_app',
// removed: base (default '/' for dev, entrypoint injects <base> for production)
},
```
Change `base` to `'./'` so index.html references become relative:
```html
<!-- Before: <script src="/_app/index.js"> (absolute, breaks with prefix) -->
<!-- After: <script src="_app/index.js"> (relative, works from any base) -->
```
### 3. Container entrypoint — inject `<base href>`
Create `docker/entrypoint.sh`:
```sh
#!/bin/sh
# Inject <base href> into index.html for runtime base path support
CONTEXT_PATH="${CONTEXT_PATH:-/platform}"
sed -i "s|<head>|<head><base href=\"${CONTEXT_PATH}/\">|" /app/static/index.html
exec java -jar /app/app.jar
```
In Dockerfile (or docker-compose override for dev):
```yaml
entrypoint: ["sh", "/app/entrypoint.sh"]
```
For dev mode (mounted dist), the `docker-compose.dev.yml` entrypoint runs the sed on the mounted file.
### 4. Frontend — derive base path at runtime
**`ui/src/config.ts`** — use `document.baseURI`:
```ts
const basePath = new URL(document.baseURI).pathname.replace(/\/$/, '');
// basePath = "/platform"
fetch(basePath + '/api/config')
```
**`ui/src/api/client.ts`** — dynamic API base:
```ts
const basePath = new URL(document.baseURI).pathname.replace(/\/$/, '');
const API_BASE = basePath + '/api';
```
**`ui/src/main.tsx`** — router basename:
```tsx
const basePath = new URL(document.baseURI).pathname.replace(/\/$/, '') || '/';
<BrowserRouter basename={basePath}>
```
### 5. Docker Compose — Traefik labels
**cameleer-saas:**
```yaml
labels:
- traefik.http.routers.saas.rule=PathPrefix(`${CONTEXT_PATH:-/platform}`)
- traefik.http.routers.saas.entrypoints=websecure
- traefik.http.routers.saas.tls=true
- traefik.http.services.saas.loadbalancer.server.port=8080
```
**logto (catch-all):**
```yaml
labels:
- traefik.http.routers.logto.rule=PathPrefix(`/`)
- traefik.http.routers.logto.priority=1
- traefik.http.routers.logto.entrypoints=websecure
- traefik.http.routers.logto.tls=true
- traefik.http.services.logto.loadbalancer.server.port=3001
```
Remove all the enumerated Logto paths — Logto is now the catch-all.
### 6. Logto ENDPOINT
Logto's ENDPOINT stays at root (no prefix):
```yaml
ENDPOINT: ${PUBLIC_PROTOCOL:-https}://${PUBLIC_HOST:-localhost}
```
OIDC issuer = `https://domain.com/oidc`. Same domain as the SPA.
### 7. Bootstrap — redirect URIs
Update redirect URIs to include the context path:
```sh
SPA_REDIRECT_URIS="[\"${PROTO}://${HOST}${CONTEXT_PATH}/callback\"]"
SPA_POST_LOGOUT_URIS="[\"${PROTO}://${HOST}${CONTEXT_PATH}/login\"]"
```
Pass `CONTEXT_PATH` to the bootstrap container.
### 8. Tests — `application-test.yml`
```yaml
server:
servlet:
context-path: /platform
```
Test MockMvc paths are relative to context-path, so existing test paths (`/api/tenants`, etc.) continue to work without changes.
## Files to modify
- `src/main/resources/application.yml` — add context-path property
- `src/main/resources/application-test.yml` — add context-path for tests
- `ui/vite.config.ts``base: './'` for relative assets
- `ui/src/config.ts` — derive base path from `document.baseURI`
- `ui/src/api/client.ts` — dynamic `API_BASE`
- `ui/src/main.tsx``BrowserRouter basename` from `document.baseURI`
- `docker/entrypoint.sh` — NEW, injects `<base href>` into index.html
- `docker-compose.yml` — Traefik labels (SaaS at `/platform`, Logto catch-all), pass `CONTEXT_PATH` to bootstrap
- `docker/logto-bootstrap.sh` — context path in redirect URIs
## Files that do NOT change
- All 11 Java controllers — Spring context-path handles prefix transparently
- `SecurityConfig.java` — matchers are relative to context-path
- `WebConfig.java` — interceptor pattern relative to context-path
- `ui/src/api/hooks.ts` — uses centralized `API_BASE`
- All test files — MockMvc is context-path aware
## Customer experience
```env
# .env
PUBLIC_HOST=cameleer.mycompany.com
PUBLIC_PROTOCOL=https
CONTEXT_PATH=/platform
# DNS: 1 record
# cameleer.mycompany.com → server IP
# docker compose up -d
# SaaS at https://cameleer.mycompany.com/platform/
# Logto at https://cameleer.mycompany.com/ (login, OIDC)
# Server UI at https://cameleer.mycompany.com/server/
```

View File

@@ -13,7 +13,7 @@ Path-based routing on one domain. SaaS app at `/platform`, server-ui at `/server
| Path | Target | Priority | Notes |
|------|--------|----------|-------|
| `/platform/*` | cameleer-saas:8080 | default | Spring context-path `/platform` |
| `/server/*` | cameleer3-server-ui:80 | default | Strip-prefix + `BASE_PATH=/server` |
| `/server/*` | cameleer-server-ui:80 | default | Strip-prefix + `BASE_PATH=/server` |
| `/` | redirect → `/platform/` | 100 | Via `docker/traefik-dynamic.yml` |
| `/*` | logto:3001 | 1 (lowest) | Catch-all: sign-in, OIDC, assets |
@@ -49,7 +49,7 @@ PUBLIC_PROTOCOL=https
- Custom `JwtDecoder`: ES384 algorithm, `at+jwt` token type, split issuer-uri / jwk-set-uri
- Redirect URIs: `${PROTO}://${HOST}/platform/callback`
## Server Integration (cameleer3-server)
## Server Integration (cameleer-server)
| Env var | Value | Purpose |
|---------|-------|---------|
@@ -64,12 +64,12 @@ Server OIDC requirements:
- `X-Forwarded-Prefix` support for correct redirect_uri construction
- Branding endpoint (`/api/v1/branding/logo`) must be publicly accessible
## Server UI (cameleer3-server-ui)
## Server UI (cameleer-server-ui)
| Env var | Value | Purpose |
|---------|-------|---------|
| `BASE_PATH` | `/server` | React Router basename + `<base>` tag |
| `CAMELEER_API_URL` | `http://cameleer3-server:8081` | nginx API proxy target |
| `CAMELEER_API_URL` | `http://cameleer-server:8081` | nginx API proxy target |
Traefik strip-prefix removes `/server` before forwarding to nginx. Server-ui injects `<base href="/server/">` via `BASE_PATH`.
@@ -80,7 +80,7 @@ Traefik strip-prefix removes `/server` before forwarding to nginx. Server-ui inj
SPA_REDIRECT_URIS=["${PROTO}://${HOST}/platform/callback"]
SPA_POST_LOGOUT_URIS=["${PROTO}://${HOST}/platform/login"]
# Traditional (cameleer3-server) — both variants until X-Forwarded-Prefix is consistent
# Traditional (cameleer-server) — both variants until X-Forwarded-Prefix is consistent
TRAD_REDIRECT_URIS=["${PROTO}://${HOST}/oidc/callback","${PROTO}://${HOST}/server/oidc/callback"]
TRAD_POST_LOGOUT_URIS=["${PROTO}://${HOST}","${PROTO}://${HOST}/server"]
```

View File

@@ -2,11 +2,11 @@
## Problem
Logto's default sign-in page uses Logto branding. While we configured colors and logos via `PATCH /api/sign-in-exp`, control over layout, typography, and components is limited. The sign-in experience is visually inconsistent with the cameleer3-server login page.
Logto's default sign-in page uses Logto branding. While we configured colors and logos via `PATCH /api/sign-in-exp`, control over layout, typography, and components is limited. The sign-in experience is visually inconsistent with the cameleer-server login page.
## Goal
Replace Logto's sign-in UI with a custom React SPA that visually matches the cameleer3-server login page, using `@cameleer/design-system` components for consistency across all deployment models.
Replace Logto's sign-in UI with a custom React SPA that visually matches the cameleer-server login page, using `@cameleer/design-system` components for consistency across all deployment models.
## Scope
@@ -54,9 +54,9 @@ The interaction cookie is set by `/oidc/auth` before the user lands on the sign-
## Visual Design
Matches cameleer3-server LoginPage exactly:
Matches cameleer-server LoginPage exactly:
- Centered `Card` (400px max-width, 32px padding)
- Logo: favicon.svg + "cameleer3" text (24px bold)
- Logo: favicon.svg + "cameleer" text (24px bold)
- Random witty subtitle (13px muted)
- `FormField` + `Input` for username and password
- Amber `Button` (primary variant, full-width)

View File

@@ -2,7 +2,7 @@
## Problem
When a Logto user SSOs into the cameleer3-server, they get `VIEWER` role by default (OIDC auto-signup). There's no automatic mapping between Logto organization roles and server roles. A SaaS admin must manually promote users in the server.
When a Logto user SSOs into the cameleer-server, they get `VIEWER` role by default (OIDC auto-signup). There's no automatic mapping between Logto organization roles and server roles. A SaaS admin must manually promote users in the server.
## Constraint

View File

@@ -2,7 +2,7 @@
**Date:** 2026-04-07
**Status:** Ready for review
**Scope:** cameleer3 (agent), cameleer3-server, cameleer-saas
**Scope:** cameleer (agent), cameleer-server, cameleer-saas
**Focus:** Responsibility boundaries, architectural fitness, simplification opportunities
**Not in scope:** Security hardening, code quality, performance
@@ -12,9 +12,9 @@
The cameleer ecosystem has a clear vision: a standalone observability and runtime platform for Apache Camel, optionally managed by a thin SaaS vendor layer. Both deployment modes must be first-class.
The agent (cameleer3) is architecturally clean. Single job, well-defined protocol.
The agent (cameleer) is architecturally clean. Single job, well-defined protocol.
The server (cameleer3-server) is solid for observability but currently lacks runtime management capabilities (deploying and managing Camel application containers). These capabilities exist in the SaaS layer today but belong in the server, since standalone customers also need them.
The server (cameleer-server) is solid for observability but currently lacks runtime management capabilities (deploying and managing Camel application containers). These capabilities exist in the SaaS layer today but belong in the server, since standalone customers also need them.
The SaaS layer (cameleer-saas) has taken on too many responsibilities: environment management, app lifecycle, container orchestration, direct ClickHouse access, and partial auth duplication. It should be a thin vendor management plane: onboard tenants, provision server instances, manage billing. Nothing more.
@@ -29,17 +29,17 @@ The SaaS layer (cameleer-saas) has taken on too many responsibilities: environme
## What's Working Well
### Agent (cameleer3)
- Clean separation: core logic in `cameleer3-core`, protocol models in `cameleer3-common`, delivery mechanisms (agent/extension) as thin wrappers
### Agent (cameleer)
- Clean separation: core logic in `cameleer-core`, protocol models in `cameleer-common`, delivery mechanisms (agent/extension) as thin wrappers
- Well-defined agent-server protocol (PROTOCOL.md) with versioning
- Dual-mode design (Java agent + Quarkus extension) is elegant
- Compatibility matrix across 40 Camel versions demonstrates maturity
- No changes needed
### Server (cameleer3-server)
### Server (cameleer-server)
- Two-database pattern (PostgreSQL control plane, ClickHouse observability data) is correct
- In-memory agent registry with heartbeat-based auto-recovery is operationally sound
- `cameleer3-server-core` / `cameleer3-server-app` split keeps domain logic framework-free
- `cameleer-server-core` / `cameleer-server-app` split keeps domain logic framework-free
- SSE command push with Ed25519 signing is well-designed
- The UI is competitive-grade (per UX audit #100)
- Independent user/group/role management works for standalone deployments
@@ -392,7 +392,7 @@ Step 8 is the SaaS cleanup after server capabilities are in place.
- **saas#37 (admin tenant creation UI):** SaaS UI becomes vendor-focused, simpler
### Issues that become more important:
- **agent#33 (version cameleer3-common independently):** Critical before server API contract stabilizes
- **agent#33 (version cameleer-common independently):** Critical before server API contract stabilizes
- **server#46 (OIDC PKCE for SPA):** Required for server-ui in oidc-only mode
- **server#101 (onboarding experience):** Server UI needs guided setup for standalone users

View File

@@ -30,7 +30,7 @@ This spec redesigns the platform around two personas — **vendor** (us) and **c
| ID | Story | Acceptance Criteria |
|----|-------|-------------------|
| V1 | As a vendor, I want to create a tenant so I can onboard a new customer | Form collects name, slug, tier. Creates DB record + Logto org. Status = PROVISIONING. |
| V2 | As a vendor, I want to provision a server for a tenant so they have a running Cameleer instance | After tenant creation, SaaS creates a cameleer3-server container via Docker API with correct env vars, network, and Traefik labels. Health check passes → status = ACTIVE. |
| V2 | As a vendor, I want to provision a server for a tenant so they have a running Cameleer instance | After tenant creation, SaaS creates a cameleer-server container via Docker API with correct env vars, network, and Traefik labels. Health check passes → status = ACTIVE. |
| V3 | As a vendor, I want to generate and assign a license to a tenant | License created with tier-appropriate features/limits/expiry. Token pushed to tenant's server via M2M API. |
| V4 | As a vendor, I want to suspend a tenant who hasn't paid | Suspend stops the server container and marks tenant SUSPENDED. Reactivation restarts it. |
| V5 | As a vendor, I want to view fleet health at a glance | Tenant list shows each tenant's server status (running/stopped/error), agent count vs limit, license expiry. |
@@ -135,7 +135,7 @@ public class TenantProvisionerAutoConfig {
| Config | Value | Source |
|--------|-------|--------|
| Image | `gitea.siegeln.net/cameleer/cameleer3-server:${VERSION}` | Global config |
| Image | `gitea.siegeln.net/cameleer/cameleer-server:${VERSION}` | Global config |
| Name | `cameleer-server-${tenant.slug}` | Derived from tenant |
| Network | `cameleer` + `cameleer-traefik` | Fixed networks from compose |
| DNS alias | `cameleer-server-${tenant.slug}` | For SaaS→server M2M calls |
@@ -146,7 +146,7 @@ public class TenantProvisionerAutoConfig {
| Env var | Value | Purpose |
|---------|-------|---------|
| `SPRING_DATASOURCE_URL` | `jdbc:postgresql://postgres:5432/cameleer3` | Shared PostgreSQL |
| `SPRING_DATASOURCE_URL` | `jdbc:postgresql://postgres:5432/cameleer` | Shared PostgreSQL |
| `CAMELEER_TENANT_ID` | `${tenant.slug}` | Tenant isolation key |
| `CAMELEER_OIDC_ISSUER_URI` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/oidc` | Logto as initial OIDC |
| `CAMELEER_OIDC_JWK_SET_URI` | `http://logto:3001/oidc/jwks` | Docker-internal JWK |
@@ -168,12 +168,12 @@ traefik.http.services.server-${slug}.loadbalancer.server.port=8081
**Server UI container per tenant:**
Each tenant also gets a `cameleer3-server-ui` container:
Each tenant also gets a `cameleer-server-ui` container:
| Config | Value |
|--------|-------|
| Name | `cameleer-server-ui-${tenant.slug}` |
| Image | `gitea.siegeln.net/cameleer/cameleer3-server-ui:${VERSION}` |
| Image | `gitea.siegeln.net/cameleer/cameleer-server-ui:${VERSION}` |
| Env | `BASE_PATH=/t/${slug}` |
| Traefik | `PathPrefix(/t/${slug})` with `priority=2` (higher than API) |
@@ -467,11 +467,11 @@ New migration `V011`:
## 8. Existing Compose Stack Changes
The default `cameleer3-server` and `cameleer3-server-ui` containers in docker-compose.yml become the "bootstrap" server for the `default` tenant. When provisioning is enabled, new tenants get their own dynamically-created containers.
The default `cameleer-server` and `cameleer-server-ui` containers in docker-compose.yml become the "bootstrap" server for the `default` tenant. When provisioning is enabled, new tenants get their own dynamically-created containers.
The existing compose stack continues to work as-is for development. The provisioner creates additional containers alongside the compose-managed ones.
For the `default` tenant (created by bootstrap), the SaaS recognizes the existing compose-managed server and doesn't try to provision a new one. This is detected by checking if a container named `cameleer-server-default` (or the compose-managed `cameleer3-server`) already exists.
For the `default` tenant (created by bootstrap), the SaaS recognizes the existing compose-managed server and doesn't try to provision a new one. This is detected by checking if a container named `cameleer-server-default` (or the compose-managed `cameleer-server`) already exists.
---

View File

@@ -376,9 +376,9 @@ Import from `lucide-react`.
### 4.5 Sign-In Branding
**Problem:** Login says "cameleer3" — internal repo name, not product brand.
**Problem:** Login says "cameleer" — internal repo name, not product brand.
**Fix:** Change to "Cameleer" (product name). Update the page title from "Sign in — cameleer3" to "Sign in — Cameleer".
**Fix:** Change to "Cameleer" (product name). Update the page title from "Sign in — cameleer" to "Sign in — Cameleer".
**Files:** `ui/sign-in/src/SignInPage.tsx`

View File

@@ -0,0 +1,52 @@
# Fleet Health at a Glance (#44)
## Context
The vendor tenant list at `/vendor/tenants` shows basic tenant info (name, slug, tier, status, server state, license expiry) but lacks live usage data. The vendor needs to see agent and environment counts per tenant to understand fleet utilization without clicking into each tenant.
## Design
### Backend
**Extend `VendorTenantSummary`** record in `VendorTenantController.java` with three fields:
```java
int agentCount, int environmentCount, int agentLimit
```
**In the list endpoint mapping**: for each ACTIVE tenant with a server endpoint, call `serverApiClient.getAgentCount(endpoint)` and `serverApiClient.getEnvironmentCount(endpoint)`. Agent limit comes from the tenant's license `limits.agents` field (-1 for unlimited). Use `CompletableFuture` to parallelize the N HTTP calls.
Tenants that are PROVISIONING/SUSPENDED/DELETED get zeroes — skip the HTTP calls.
### Frontend
**Add two columns** to the DataTable in `VendorTenantsPage.tsx`:
| Column | Key | Display |
|--------|-----|---------|
| Agents | `agentCount` | "3 / 10" or "0 / ∞" (uses agentCount + agentLimit) |
| Envs | `environmentCount` | "1" |
Place them after the Server column, before License.
**Update `VendorTenantSummary` type** in `ui/src/types/api.ts` to include `agentCount: number`, `environmentCount: number`, `agentLimit: number`.
### Existing infrastructure reused
- `ServerApiClient.getAgentCount()` and `getEnvironmentCount()` — already implemented for tenant dashboard
- `LicenseService.getActiveLicense()` — already used in the list endpoint for license expiry
- 30-second refetch interval on `useVendorTenants()` — already in place
### No changes to
- ServerStatusBadge column — kept as-is
- License column — kept as-is
- Detail page — already has its own health data
## Files to modify
| File | Change |
|------|--------|
| `VendorTenantController.java` | Extend summary record + parallel health fetch in list endpoint |
| `VendorTenantsPage.tsx` | Add Agents and Envs columns |
| `ui/src/types/api.ts` | Add fields to VendorTenantSummary type |

View File

@@ -0,0 +1,164 @@
# Externalize Docker Compose Templates
**Date:** 2026-04-15
**Status:** Approved
## Problem
The installer scripts (`install.sh` and `install.ps1`) generate docker-compose YAML inline via heredocs and string interpolation. This causes three problems:
1. **Maintainability** -- ~450 lines of compose generation are duplicated across two scripts in two languages. Every compose change must be made twice.
2. **Readability** -- The compose structure is buried inside 1800-line scripts and difficult to review or edit.
3. **User customization** -- Users cannot tweak the compose after installation without re-running the installer or editing generated output that gets overwritten on next run.
## Design
Replace inline compose generation with static template files. The installer collects user input, writes `.env`, copies templates, and runs `docker compose up -d`.
### File Layout
```
installer/
templates/
docker-compose.yml # Infra: traefik, postgres, clickhouse
docker-compose.server.yml # Standalone: server + server-ui
docker-compose.saas.yml # SaaS: logto + cameleer-saas
docker-compose.tls.yml # Overlay: custom cert volume on traefik
docker-compose.monitoring.yml # Overlay: override monitoring network to external
.env.example # Documented variable reference
install.sh
install.ps1
```
### Compose File Responsibilities
#### `docker-compose.yml` (infra base, always loaded)
- **Services:** `cameleer-traefik`, `cameleer-postgres`, `cameleer-clickhouse`
- **Prometheus labels** on all services unconditionally (harmless when no scraper connects)
- **Logto console port** always mapped; bind address controlled by `${LOGTO_CONSOLE_BIND:-127.0.0.1}` (exposed = `0.0.0.0`, unexposed = localhost-only)
- **Networks:** `cameleer`, `cameleer-traefik`, `monitoring` (local noop bridge by default)
- **Volumes:** `cameleer-pgdata`, `cameleer-chdata`, `cameleer-certs`
- All services reference the `monitoring` network
#### `docker-compose.server.yml` (standalone mode)
- **Services:** `cameleer-server`, `cameleer-server-ui`
- **Additional volumes:** `jars`
- **Additional network:** `cameleer-apps`
- **Monitoring network:** defines local noop bridge, both services reference it
#### `docker-compose.saas.yml` (SaaS mode)
- **Services:** `cameleer-logto`, `cameleer-saas`
- **Additional volume:** `cameleer-bootstrapdata`
- **Monitoring network:** defines local noop bridge, both services reference it
#### `docker-compose.tls.yml` (overlay)
- Adds `./certs:/user-certs:ro` volume mount to `cameleer-traefik`
#### `docker-compose.monitoring.yml` (overlay)
- Overrides the `monitoring` network definition from local noop bridge to `external: true` with `name: ${MONITORING_NETWORK}`
- No per-service entries needed -- services already reference the `monitoring` network in their base files
### Monitoring Network Pattern
Each compose file defines the `monitoring` network as a local bridge with a throwaway name:
```yaml
# In docker-compose.yml, docker-compose.server.yml, docker-compose.saas.yml
networks:
monitoring:
name: cameleer-monitoring-noop
```
Services reference this network unconditionally. Without the monitoring overlay, it is an inert local bridge. When `docker-compose.monitoring.yml` is included, Docker Compose merges the network definition, overriding it to external:
```yaml
# docker-compose.monitoring.yml
networks:
monitoring:
external: true
name: ${MONITORING_NETWORK}
```
This avoids conditional YAML injection and keeps a single monitoring overlay file regardless of deployment mode.
### Logto Console Port Handling
Instead of conditionally injecting the port mapping, always include it with a bind address variable:
```yaml
# In docker-compose.yml (traefik ports)
- "${LOGTO_CONSOLE_BIND:-127.0.0.1}:${LOGTO_CONSOLE_PORT:-3002}:3002"
```
The installer writes `LOGTO_CONSOLE_BIND=0.0.0.0` when the user chooses to expose the console, and omits it (defaulting to `127.0.0.1`) when not.
### `.env.example`
Documented reference of all variables, grouped by section:
- **Version/images:** `VERSION`, `CAMELEER_SAAS_PROVISIONING_SERVERIMAGE`, `CAMELEER_SAAS_PROVISIONING_SERVERUIIMAGE`
- **Public access:** `PUBLIC_HOST`, `PUBLIC_PROTOCOL`
- **Ports:** `HTTP_PORT`, `HTTPS_PORT`, `LOGTO_CONSOLE_BIND`, `LOGTO_CONSOLE_PORT`
- **PostgreSQL:** `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`
- **ClickHouse:** `CLICKHOUSE_PASSWORD`
- **Admin credentials (SaaS):** `SAAS_ADMIN_USER`, `SAAS_ADMIN_PASS`
- **Admin credentials (standalone):** `SERVER_ADMIN_USER`, `SERVER_ADMIN_PASS`, `BOOTSTRAP_TOKEN`
- **Docker:** `DOCKER_SOCKET`, `DOCKER_GID`
- **TLS (optional):** `CERT_FILE`, `KEY_FILE`, `CA_FILE`
- **Monitoring (optional):** `MONITORING_NETWORK`
- **Compose file assembly:** `COMPOSE_FILE`
- **TLS validation:** `NODE_TLS_REJECT`
### `COMPOSE_FILE` Assembly
The installer builds the `COMPOSE_FILE` value in `.env` based on user choices:
| Mode | COMPOSE_FILE |
|---|---|
| Standalone | `docker-compose.yml:docker-compose.server.yml` |
| Standalone + TLS | `docker-compose.yml:docker-compose.server.yml:docker-compose.tls.yml` |
| Standalone + monitoring | `docker-compose.yml:docker-compose.server.yml:docker-compose.monitoring.yml` |
| SaaS | `docker-compose.yml:docker-compose.saas.yml` |
| SaaS + TLS | `docker-compose.yml:docker-compose.saas.yml:docker-compose.tls.yml` |
| SaaS + TLS + monitoring | `docker-compose.yml:docker-compose.saas.yml:docker-compose.tls.yml:docker-compose.monitoring.yml` |
### Installer Script Changes
**Removed:**
- `generate_compose_file()` / `Generate-ComposeFile` (~250 lines each, SaaS mode)
- `generate_compose_file_standalone()` / `Generate-ComposeFileStandalone` (~200 lines each, standalone mode)
- All conditional YAML injection logic (logto console, TLS, monitoring, GID)
**Added:**
- Copy template files from `templates/` to install directory
- `COMPOSE_FILE` assembly logic in `.env` generation
- `LOGTO_CONSOLE_BIND` variable (replaces conditional port injection)
- `DOCKER_GID` written to `.env` (replaces inline `stat` in heredoc)
**Unchanged:**
- User prompting, argument parsing, config file reading
- Password generation, Docker GID detection
- `credentials.txt`, `INSTALL.md`, `cameleer.conf` generation
- `docker compose up -d` invocation
### Password Fallback Safety
All password variables in templates use `:?` (fail-if-unset) instead of `:-` (default) to prevent silent fallback to weak credentials:
```yaml
SAAS_ADMIN_PASS: ${SAAS_ADMIN_PASS:?SAAS_ADMIN_PASS must be set in .env}
```
Username defaults (`:-admin`) are acceptable and retained.
### Migration for Existing Installs
Existing `installer/cameleer/` has a single generated `docker-compose.yml`. On re-install, the installer copies the template files and regenerates `.env`. The old monolithic compose file is replaced by the multi-file set. `cameleer.conf` preserves user choices, so re-running the installer reproduces the same configuration with the new file structure.

View File

@@ -0,0 +1,147 @@
# Per-Tenant PostgreSQL Isolation
**Date:** 2026-04-15
**Status:** Approved
## Context
The cameleer-server team introduced `currentSchema` and `ApplicationName` JDBC parameters (commit `7a63135`) to scope admin diagnostic queries to a single tenant's connections. Previously, all tenant servers shared one PostgreSQL user and connected to the `cameleer` database without schema isolation — a tenant's server could theoretically see SQL text from other tenants via `pg_stat_activity`.
This spec adds per-tenant PostgreSQL users and schemas so each tenant server can only access its own data at the database level.
## Architecture
### Current State
- All tenant servers connect as the shared admin PG user to `cameleer` database, `public` schema.
- No per-tenant schemas exist — the server's Flyway runs in `public`.
- `TenantDataCleanupService` already attempts `DROP SCHEMA tenant_<slug>` on delete (no-op today since schemas don't exist).
- Standalone mode sets `currentSchema=tenant_default` in the compose file and is unaffected by this change.
### Target State
- Each tenant gets a dedicated PG user (`tenant_<slug>`) and schema (`tenant_<slug>`).
- The tenant user owns only its schema. `REVOKE ALL ON SCHEMA public` prevents cross-tenant access.
- The server's Flyway runs inside `tenant_<slug>` via the `currentSchema` JDBC parameter.
- `ApplicationName=tenant_<slug>` scopes `pg_stat_activity` visibility per the server team's convention.
- On tenant delete, both schema and user are dropped.
## New Component: `TenantDatabaseService`
A focused service with two methods:
```java
@Service
public class TenantDatabaseService {
void createTenantDatabase(String slug, String password);
void dropTenantDatabase(String slug);
}
```
### `createTenantDatabase(slug, password)`
Connects to `cameleer` using the admin PG credentials from `ProvisioningProperties`. Executes:
1. Validate slug against `^[a-z0-9-]+$` (reject unexpected characters).
2. `CREATE USER "tenant_<slug>" WITH PASSWORD '<password>'` (skip if user already exists — idempotent for re-provisioning).
3. `CREATE SCHEMA "tenant_<slug>" AUTHORIZATION "tenant_<slug>"` (skip if schema already exists).
4. `REVOKE ALL ON SCHEMA public FROM "tenant_<slug>"`.
All identifiers are double-quoted. The password is a 32-character random alphanumeric string generated by the same `SecureRandom` utility used for other credential generation.
### `dropTenantDatabase(slug)`
1. `DROP SCHEMA IF EXISTS "tenant_<slug>" CASCADE`
2. `DROP USER IF EXISTS "tenant_<slug>"`
Schema must be dropped first (with `CASCADE`) because PG won't drop a user that owns objects.
## Entity Change
**New Flyway migration:** `V014__add_tenant_db_password.sql`
```sql
ALTER TABLE tenants ADD COLUMN db_password VARCHAR(255);
```
Nullable — existing tenants won't have it. Code checks for null and falls back to shared credentials for backwards compatibility.
**TenantEntity:** new `dbPassword` field with JPA `@Column` mapping.
## Provisioning Flow Changes
### `VendorTenantService.provisionAsync()` — new steps before container creation
```
1. Generate 32-char random password
2. tenantDatabaseService.createTenantDatabase(slug, password)
3. entity.setDbPassword(password)
4. tenantRepository.save(entity)
5. tenantProvisioner.provision(request) ← request now includes dbPassword
6. ... rest unchanged (health check, license push, OIDC push)
```
If step 2 fails, provisioning aborts with a stored error — no orphaned containers.
### `DockerTenantProvisioner` — JDBC URL construction
The `ProvisionRequest` record gains `dbPassword` field.
**When `dbPassword` is present** (new tenants):
```
SPRING_DATASOURCE_URL=jdbc:postgresql://cameleer-postgres:5432/cameleer?currentSchema=tenant_<slug>&ApplicationName=tenant_<slug>
SPRING_DATASOURCE_USERNAME=tenant_<slug>
SPRING_DATASOURCE_PASSWORD=<generated>
```
**When `dbPassword` is null** (pre-existing tenants, backwards compat):
```
SPRING_DATASOURCE_URL=<props.datasourceUrl()> (no currentSchema/ApplicationName)
SPRING_DATASOURCE_USERNAME=<props.datasourceUsername()>
SPRING_DATASOURCE_PASSWORD=<props.datasourcePassword()>
```
Server restart/upgrade re-creates containers via `provisionAsync()`, which re-reads `dbPassword` from the entity. Restarting an upgraded tenant picks up isolated credentials automatically.
## Delete Flow Changes
### `VendorTenantService.delete()`
```
1. tenantProvisioner.remove(slug) ← existing
2. licenseService.revokeLicense(...) ← existing
3. logtoClient.deleteOrganization(...) ← existing
4. tenantDatabaseService.dropTenantDatabase(slug) ← replaces TenantDataCleanupService PG logic
5. dataCleanupService.cleanupClickHouse(slug) ← ClickHouse cleanup stays separate
6. entity.setStatus(DELETED) ← existing
```
`TenantDataCleanupService` loses its PostgreSQL cleanup responsibility (delegated to `TenantDatabaseService`). It keeps only the ClickHouse cleanup. Rename method to `cleanupClickHouse(slug)` for clarity.
## Backwards Compatibility
| Scenario | Behavior |
|----------|----------|
| **Standalone mode** | Unaffected. Server is in compose, not provisioned by SaaS. Defaults to `tenant_default`. |
| **Existing SaaS tenants** (dbPassword=null) | Shared credentials, no `currentSchema`. Same as before. |
| **Existing tenants after restart/upgrade** | Still use shared credentials until re-provisioned with new code. |
| **New tenants** | Isolated user+schema+JDBC URL. Full isolation. |
| **Delete of pre-existing tenant** | `DROP USER IF EXISTS` is a no-op (user doesn't exist). Schema drop unchanged. |
## InfrastructureService
No changes needed. Already queries `information_schema.schemata WHERE schema_name LIKE 'tenant_%'`. With per-tenant schemas now created, the PostgreSQL tenant table on the Infrastructure page will populate automatically.
## Files Changed
| File | Change |
|------|--------|
| `TenantDatabaseService.java` | **New** — create/drop PG user+schema |
| `TenantEntity.java` | Add `dbPassword` field |
| `V014__add_tenant_db_password.sql` | **New** — nullable column |
| `VendorTenantService.java` | Call `createTenantDatabase` in provision, `dropTenantDatabase` in delete |
| `DockerTenantProvisioner.java` | Construct per-tenant JDBC URL, username, password |
| `ProvisionRequest` record | Add `dbPassword` field |
| `TenantDataCleanupService.java` | Remove PG logic, keep ClickHouse only, rename method |

View File

@@ -68,7 +68,7 @@ The sidebar provides access to all major sections:
| **Environments** | Expandable tree showing all environments and their apps |
| **License** | License tier, features, limits, and token |
| **Platform** | Platform-wide tenant management (visible only to platform admins) |
| **View Dashboard** | Opens the observability dashboard (cameleer3-server) in a new tab |
| **View Dashboard** | Opens the observability dashboard (cameleer-server) in a new tab |
| **Account** | Log out of the current session |
The Environments section in the sidebar renders as a collapsible tree: environments at the top level, with their applications nested underneath. Clicking any item navigates directly to its detail page.
@@ -239,10 +239,10 @@ Failed deployments are highlighted with a red accent for quick identification.
### Accessing the Observability Dashboard
The observability dashboard is provided by cameleer3-server and opens in a separate browser tab:
The observability dashboard is provided by cameleer-server and opens in a separate browser tab:
1. In the sidebar footer, click **View Dashboard**.
2. The dashboard opens at the cameleer3-server URL.
2. The dashboard opens at the cameleer-server URL.
Alternatively, from any app detail page, the **Agent Status** card includes a "View in Dashboard" link.
@@ -456,11 +456,11 @@ The platform runs as a Docker Compose stack with these services:
| Service | Purpose | Dev Port |
|---------|---------|----------|
| **traefik** | Reverse proxy and TLS termination | 80, 443 |
| **postgres** | Database for platform data, Logto, and cameleer3-server | 5432 |
| **postgres** | Database for platform data, Logto, and cameleer-server | 5432 |
| **logto** | Identity provider (OIDC/SSO) | 3001, 3002 |
| **logto-bootstrap** | One-time setup (runs and exits) | -- |
| **cameleer-saas** | SaaS API server and frontend | 8080 |
| **cameleer3-server** | Observability backend | 8081 |
| **cameleer-server** | Observability backend | 8081 |
| **clickhouse** | Trace, metrics, and log storage | 8123 |
In production mode (`docker compose up`), only ports 80 and 443 are exposed via Traefik. In development mode (`docker compose -f docker-compose.yml -f docker-compose.dev.yml up`), individual service ports are exposed directly for debugging.
@@ -469,11 +469,11 @@ In production mode (`docker compose up`), only ports 80 and 443 are exposed via
On first boot, the `logto-bootstrap` container automatically:
1. Waits for Logto and cameleer3-server to be healthy.
1. Waits for Logto and cameleer-server to be healthy.
2. Creates three Logto applications:
- **Cameleer SaaS** (SPA) -- for the management UI frontend.
- **Cameleer SaaS Backend** (Machine-to-Machine) -- for server-to-Logto API calls.
- **Cameleer Dashboard** (Traditional Web App) -- for cameleer3-server OIDC login.
- **Cameleer Dashboard** (Traditional Web App) -- for cameleer-server OIDC login.
3. Creates an API resource (`https://api.cameleer.local`) with 10 OAuth2 scopes (see Section 9).
4. Creates organization roles with **API resource scopes** (not standalone org permissions):
- `admin` -- 9 tenant scopes (all except `platform:admin`).
@@ -482,7 +482,7 @@ On first boot, the `logto-bootstrap` container automatically:
- Platform admin (default: `admin` / `admin`) -- has the `admin` org role plus the global `platform-admin` role (which grants `platform:admin` scope).
- Demo user (default: `camel` / `camel`) -- added to the default organization with the `member` role.
6. Creates a Logto organization ("Example Tenant") and assigns both users.
7. Configures cameleer3-server with Logto OIDC settings for dashboard authentication.
7. Configures cameleer-server with Logto OIDC settings for dashboard authentication.
8. Writes all generated IDs and secrets to `/data/logto-bootstrap.json` for the SaaS backend to consume.
The bootstrap is idempotent -- re-running it will skip resources that already exist.
@@ -571,15 +571,15 @@ The Cameleer SaaS application itself does not need any changes -- all identity c
**Possible causes:**
- The agent cannot reach the cameleer3-server endpoint. Check network connectivity between the deployed container and the observability server.
- The agent cannot reach the cameleer-server endpoint. Check network connectivity between the deployed container and the observability server.
- The bootstrap token does not match. The agent uses `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` to register with the server.
- The cameleer3-server is not healthy.
- The cameleer-server is not healthy.
**Resolution:**
1. Check cameleer3-server health: `docker compose logs cameleer3-server`.
1. Check cameleer-server health: `docker compose logs cameleer-server`.
2. Verify the app container's logs for agent connection errors (use the Logs tab on the app detail page).
3. Confirm that `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` is the same in both the `cameleer-saas` and `cameleer3-server` service configurations.
3. Confirm that `CAMELEER_SERVER_SECURITY_BOOTSTRAPTOKEN` is the same in both the `cameleer-saas` and `cameleer-server` service configurations.
### Container Health Check Failing

32
installer/CLAUDE.md Normal file
View File

@@ -0,0 +1,32 @@
# Installer
## Deployment Modes
The installer (`installer/install.sh`) supports two deployment modes:
| | Multi-tenant SaaS (`DEPLOYMENT_MODE=saas`) | Standalone (`DEPLOYMENT_MODE=standalone`) |
|---|---|---|
| **Containers** | traefik, postgres, clickhouse, logto, cameleer-saas | traefik, postgres, clickhouse, server, server-ui |
| **Auth** | Logto OIDC (SaaS admin + tenant users) | Local auth (built-in admin, no identity provider) |
| **Tenant management** | SaaS admin creates/manages tenants via UI | Single server instance, no fleet management |
| **PostgreSQL** | `cameleer-postgres` image (multi-DB init) | Stock `postgres:16-alpine` (server creates schema via Flyway) |
| **Use case** | Platform vendor managing multiple customers | Single customer running the product directly |
Standalone mode generates a simpler compose with the server running directly. No Logto, no SaaS management plane, no bootstrap. The admin logs in with local credentials at `/`.
## Compose templates
The installer uses static docker-compose templates in `installer/templates/`. Templates are copied to the install directory and composed via `COMPOSE_FILE` in `.env`:
- `docker-compose.yml` — shared infrastructure (traefik, postgres, clickhouse)
- `docker-compose.saas.yml` — SaaS mode (logto, cameleer-saas)
- `docker-compose.server.yml` — standalone mode (server, server-ui)
- `docker-compose.tls.yml` — overlay: custom TLS cert volume
- `docker-compose.monitoring.yml` — overlay: external monitoring network
## Env var naming convention
- `CAMELEER_AGENT_*` — agent config (consumed by the Java agent)
- `CAMELEER_SERVER_*` — server config (consumed by cameleer-server)
- `CAMELEER_SAAS_*` — SaaS management plane config
- `CAMELEER_SAAS_PROVISIONING_*` — "SaaS forwards this to provisioned tenant servers"
- No prefix (e.g. `POSTGRES_PASSWORD`, `PUBLIC_HOST`) — shared infrastructure, consumed by multiple components

Some files were not shown because too many files have changed in this diff Show More