ADR-021: CLARISSA Portal - System & Security Architecture¶
| Status | Proposed |
|---|---|
| Date | 2026-01-22 |
| Authors | Wolfram Laube, Claude (AI Assistant) |
| Supersedes | - |
| Related | ADR-020 (Portal Vision), ADR-022 (Software Architecture), ADR-016 (Runner Load Balancing) |
Context¶
CLARISSA requires a central portal for the distributed team (Schรคrding + Houston). The portal is being built during the experimental phase by ~4 developers, but should later be used productively by significantly more users.
This ADR defines: 1. System Architecture: Infrastruktur, Deployment, Services 2. Security Architecture: Authentication, Authorization, Session Management
The Software Architecture (Hexagonal Pattern, API Design, Code Structure) is documented in ADR-022.
Decision¶
Key Decisions¶
| Decision | Choice | Rationale |
|---|---|---|
| Compute | Cloud Run (GCP) | Scale-to-zero, $0 Free Tier, Team-Zugang |
| Local Dev | OpenWhisk Standalone | Benchmarking, no GCP dependency for devs |
| Database | Firestore | Serverless, Free Tier, Document Model passt |
| Storage | GCS | PDFs, Standard, Free Tier |
| Frontend Hosting | GitLab Pages | Already available, $0 |
| Auth Provider | GitLab (OIDC) | Team already has GitLab accounts |
| OAuth Flow | Authorization Code + PKCE | Best Practice, Defense-in-Depth |
System Architecture¶
Overview¶
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ GitLab Pages (Static Frontend) โ
โ https://clarissa.gitlab.io โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Portal SPA (Alpine.js) โ MkDocs Documentation โ โ
โ โ / Landing โ /docs/ ADRs, Guides โ โ
โ โ /portal/time Time Track โ /docs/notebooks Jupyter โ โ
โ โ /portal/billing Invoices โ โ โ
โ โ /portal/benchmarks Runner Grid โ โ โ
โ โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ REST API (authenticated)
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ GCP Cloud Run (All Services) โ
โ Scale-to-Zero, $0/month (Free Tier) โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Portal API Service โ โ
โ โ โโโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ Auth โ Time โ Benchmarks โ Billing (Light) โ โ โ
โ โ โ (OIDC) โ (CRUD) โ (Read) โ (CRUD, Status) โ โ โ
โ โ โ ~200ms โ ~100ms โ ~150ms โ ~100ms โ โ โ
โ โ โโโโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโ โ
โ โ async HTTP โ โ โ
โ โผ โผ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Worker Service (Heavy Lifting) โ โ
โ โ โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ billing/ โ benchmarks/ โ (extensible) โ โ โ
โ โ โ generate-pdf โ aggregate โ โ โ โ
โ โ โ ~30-60s โ ~10-30s โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ Timeout: 15min โ Retry: 3x exponential โ Scale: 0-10 โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โโโโโโโโดโโโโโโโ โ
โ โ Firestore โ โ
โ โ (NoSQL) โ โ
โ โโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Apache OpenWhisk (Linux Yoga) - LOCAL โ
โ Development & Benchmarking Only โ
โ โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ billing/ โ benchmarks/ โ clarissa/ โ โ
โ โ generate-pdf โ aggregate โ simulate (Future) โ โ
โ โ (same core) โ (same core) โ (unbounded runtime) โ โ
โ โโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Setup: OpenWhisk Standalone (java -jar) OR K3s + Helm โ
โ Purpose: Local dev, A/B benchmarking vs Cloud Run, Future simulations โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Cloud Run Services¶
| Service | Purpose | Timeout | Memory | Public |
|---|---|---|---|---|
clarissa-portal-api |
Auth, Time, Billing CRUD, Benchmarks | 30s | 512Mi | โ Yes |
clarissa-worker |
PDF Generation, Aggregation | 15min | 1Gi | โ No (IAM) |
# Portal API Deployment
gcloud run deploy clarissa-portal-api \
--image=gcr.io/$PROJECT/clarissa-portal-api:$SHA \
--region=europe-west1 \
--platform=managed \
--allow-unauthenticated \
--timeout=30 \
--memory=512Mi \
--set-env-vars="WORKER_URL=https://clarissa-worker-xxx.run.app"
# Worker Service Deployment
gcloud run deploy clarissa-worker \
--image=gcr.io/$PROJECT/clarissa-worker:$SHA \
--region=europe-west1 \
--platform=managed \
--no-allow-unauthenticated \
--timeout=900 \
--memory=1Gi \
--max-instances=10 \
--min-instances=0
OpenWhisk (Local Development)¶
For development and benchmarking on Linux Yoga:
# Option A: Standalone (recommended for dev)
# 1GB RAM, 2 Minuten Setup
java -jar openwhisk-standalone.jar
# Option B: K3s + Helm (for K8s overhead benchmarks)
# 4GB RAM, ~1h Setup
helm install owdev openwhisk/openwhisk -n openwhisk --create-namespace
Team Distribution¶
| Rolle | Maschine | Cloud Run | OpenWhisk |
|---|---|---|---|
| Mike, Ian, Doug (Houston) | Any | โ Cloud Run only | โ Not needed |
| Wolfram (Schรคrding) | Linux Yoga | โ Cloud Run | โ OpenWhisk (Dev/Benchmark) |
Security Architecture¶
Access Model¶
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ACCESS MODEL โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ GitLab Pages (PUBLIC) โ โ
โ โ โ โ
โ โ โข Static HTML/JS/CSS โ โ Anyone can see โ
โ โ โข No secrets, no sensitive data โ โ
โ โ โข Login button redirects to OIDC โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โ API Calls (require valid session) โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Portal API (PROTECTED) โ โ
โ โ โ โ
โ โ โข All endpoints require auth โ โ Only authenticated users โ
โ โ โข Session via HttpOnly cookie โ โ
โ โ โข CORS: only *.gitlab.io โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โ Service-to-Service (IAM) โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Worker Service (INTERNAL) โ โ
โ โ โ โ
โ โ โข No public access โ โ Only Portal API can call โ
โ โ โข GCP IAM authentication โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key Point: GitLab Pages are public - this is OK because: - No secrets in frontend code - No API keys in JavaScript - All sensitive data only via authenticated API calls
GitLab als OIDC Provider¶
GitLab.com ist ein vollwertiger OpenID Connect Provider:
OIDC Discovery URL:
https://gitlab.com/.well-known/openid-configuration
Endpoints:
โโโ authorization: https://gitlab.com/oauth/authorize
โโโ token: https://gitlab.com/oauth/token
โโโ userinfo: https://gitlab.com/oauth/userinfo
โโโ jwks_uri: https://gitlab.com/oauth/discovery/keys
OAuth Flow: Authorization Code + PKCE¶
Why PKCE even though we are a Confidential Client? - Defense-in-Depth: Protects against Authorization Code Interception - Best Practice: OAuth 2.1 will require PKCE for all clients - Future-proof: If we add mobile apps later - No downside: Minimal overhead
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Authorization Code + PKCE Flow โ
โ โ
โ Browser Portal API GitLab OIDC โ
โ โ โ โ โ
โ โ 1. Click Login โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโบโ โ โ
โ โ โ โ โ
โ โ โ Generate: โ โ
โ โ โ - code_verifier (random 32 bytes) โ
โ โ โ - code_challenge = SHA256(verifier) โ
โ โ โ - state (CSRF token) โ โ
โ โ โ โ โ
โ โ โ Store verifier in โ โ
โ โ โ Redis (TTL 10min) โ โ
โ โ โ โ โ
โ โ 2. Redirect โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ Location: gitlab.com/oauth/authorize? โ โ
โ โ response_type=code โ โ
โ โ client_id=xxx โ โ
โ โ redirect_uri=.../callback โ โ
โ โ scope=openid+profile+email โ โ
โ โ state=csrf_token โ โ
โ โ code_challenge=abc123... โโโ PKCE โ โ
โ โ code_challenge_method=S256 โโโ PKCE โ โ
โ โ โ โ โ
โ โ 3. User Login + Consent โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโบโ โ
โ โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ โ โ
โ โ 4. Redirect with code โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ ?code=AUTH_CODE&state=csrf_token โ โ
โ โ โ โ โ
โ โ 5. Send code to backend โ โ
โ โโโโโโโโโโโโโโโโโโโโโโบโ โ โ
โ โ โ โ โ
โ โ โ 6. Token Exchange โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโบโ โ
โ โ โ POST /oauth/token โ โ
โ โ โ code=AUTH_CODE โ โ
โ โ โ code_verifier=... โโโ PKCE โ
โ โ โ client_id + secret โ โ
โ โ โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ { access_token, โ โ
โ โ โ id_token, โ โ
โ โ โ refresh_token } โ โ
โ โ โ โ โ
โ โ โ 7. Validate id_token โ โ
โ โ โ - Signature (JWKS) โ โ
โ โ โ - Issuer, Audience โ โ
โ โ โ - Expiry โ โ
โ โ โ โ โ
โ โ 8. Session Cookie โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ Set-Cookie: session=...; HttpOnly; Secure โ โ
โ โ โ โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
GitLab Application Setup¶
GitLab โ User Settings โ Applications โ New Application
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Add new application โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Name: CLARISSA Portal โ
โ โ
โ Redirect URI: https://clarissa-portal-api-xxxxx.run.app โ
โ /api/v1/auth/callback โ
โ โ
โ Confidential: โ Yes โ
โ โ
โ Scopes: โ openid (OIDC ID Token) โ
โ โ profile (Name, Avatar) โ
โ โ email (Email-Adresse) โ
โ โ
โ [Save application] โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ Application ID + Secret in GCP Secret Manager speichern โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Session Management¶
| Aspekt | Implementation |
|---|---|
| Session Token | JWT, selbst signiert (HS256) |
| Storage | HttpOnly Cookie |
| Lifetime | 1 Stunde |
| Refresh | Via GitLab refresh_token (in Firestore) |
| PKCE Verifier | Redis mit 10min TTL |
Service-to-Service Auth¶
Portal API โ Worker:
from google.auth.transport.requests import Request
from google.oauth2 import id_token
# Get GCP identity token for Worker service
auth_req = Request()
token = id_token.fetch_id_token(auth_req, WORKER_URL)
response = await client.post(
f"{WORKER_URL}/worker/billing/generate-pdf",
headers={"Authorization": f"Bearer {token}"}
)
Security Summary¶
| Aspect | Implementation |
|---|---|
| Identity Provider | GitLab.com (OIDC) |
| OAuth Flow | Authorization Code + PKCE (S256) |
| OIDC Scopes | openid profile email |
| Session Storage | HttpOnly Cookie (JWT, 1h) |
| Token Refresh | GitLab refresh_token in Firestore |
| PKCE Verifier | Redis (10min TTL) |
| CORS | Restrict to *.gitlab.io |
| CSRF Protection | state parameter |
| Secrets | GCP Secret Manager |
| Service Auth | GCP IAM (Portal API โ Worker) |
CI/CD Pipeline¶
# .gitlab-ci.yml (vereinfacht)
stages:
- test
- build
- deploy
- pages
variables:
PORTAL_API_IMAGE: gcr.io/$GCP_PROJECT/clarissa-portal-api
WORKER_IMAGE: gcr.io/$GCP_PROJECT/clarissa-worker
REGION: europe-west1
build:portal-api:
stage: build
image: gcr.io/kaniko-project/executor:latest
script:
- /kaniko/executor
--context=.
--dockerfile=services/portal-api/Dockerfile
--destination=$PORTAL_API_IMAGE:$CI_COMMIT_SHA
--destination=$PORTAL_API_IMAGE:latest
rules:
- if: $CI_COMMIT_BRANCH == "main"
changes: [services/portal-api/**/*, shared/**/*]
build:worker:
stage: build
image: gcr.io/kaniko-project/executor:latest
script:
- /kaniko/executor
--context=.
--dockerfile=services/worker/Dockerfile
--destination=$WORKER_IMAGE:$CI_COMMIT_SHA
--destination=$WORKER_IMAGE:latest
rules:
- if: $CI_COMMIT_BRANCH == "main"
changes: [services/worker/**/*, shared/**/*]
deploy:portal-api:
stage: deploy
image: google/cloud-sdk:slim
script:
- echo "$GCP_SERVICE_KEY" | gcloud auth activate-service-account --key-file=-
- gcloud run deploy clarissa-portal-api
--image=$PORTAL_API_IMAGE:$CI_COMMIT_SHA
--region=$REGION
--platform=managed
--allow-unauthenticated
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy:worker:
stage: deploy
image: google/cloud-sdk:slim
script:
- echo "$GCP_SERVICE_KEY" | gcloud auth activate-service-account --key-file=-
- gcloud run deploy clarissa-worker
--image=$WORKER_IMAGE:$CI_COMMIT_SHA
--region=$REGION
--platform=managed
--no-allow-unauthenticated
--timeout=900
rules:
- if: $CI_COMMIT_BRANCH == "main"
pages:
stage: pages
script:
- pip install mkdocs mkdocs-material
- mkdocs build --site-dir public/docs
- cp -r frontend/portal/* public/
artifacts:
paths: [public]
rules:
- if: $CI_COMMIT_BRANCH == "main"
Cost Analysis¶
| Component | Monthly Cost | Notes |
|---|---|---|
| Cloud Run: Portal API | $0 | Free tier: 2M requests |
| Cloud Run: Worker | $0 | Free tier: 180K vCPU-s |
| Firestore | $0 | Free tier: 1GB, 50K reads/day |
| GCS | ~$0.02 | 5GB free |
| OpenWhisk | $0 | Self-hosted (Linux Yoga) |
| GitLab Pages | $0 | Included |
| Total | ~$0/month | Within free tiers |
Scaling Projections¶
| Users | Requests/mo | Est. Cost |
|---|---|---|
| 4 (dev) | ~30K | $0 |
| 50 | ~150K | $0 |
| 500 | ~1.5M | $0 |
| 1000+ | ~3M+ | ~$10+ |
Implementation Roadmap¶
Phase 1: Foundation (Week 1-2)¶
- [ ] GitLab Application registrieren
- [ ] Cloud Run Services skeleton (FastAPI)
- [ ] Firestore collections setup
- [ ] OIDC Auth flow mit PKCE implementieren
Phase 2: Core Infrastructure (Week 3-4)¶
- [ ] Worker Service deployment
- [ ] Service-to-Service Auth (IAM)
- [ ] CI/CD Pipeline komplett
Phase 3: OpenWhisk (Week 5)¶
- [ ] OpenWhisk Standalone auf Linux Yoga
- [ ] Benchmark comparison Cloud Run vs OpenWhisk