From d53afe43cc62db9bbbd0d23b04e93f014a6e4b01 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Wed, 15 Apr 2026 00:26:36 +0200 Subject: [PATCH] docs: update CLAUDE.md for per-tenant PG isolation and consolidated migrations - 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) --- CLAUDE.md | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 827fdec..c136f8e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,7 @@ Agent-server protocol is defined in `cameleer3/cameleer3-common/PROTOCOL.md`. Th - `MeController.java` — GET /api/me (authenticated user, tenant list) **tenant/** — Tenant data model -- `TenantEntity.java` — JPA entity (id, name, slug, tier, status, logto_org_id, stripe IDs, settings JSONB) +- `TenantEntity.java` — JPA entity (id, name, slug, tier, status, logto_org_id, stripe IDs, settings JSONB, db_password) **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 @@ -44,8 +44,9 @@ Agent-server protocol is defined in `cameleer3/cameleer3-common/PROTOCOL.md`. Th **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 +- `DockerTenantProvisioner.java` — Docker implementation, creates per-tenant server + UI containers with per-tenant JDBC credentials (`currentSchema=tenant_{slug}&ApplicationName=tenant_{slug}`). `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. +- `TenantDatabaseService.java` — creates/drops per-tenant PostgreSQL users (`tenant_{slug}`) and schemas; used during provisioning and delete +- `TenantDataCleanupService.java` — GDPR data erasure on tenant delete: deletes ClickHouse data across all tables with `tenant_id` column (PostgreSQL cleanup handled by `TenantDatabaseService`) - `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 @@ -177,6 +178,9 @@ These env vars are injected into provisioned per-tenant server containers: | Env var | Value | Purpose | |---------|-------|---------| +| `SPRING_DATASOURCE_URL` | `jdbc:postgresql://cameleer-postgres:5432/cameleer3?currentSchema=tenant_{slug}&ApplicationName=tenant_{slug}` | Per-tenant schema isolation + diagnostic query scoping | +| `SPRING_DATASOURCE_USERNAME` | `tenant_{slug}` | Per-tenant PG user (owns only its schema) | +| `SPRING_DATASOURCE_PASSWORD` | (generated, stored in `TenantEntity.dbPassword`) | Per-tenant PG password | | `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. | @@ -296,9 +300,10 @@ When SaaS admin creates a tenant via `VendorTenantService`: 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 +7. Create per-tenant PostgreSQL user + schema via `TenantDatabaseService.createTenantDatabase(slug, password)`, store `dbPassword` on entity +8. Create tenant-isolated Docker network (`cameleer-tenant-{slug}`) +9. Create server container with per-tenant JDBC URL (`currentSchema=tenant_{slug}&ApplicationName=tenant_{slug}`), Traefik labels (`traefik.docker.network`), health check, Docker socket bind, JAR volume, certs volume (ro) +10. 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 @@ -314,7 +319,8 @@ When SaaS admin creates a tenant via `VendorTenantService`: **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) +- `TenantDatabaseService.dropTenantDatabase(slug)` — drops PostgreSQL `tenant_{slug}` schema + `tenant_{slug}` user +- `TenantDataCleanupService.cleanupClickHouse(slug)` — deletes ClickHouse data across all tables with `tenant_id` column (GDPR) **Password management** (tenant portal): - `POST /api/tenant/password` — tenant admin changes own Logto password (via `@AuthenticationPrincipal` JWT subject) @@ -324,18 +330,7 @@ When SaaS admin creates a tenant via `VendorTenantService`: ## 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