CELINE Grid
Backend-for-Frontend (BFF) for the Grid Resilience UI (celine-frontend/apps/grid).
It acts as the secure API gateway between the grid dashboard and the CELINE platform, proxying risk data from the Digital Twin and managing per-operator alert rules and notification preferences.
Features
- Grid risk data proxy — forwards wind and heat resilience data (risk rows, nowcast, 8-hour windows, km exposure, trendline, substation layouts, filter metadata, summary) from the Digital Twin to the frontend, scoped to the operator's DSO network
- Alert rules — per-user CRUD for wind/heat risk alert rules, each carrying a threshold (
WARNINGorALERT) and optional recipient override - Notification settings — per-user email recipients and webhook URL for alert delivery
- Automated alert dispatch — listens on MQTT for
grid-resilience-flowpipeline completions, evaluates active alert rules against live DT distributions, and sends nudging events via the nudging-tool - OPA-enforced access control — fine-grained authorization via
policies/grid.rego; DSO org membership drives network ownership; service accounts use OAuth2 scopes
API
The service runs on port 8015. Interactive docs are available at /api/docs.
| Group | Endpoints |
|---|---|
| user | GET /api/me |
| grid — infra | GET /api/grid/{network_id}/substations/map, /filters, /summary |
| grid — CIM topology | GET /api/grid/{network_id}/tile-index, /shapes, /risks, /risks-now, /trendline |
| alerts | GET/POST /api/alert-rules, PATCH/DELETE /api/alert-rules/{id} |
GET/PUT /api/notification-settings |
|
| ops | GET /health |
The CIM topology endpoints proxy ValueFetcherSpec-backed queries from the Digital Twin. /tile-index and /shapes set Cache-Control: public, max-age=3600. /shapes assembles GeoJSON FeatureCollections from per-row feature data. /risks-now returns current nowcasting observations without date filtering.
Local development
Prerequisites
- Python 3.12+,
uv - PostgreSQL at
localhost:15432(credentialspostgres:securepassword123) - MQTT broker (optional — the listener degrades gracefully if unavailable at startup)
Setup
uv sync
docker compose run --rm db-create # create the 'grid' database
task alembic:upgrade # apply migrations
task run # hot-reload on :8015
For debugpy attach on port 48015:
task debug
Docker Compose
docker compose up
This runs db-create, migrate, and the api container in order.
Configuration
All settings are read from environment variables or a .env file. The table below lists the most relevant variables.
Posture: CELINE_ENV=dev is required for the dev defaults
The defaults below are development defaults (the local database password, a client
secret equal to the client id, the local Keycloak). They are accepted only when the
process environment says CELINE_ENV=dev (or ENVIRONMENT=dev). Unset means
hardened: so does staging, prod or anything else. Outside dev the service refuses
to start (InsecureConfiguration, listing every offending variable) while any of them is
still in use or the policy bundle did not load, and a policy engine that is missing or
raises denies instead of allowing. See REQ-0013 and REQ-0050.
task run/task debugexportCELINE_ENV=devfor you.CELINE_ENV=staging task runis the prod-like local mode; set real values first.docker compose uppasses only what.envholds — addCELINE_ENV=devthere for a local container.- The check reads the real process environment, not
.envvia pydantic.
This needs celine.sdk.posture, first released in celine-sdk 2.0.0.
| Variable | Default | Description |
|---|---|---|
CELINE_ENV |
— (unset = hardened) | dev relaxes the posture checks; anything else is hardened |
DATABASE_URL |
postgresql+asyncpg://postgres:securepassword123@host.docker.internal:15432/grid |
PostgreSQL DSN |
DIGITAL_TWIN_API_URL |
http://host.docker.internal:8002 |
Digital Twin base URL |
NUDGING_API_URL |
http://host.docker.internal:8016 |
nudging-tool base URL |
DATABASE_ECHO |
false |
Log SQL statements |
CELINE_OIDC_CLIENT_SECRET |
svc-grid (dev only) |
OIDC client secret — must differ from the client id outside dev |
CELINE_OIDC_BASE_URL |
(from sdk default) | OIDC issuer base URL |
CELINE_OIDC_CLIENT_ID |
svc-grid |
OIDC client ID |
DT_CLIENT_SCOPE |
— | OAuth2 scope for outbound DT calls |
NUDGING_SCOPE |
— | OAuth2 scope for outbound nudging calls |
CORS_ORIGINS |
["http://localhost:3006"] |
Allowed CORS origins |
CELINE_POLICIES_DIR |
./policies |
Directory containing .rego policy files |
GRID_PIPELINE_FLOW |
grid-resilience-flow |
Prefect flow name to listen for |
CELINE_MQTT_HOST |
host.docker.internal |
MQTT broker host |
CELINE_MQTT_PORT |
1883 |
MQTT broker port |
MQTT_STARTUP_TIMEOUT_SECONDS |
30.0 |
Maximum startup wait for the MQTT listener |
JWT_HEADER_NAME |
x-auth-request-access-token |
Header carrying the bearer token (set by OAuth2 Proxy) |
Prefixed names come from celine-sdk's own settings models. Until 2026-10-15 the policies
directory was doubly prefixed by construction (CELINE_POLICIES_ + policies_dir, so
CELINE_POLICIES_POLICIES_DIR); the SDK now reads CELINE_POLICIES_DIR and still accepts
the doubled spelling as a fallback. Three rows of this table named variables that did not
exist until 2026-08-15 — MQTT__HOST among them was silently ignored, so following the old
table left the service on its permissive policy fallback with no indication. .env.example was correct throughout and is the
reference if the two ever disagree again.
The default policies path is relative, so the bundle loads only when the process's
working directory is the repository root. See
.agents/knowledge/the-policy-engine-fails-open.md.
Tests
task test # the whole suite, ~11s
task test -- -k "network" # one test
task test -- tests/unit/test_policy.py
No PostgreSQL, no Digital Twin, no nudging-tool, no MQTT broker and no network. The
database is real SQLAlchemy on SQLite, and policies/grid.rego is evaluated for real —
the suite refuses to start if the bundle does not load, because the suite pins
CELINE_ENV=dev, where the policy fails open, and an unloaded bundle would make every
authorisation test pass for the wrong reason.
One opt-in layer does reach a service: tests/integration/ mints real tokens from a
local Keycloak and sends them through the real token check and policy. It is skipped
unless CELINE_GRID_IT_KEYCLOAK names the realm issuer; its docstring lists the users and
variables it expects.
CELINE_GRID_IT_KEYCLOAK=http://keycloak.celine.localhost/realms/celine task test -- tests/integration
- What the service must do, as numbered requirements:
docs/specifications/ - Why the suite is shaped this way:
docs/decisions/ - How to test a change, and what
task testcannot see:.agents/playbooks/testing.md
Database migrations
task alembic:upgrade # apply all pending migrations
task alembic:create -- "my change" # autogenerate a new migration
Release
task release # bumps version via semantic-release, pushes commit + tag
The CI pipeline in .github/workflows/release.yml builds and publishes the Docker image on each version tag.