Code: the `portal` repository. The backend is in `src/portal_service/`, the React app in
`portal-gui/`, the Helm chart in `deployment/`.
---
## O. Code Layout
### O.1 Backend
| Folder | Holds |
|---|---|
| `domain/` | Models, errors, the API product catalog, and the ports (interfaces) the rest implements. No web, database or Keycloak code |
| `application/` | The rules: logging in, sessions, API clients, housekeeping, encryption |
| `adapters/database/` | Tables and repositories (SQLAlchemy, PostgreSQL) |
| `adapters/keycloak/` | Login, token checks, client registration, the Admin API calls |
| `api/` | Routes, wiring (`dependencies.py`), RFC 7807 errors, request-id and logging middleware, serving the app, health |
| `main.py`, `config.py`, `background.py` | Building the app, settings, the housekeeping loop |
import-linter enforces the direction: `domain` imports nothing of ours, and `application` never
imports `api` or `adapters`. Business rules can then be tested with in-memory stand-ins for every
port.
### O.2 React app
| Folder | Holds |
|---|---|
| `app/` | Router, routes, API addresses, and the guards (logged in, logged out, role) |
| `pages/` | One entry per route |
| `features/` | Each page's content |
| `components/ui/` | Base components (shadcn on Radix) |
| `components/common/`, `data-table/`, `forms/` | Pieces every page shares: page states, confirm dialog, tables, form fields |
| `components/<feature>/` | A feature's own components, such as dialogs and table columns |
| `layouts/` | The page frames: sidebar, dashboard and login layouts |
| `hooks/`, `services/`, `types/`, `lib/` | Data hooks, calls to the backend, types, helpers |
Behaviour that more than one page needs lives in the shared components, so every page behaves the
same. A fix to a table, dialog or form goes there, not into one feature.
---
## P. Configuration
Environment variables, nested with `__`. `.env.example` lists the local set; the Helm chart sets
them in the cluster.
| Group | Settings | Notes |
|---|---|---|
| Login | `AUTH__SECURITY_MODE` | `jwt` (default) or `none` for local development only |
| Keycloak | `OIDC__ISSUER`, `OIDC__CLIENT_ID`, `OIDC__CLIENT_SECRET`, `OIDC__REDIRECT_URI` | Required with `jwt`. The other endpoints are derived from the issuer. Helm points the backend's own calls (signing keys, token, Admin API) at Keycloak's in-cluster address; the login and logout pages stay public |
| Session | `SESSION__HTTPS_ONLY`, `SESSION__MAX_AGE` | `HTTPS_ONLY` sets the cookies' `Secure` flag |
| Database | `POSTGRESQL_SETTINGS__URL`, `POSTGRESQL_SETTINGS__CREATE_SCHEMA_ON_STARTUP` | Creating tables is off by default; Helm turns it on |
| OEG | `OEG__BASE_URL`, `OEG__PUBLIC_BASE_URL` | Where `/api/oeg/*` goes, and the address shown to developers |
| API products | `API_PRODUCTS__FILE` | JSON list of products and scopes. The portal does not start without it |
| API clients | `PROVISIONING__CLIENT_ID`, `__CLIENT_SECRET`, `__OEG_CLIENT_UUID`, `__ENCRYPTION_KEY`, `__MAX_CLIENTS_PER_CREATOR` | All absent turns API clients off. The key is 64 hex characters |
| Other | `GUI_DIR`, `CLEANUP__INTERVAL_SECONDS`, `CORS__ALLOWED_ORIGINS`, `APP_VERSION` | `GUI_DIR` is set in the image; empty in development, where Vite serves the app |
---
## Q. Build and Deployment
**Image.** The `Dockerfile` has three stages: Node builds the React app, uv installs the backend,
and the final `python:3.12-slim` image holds only the virtualenv and the built app. It runs as a
non-root user on port 8000.
**Helm.** The chart is in the portal repository (`deployment/`) and, identical, in the helm
| Database | Database and role `portal` from the platform's PostgreSQL chart; an init container waits until the role can log in |
| Keycloak secrets | `oop-portal` and `oop-portal-provisioner` secrets from `keycloak-credentials` |
| Encryption key | Generated into the Secret `portal-credentials` on first install, kept on `helm uninstall` |
| API products | From `global.apiProducts`, the same list Keycloak's product roles are built from |
| Login off | `global.auth.enabled: false` sets `AUTH__SECURITY_MODE=none` |
| Pod | `/healthz` and `/readyz` probes, non-root, read-only root filesystem |
**Local.**`make dev` runs the backend on `:8000` and Vite on `:3000`, which proxies `/api` to the
backend. The repository README has the steps.
---
## R. Observability
-**Logs.** One line when a request arrives and one when it completes, with request id, method,
path, client, status and duration. Security events are logged on their own: why a login
failed, refused cross-site writes, OEG or AI² refusing a token, an orphaned Keycloak client.
Tokens and secrets are never logged. The output is structlog's default, not yet the
platform's JSON format.
-**Request id.**`X-Request-ID` is taken from the request when it is a UUID4, otherwise made,
and returned on the response.
-**Health.**`/healthz` answers while the process runs; `/readyz` checks the database.
-**Metrics.** None yet.
---
## S. Testing
| Kind | Covers | Runs with |
|---|---|---|
| Unit | Rules and routes with in-memory stand-ins for the ports; the Keycloak adapters against fake Keycloak HTTP; encryption, settings | `make test` (runs the integration tests too) |
| Integration | Repositories, routes and concurrent refresh on a real PostgreSQL; the login flow against a Keycloak container with a committed test realm. Optionally, a deployed realm's settings when `KEYCLOAK_ISSUER` is set | `make integration` (testcontainers, needs Docker) |
| Static | ruff (format and lint), mypy strict, import-linter; for the app, Prettier, ESLint, TypeScript | `make check` |
`make check` runs what the GitLab pipeline runs. The React app has no unit tests yet.
---
## T. Known Gaps in Release 2
These are deliberate. Each has a planned home in §U.
| Gap | Why it is acceptable for now |
|---|---|
| Nothing keeps the portal's records and Keycloak in sync (requirements §L.5) | A few trusted operators follow the how-to in the Keycloak chart README |
| API clients belong to their creator, not to an organization | OEG also scopes by creator in Release 2; the table already stores `application_owner_id` |
| A leaked secret is fixed by deleting and recreating the client; products cannot change | Keeps Release 2 to create, list and delete |
| A client an operator deleted in Keycloak cannot be deleted from the portal | Rare; the client is already gone from Keycloak, so only the portal's list is wrong |
| Revoking a user takes up to 5 minutes | OEG checks tokens locally, without asking Keycloak |
| Apps acting for an end user (browser or mobile, with consent) are not supported | Needs browser-login clients and consent, which OEG does not handle yet |
---
## U. Next Release
In the agreed order. Each item says what is missing, how we plan to build it, and what is
already in place.
### U.1 Ownership by organization
-**Missing.** API clients, their lists and their clean-up follow the creator. Product grants are
per user.
`organization`.
### U.2 Keeping the portal and Keycloak in sync
-**Missing.** A disabled developer's clients keep working until an operator deletes them
(ADR-0066).
### U.3 API client lifecycle
-**Missing.** Rotating a secret, disabling and enabling a client, and changing its products.
### U.4 Operator approvals in the portal
-**Missing.** Operators grant products in the Keycloak console.
### U.5 User administration
-**Missing.**`platform-admin` has no page (ADR-0048 decision 4).
### U.6 No-access state and "Your API products"
-**Missing.** A page for a product the user does not hold shows OEG's error.
### U.7 Quality on Demand and Traffic Influence pages
-**Missing.** Only Edge Application Management has pages to work with; the other APIs have the
catalog and "try it".
### U.8 Deployments
-**Missing.** The app treats OEG's `501` on deployments as "not available yet". OEG `develop` now
accepts deployments to several zones; updating a deployment still answers `501`.
### U.9 AI² conversation history
-**Missing.** The list of conversations lives only in the browser tab and is lost on reload.
### U.10 Later
A TMF931-shaped API in front of the same services, then the product catalogue (TMF936) and usage
(TMF937). OEG logging which client called (`azp`) already makes usage per client possible.
---
## Revision History
| Version | Date | Summary |
|---------|------|---------|
| 2.0 | 2026-10-08 | First version, describing the Release 2 implementation and the next release. |