Skip to content

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 (WARNING or ALERT) 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-flow pipeline 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 (credentials postgres: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 test cannot 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.