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 (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 (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 — 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.

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 debug export CELINE_ENV=dev for you.
  • CELINE_ENV=staging task run is the prod-like local mode; set real values first.
  • docker compose up passes only what .env holds — add CELINE_ENV=dev there for a local container.
  • The check reads the real process environment, not .env via 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 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.