Files
cameleer-saas/docker/CLAUDE.md
hsiegeln 9ed2cedc98
All checks were successful
CI / build (push) Successful in 1m14s
CI / docker (push) Successful in 1m15s
feat: self-service sign-up with email verification and onboarding
Complete sign-up pipeline: email registration via Logto Experience API,
SMTP email verification, and self-service trial tenant creation.

Layer 1 — Logto config:
- Bootstrap Phase 8b: SMTP email connector with branded HTML templates
- Bootstrap Phase 8c: enable SignInAndRegister (email+password sign-up)
- Dockerfile installs official Logto connectors (ensures SMTP available)
- SMTP env vars in docker-compose, installer templates, .env.example

Layer 2 — Experience API (ui/sign-in/experience-api.ts):
- Registration flow: initRegistration → sendVerificationCode → verifyCode
  → addProfile (password) → identifyUser → submit
- Sign-in auto-detects email vs username identifier

Layer 3 — Custom sign-in UI (ui/sign-in/SignInPage.tsx):
- Three-mode state machine: signIn / register / verifyCode
- Reads first_screen=register from URL query params
- Toggle links between sign-in and register views

Layer 4 — Post-registration onboarding:
- OnboardingService: reuses VendorTenantService.createAndProvision(),
  adds calling user to Logto org as owner, enforces one trial per user
- OnboardingController: POST /api/onboarding/tenant (authenticated only)
- OnboardingPage.tsx: org name + auto-slug form
- LandingRedirect: detects zero orgs → redirects to /onboarding
- RegisterPage.tsx: /platform/register initiates OIDC with firstScreen

Installers (install.sh + install.ps1):
- Both prompt for SMTP config in SaaS mode
- CLI args, env var capture, cameleer.conf persistence

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:21:07 +02:00

8.6 KiB

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. Supports both sign-in and self-service registration.

  • Built as custom Logto Docker image (cameleer-logto): ui/sign-in/Dockerfile = node build stage + FROM ghcr.io/logto-io/logto:latest + install official connectors (SMTP) + COPY dist over /etc/logto/packages/experience/dist/
  • Uses @cameleer/design-system components (Card, Input, Button, FormField, Alert)
  • Sign-in: Logto Experience API (4-step: init -> verify password -> identify -> submit -> redirect). Auto-detects email vs username identifier.
  • Registration: 2-phase flow. Phase 1: init Register -> send verification code to email. Phase 2: verify code -> set password -> identify (creates user) -> submit -> redirect.
  • Reads first_screen=register from URL query params to show register form initially (set by @logto/react SDK's firstScreen option)
  • 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)
  6. Configure Logto sign-in branding (Cameleer colors #C6820E/#D4941E, logo from /platform/logo.svg) 8b. Configure SMTP email connector (if SMTP_HOST/SMTP_USER env vars set) — discovers factory via /api/connector-factories, creates connector with Cameleer-branded HTML email templates for Register/SignIn/ForgotPassword/Generic. Skips gracefully if SMTP not configured. 8c. Enable self-service registration — sets signInMode: "SignInAndRegister", signUp: { identifiers: ["email"], password: true, verify: true }, sign-in methods: email+password and username+password (backwards-compatible with admin user).
  7. Cleanup seeded Logto apps
  8. Write bootstrap results to /data/logto-bootstrap.json
  9. Create saas-vendor global role with all API scopes and assign to admin user (always runs — admin IS the platform admin).

SMTP env vars for email verification: SMTP_HOST, SMTP_PORT (default 587), SMTP_USER, SMTP_PASS, SMTP_FROM_EMAIL (default noreply@cameleer.io). Passed to cameleer-logto container via docker-compose. Both installers prompt for these in SaaS mode.

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.