Development
Prerequisites
- Python >= 3.12 and
uv task(go-task)- PostgreSQL at
localhost:15432— to run the service. The test suite needs no database; see Testing.
Environment Variables
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
postgresql+asyncpg://...host.docker.internal:15432/celine_webapp |
PostgreSQL async URL |
DATABASE_ECHO |
false |
Log SQL statements |
DIGITAL_TWIN_API_URL |
http://host.docker.internal:8002 |
Digital Twin service URL |
NUDGING_API_URL |
http://host.docker.internal:8016 |
nudging-tool service URL |
FLEXIBILITY_API_URL |
http://host.docker.internal:8017 |
flexibility-api service URL |
REC_REGISTRY_URL |
http://host.docker.internal:8004 |
rec-registry service URL |
ONBOARDING_API_URL |
http://host.docker.internal:8040 |
onboarding service URL — reached with the member's own token, for data sharing and for the terms gate's acceptances given at application |
SMART_METER_API_URL |
— | Optional smart meter API URL |
NUDGING_INGEST_SCOPE |
nudging.ingest |
OAuth2 scope for nudging ingest calls |
POLICY_VERSION |
2024-01-01 |
Current terms version string |
LEGAL_BASE_URL |
— | The legal host. A community link the rec-registry leaves empty resolves to <LEGAL_BASE_URL>/<community>/<slot>/, and the terms gate asks for the community's own documents and versions (docs/features.md); unset, links come from the registry only and the gate uses POLICY_VERSION |
JWT_HEADER_NAME |
x-auth-request-access-token |
Header carrying the bearer token |
CORS_ORIGINS |
["http://localhost:5173"] |
Allowed CORS origins |
CELINE_OIDC_* |
(from celine-sdk defaults) | OIDC settings — issuer, JWKS URI, audience |
CELINE_ENV |
— (hardened) | dev accepts the local defaults; see Deployment posture. ENVIRONMENT is read when it is empty |
Deployment posture
The service follows celine.sdk.posture: only CELINE_ENV=dev relaxes. Unset, empty,
staging, prod or a typo is hardened, and create_app() refuses to start — before the
database is opened — while:
DATABASE_URLcarries a local-stack password;CELINE_OIDC_CLIENT_SECRETis empty or equal toCELINE_OIDC_CLIENT_ID(only checked when a client id is set; it is used for nudging reminders);CELINE_OIDC_BASE_URL/CELINE_OIDC_JWKS_URIare left on the SDK's local Keycloak default.
In dev the same list is one startup warning. Outside dev /api/docs, /api/redoc and
/api/openapi.json are not mounted (404) unless CELINE_PUBLIC_DOCS=true.
task run exports CELINE_ENV=dev;
CELINE_ENV=staging task run runs the same entry point hardened.
CELINE_OIDC_AUDIENCE is not required: with it unset the member token's audience is not
checked (issuer, signature and expiry are). Requiring it needs the realm to put this service's
audience into the member's token first.
celine.sdk.posture ships in celine-sdk 2.0.0, the floor in pyproject.toml.
Data sharing
Off by default. The section is hidden in the app and the routes answer 404 until the dataspace is deployed, because a sharing screen that cannot record a decision is worse than no screen.
| Variable | Default | Description |
|---|---|---|
DATA_SHARING_ENABLED |
false |
Master switch for the whole surface |
DATA_SHARING_REVIEW_AFTER_DAYS |
180 |
How long a decision stands before the member is asked to review it. 0 or less asks once and never again |
The routes stay off unless DATA_SHARING_ENABLED is true and ONBOARDING_API_URL
is set.
The dataspace is not reached from here. Onboarding resolves the member's credential,
speaks to the connector and serves their history; this service forwards the member's own
token to /api/me/data-sharing and passes the answer back. There is no identity-registry
URL, no connector URL and no service account in this repository — a credential that never
leaves the service that resolved it cannot leak from the one that did not need it.
Backend Setup
uv sync
uv run alembic upgrade head
task run
# Listens on http://localhost:8014
Taskfile Commands
| Command | Description |
|---|---|
task setup |
uv sync |
task test |
Run the test suite |
task run |
Start dev server on port 8014 |
task debug |
Start with debugger |
task migrate |
Apply all pending migrations |
task migrate:create |
Create a new Alembic migration |
task release |
Run semantic-release |
Frontend
The participant frontend is in celine-frontend apps/webapp. See that repository for frontend setup instructions.
Calling the API locally
In local dev the BFF reads X-Auth-Request-Access-Token, the header oauth2-proxy injects
in deployment. Authorization: Bearer <token> works as a fallback.
curl http://localhost:8014/api/me \
-H "X-Auth-Request-Access-Token: <token>"
The token must be a real one from Keycloak. It is verified in full here — the signing
key is resolved from the realm's JWKS by the token's kid, the signature is checked, and
exp and iss are enforced. There is no unverified path and no development bypass, so a
hand-assembled or alg: none token will be rejected.
A 401 accompanied by this log line is the usual result, and it means the token, not the network:
Failed to fetch signing key from …/protocol/openid-connect/certs:
Unable to find a signing key that matches: "None"
CLI
celine-webapp-export-feedback # Export user feedback data
Database Migrations
uv run alembic upgrade head
uv run alembic revision --autogenerate -m "add preference column"
Testing
task test # the whole suite
task test -- -x -q # arguments pass through to pytest
The suite needs nothing running — no PostgreSQL, no Keycloak, no upstream service. It runs on a clean checkout and in CI, and a test that needs a live service does not belong in it.
That is achieved at two seams, and both matter when you write a test:
- Identity is real; only the JWKS fetch is faked. Tests sign genuine RS256 tokens with a throwaway key, and the service verifies them exactly as it does in production — signature, expiry, issuer. Nothing overrides the authentication dependency, so a test cannot accidentally prove that a token this service should reject is accepted.
- The database is SQLite, one file per test. Production is PostgreSQL, so a schema change is not verified by a green run alone — exercise the migration separately.
What the suite does and does not tell you
This service is a backend-for-frontend: most of what it does is fan out to the Digital
Twin, rec-registry, flexibility-api and nudging-tool through celine-sdk, then compose
the results. Those four are replaced by fakes in tests/fakes.py, which reproduce the
shape the SDK returned when they were written.
Onboarding, the fifth, is stubbed differently: celine.sdk.onboarding builds its own
httpx.AsyncClient, so the mock_upstream_http fixture replaces the socket and lets the
wrapper, the generated client and the schema conversion all run for real.
So a green suite says the composition logic is correct against that shape.
tests/test_sdk_contract.py checks the fakes against the installed celine-sdk models,
which catches drift in the package — but not that a deployed upstream serves what its
own SDK describes. A celine-sdk version bump can still change this service's behaviour
with no file here changing. Treat an SDK bump as a change to this service rather than as
dependency maintenance.
Layout
| File | Covers |
|---|---|
tests/test_auth_boundary.py |
what is accepted as an identity, and what is rejected |
tests/test_overview_fanout.py |
/api/overview — aggregation, trend building, degradation |
tests/test_gamification_fanout.py |
/api/gamification — season scoring and its fallback |
tests/test_nudging_fanout.py |
/api/settings and /api/notifications |
tests/test_sdk_contract.py |
that the fakes still match the installed celine-sdk models |
tests/test_data_sharing.py |
the data-sharing surface, onboarding stubbed at the socket |
tests/test_onboarding_dep.py |
that the onboarding client is built per request from the caller's token |
tests/test_api.py, tests/test_forecast.py |
pure mapping and window functions |
tests/test_feedback.py |
feedback filing and the manager check: realm role platform-admin vs the REC's own groups |
tests/integration/test_real_tokens.py |
the same two levels with tokens a local Keycloak issued — opt-in, skipped by default |
tests/fakes.py |
the four upstream fakes |
If you add a field to a fake, assert it in test_sdk_contract.py in the same change.
The fakes are written by hand from reading route code, so an invented attribute will
reproduce whatever you expect of it — that is not a hypothetical, it produced a fully
reproducible and entirely wrong bug report the day the fakes were written.
tests/integration/ is the one exception to "needs nothing running", and it is opt-in so
the default run and CI stay hermetic. With the local stack's Keycloak up, it mints real
oauth2_proxy tokens for the dev users and verifies them against the realm's real JWKS:
CELINE_WEBAPP_REAL_TOKENS=1 uv run pytest tests/integration
It refuses any issuer that is not *.localhost or loopback. The module docstring lists the
users, aliases and environment variables it reads; the retired-realm-group case runs only
when a token carrying one is passed in CELINE_WEBAPP_LEGACY_GROUP_TOKEN.
Tests marked xfail with strict=True are known defects, pinned deliberately, each
with its reason in the marker. Fixing one turns the run red with XPASS(strict) — the
signal to remove the marker in the same change. There are none at present.
Project Layout
src/celine/webapp/
main.py # FastAPI app factory
settings.py # Pydantic settings
routes.py # Router registration
cli.py # CLI (celine-webapp-export-feedback)
api/
user.py # /api/me, /api/terms/accept
overview.py # /api/overview
weather.py # /api/weather
forecast.py # /api/forecast
community.py # /api/community
suggestions.py # /api/suggestions, /api/commitments
gamification.py # /api/gamification
co2_settings.py # /api/settings/co2
settings_routes.py # /api/settings
notifications.py # /api/notifications, webpush
feedback.py # /api/feedback
data_sharing.py # /api/data-sharing
meta.py # /health
deps.py # FastAPI dependencies — every outbound client is resolved here
schemas.py # Pydantic schemas
services/
data_sharing.py # Dataspace calls (identity registry, connector, provenance)
db/
models.py # SQLAlchemy ORM models
session.py # Async session management
user_settings.py # User settings helpers
alembic/ # Database migrations
tests/ # See Testing above