Skip to content

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

References