Skip to content

ADR 009 Track Feed

Daniel Truong edited this page Oct 16, 2026 · 5 revisions

ADR-009: Track feed into DEMI

Status

Accepted 2026-10-16. Option A (DEMI pulls) is what shipped. The nightly syncTrackTeams timer reads GET /api/v1/projects and mirrors it into DEMI, and the syncTrackProjects timer polls the same list every five minutes (eagle-demi PRs #627, #628), so a Track edit reaches DEMI within five minutes. Option B (Track pushes) is not built; Track does not call PUT /api/projects/<trackProjectId>. The timers are in Sync-Architecture § The Track side. The Context below is the state when the ADR was proposed (2026-08-24).

Context

Track (epictrack-api) owns the master project registry, and the Track project id is the master project id across EPIC. DEMI stores it that way: a DEMI project's id is its trackProjectId verbatim, and createProject refuses a project without one.

DEMI has no live connection to Track. Its entire Track dataset is a checked-in file, src/data/track_projects_enriched.json, 382 records, snapshot 2026-07-29, read off disk by src/seed/sources.js. Every project created, renamed, re-phased or withdrawn in Track since that snapshot is invisible to DEMI, and DEMI serves eagle-public's test search. The rule is fail-closed, so the symptom is absence rather than error: a project created after the snapshot never appears, and nothing logs that it is missing.

Track runs in the Gold namespaces c8b80a-{dev,test,prod}; the API is https://epictrack-api-c8b80a-<env>.apps.gold.devops.gov.bc.ca, and /ops/healthz is the unauthenticated liveness path (every /api/v1 route answers 500, not 401, to a missing or bad token). c72cba-* holds a dormant clone whose DeploymentConfigs sit at 0 replicas; its hostnames resolve to the wildcard router and answer 503.

Decision

Ask Track for one of two directions. Option B is the recommendation: it costs Track one HTTP call, and it needs nothing built in DEMI and no credential from Track.

Option A — DEMI pulls from Track

DEMI calls GET /api/v1/projects on epictrack-api on a schedule (nightly is enough; the data changes at human speed) and reconciles. Track must supply:

  1. The epictrack-api base URL in each environment.
  2. A service credential: a Keycloak client in the eao-epic realm with the view role and audience epictrack-web, issued to DEMI as a service account rather than a person's token. This is the critical path; the rest is a morning's work.
  3. Confirmation of the response contract. DEMI assumes the endpoint is unpaginated and serves epic_guid per project.
  4. Whether deletes are observable. Without a tombstone or an is-deleted flag, reconciliation can only infer a delete from absence.

Option B — Track pushes to DEMI

Track calls PUT /api/projects/<trackProjectId> on DEMI on create, on update, and on whatever Track considers a withdrawal or delete. The path id is the Track project id because that is DEMI's own project id. The route is authenticated and write-gated (src/http/routes.js, authMiddleware + requireWrite), and DEMI mints Track a scoped, revocable, expiring key — see Connecting-an-Application-to-DEMI.

Out of scope either way

No write access to Track: nothing in DEMI writes upstream, by design. No database access: an HTTP contract in either direction is enough. No real-time guarantee: minutes or hours of lag is fine, months is not.

Consequences

  • Master ids follow Track within five minutes. Before 2026-10-16 the id space was a snapshot, so a stale Track file was a stale primary key space, not a stale side table.
  • The feed makes absence observable. A DEMI project Track no longer lists is counted as orphaned and retained, never deleted; an Eagle project with no Track row is retained and flagged.
  • src/data/track_projects_enriched.json is the seed's offline fallback only.
  • Option A adds two scheduled jobs and a Track client credential to rotate. Option B would have added neither, and moved the failure mode to Track missing a call.

See also

DEMI Wiki

Home

Current system

Azure

Domain

ADRs

History

Related

Clone this wiki locally