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 (maps, alert distributions, trends, 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 — wind | GET /api/grid/{network_id}/wind/map, /wind/bosco, /wind/alert-distribution, /wind/trend |
| grid — heat | GET /api/grid/{network_id}/heat/map, /heat/alert-distribution, /heat/trend |
| 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.
| Variable | Default | Description |
|---|---|---|
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 |
OIDC client secret |
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_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 and are doubly prefixed by
construction: CELINE_POLICIES_ + policies_dir. Three rows of this table named
variables that do not exist until 2026-08-15 — CELINE_POLICIES_DIR and MQTT__HOST are
both 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 policy fails open and
an unloaded bundle would make every authorisation test pass for the wrong reason.
- 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.