Repository navigation
Eagle DEMI
DEMI (Document Extraction & Machine Intelligence) is EPIC's central data store. demi-api is the
master directory for projects, documents, chunks and administrative boundaries, and it serves the
search that eagle-public uses.
DEMI runs on Azure, not OpenShift: Azure App Service for the API, Cosmos DB for NoSQL for the data, Azure AI Search for the indexes, and the NRS object store for files. Cosmos and AI Search are reachable through private endpoints only.
Repository: digitalspace/eagle-demi. Its own wiki owns the detail; this page covers what an EPIC developer needs to know from the Eagle side.
flowchart LR
Track["epictrack-api<br/>master project registry"]
EagleAPI["eagle-api<br/>MongoDB"]
DEMI["demi-api"]
Cosmos[("Cosmos DB for NoSQL")]
Search[("Azure AI Search<br/>demi-search-*")]
Store[("NRS object store")]
Public["eagle-public"]
Track -->|"static snapshot, checked in"| DEMI
EagleAPI -->|"PUT /api/eagle/* on every write"| DEMI
DEMI --> Cosmos
DEMI --> Search
DEMI --> Store
Public -->|"/demi-search/search"| DEMI
Project identity comes from Track: a DEMI project's id is the Track project id as a string. The
Eagle _id is kept on the record as eagleId so Eagle URLs still resolve. An Eagle project with no
Track counterpart is kept, not dropped: it gets id of eagle-<eagleId>, a null trackProjectId
and sourceSystem: 'eagle'.
eagle-api pushes every write to DEMI. DEMI writes into Eagle only for ENGAGE-owned comment
periods, through its sync-out worker and eagle-api's protected /api/commentperiod endpoints.
Nothing writes Eagle's MongoDB directly. See ENGAGE–EAGLE Integration
for the flow and the DEMI wiki page Sync-Architecture and ADR-014-Sync-Out for the worker.
| Environment | Resource group | API | Search service |
|---|---|---|---|
| Test | c4b0a8-test-rg |
demi-api-test.azurewebsites.net |
demi-search-test |
| Prod | rg-demi-prod |
demi-api-prod.azurewebsites.net |
demi-search-prod |
eagle-public reaches demi-api through rproxy at /demi-search/search, which keeps the browser
same-origin. SEARCH_API_PATH in the eagle-api Config document selects it: /demi-search in both
test and prod. See Eagle Search.
That one URL carries the Project, Document and DocumentChunk searches. eagle-public reads
everything else from eagle-api: the project record, participating Indigenous Nations, comment
periods, recent activities, dropdown lists, and document downloads.
/config/public serves DEMI's live mirror of eagle-api's /api/config, reachable through rproxy at
/demi-search/config. eagle-api keeps it current with PUT /eagle/config/public, fired whenever a
read of /api/config sees the served payload change. It answers 503 when the document has not
been seeded rather than a default payload. See Configuration Management
for the fetch order and push mechanism, and the eagle-demi wiki Seeding page for the bootstrap
command.
demi-api mounts its router at both /api and /. Reads are open and filtered by ACL; every
mutation is gated.
| Endpoint | Method | Access |
|---|---|---|
/api-docs, /health
|
GET | Open |
/config |
GET | Open |
/config/public |
GET | Open |
/search |
GET | ACL-filtered |
/search/summary |
GET | Privileged |
/projects, /projects/:id
|
GET | ACL-filtered |
/projects, /projects/:id
|
POST, PUT, DELETE | Write |
/documents, /documents/:id
|
GET | ACL-filtered |
/documents/:id/download |
GET | ACL-filtered, presigned URL |
/documents, /documents/:id
|
POST, PUT, DELETE | Write |
/documents/extract |
POST | Write |
/documents/:id/published |
PUT | Write |
/documents/:id/chunks |
POST | Write |
/boundaries, /boundaries/:id
|
GET | ACL-filtered |
/boundaries, /boundaries/:id
|
POST, PUT, DELETE | Write |
/eagle/projects/:eagleId, /eagle/documents/:eagleId
|
PUT | Write |
/eagle/config/public |
PUT | Write |
/db/stats, /admin/index-progress
|
GET | Privileged |
/admin/sync/wildfires |
POST | Write |
/admin/api-keys, /admin/api-keys/:id
|
GET, POST, DELETE | Write |
ACL-filtered reads run passive authentication: a valid credential populates the caller's identity,
anything else continues as anonymous, and visibility is decided by the record's read[] array
rather than by rejecting the request. A rejected credential is logged, so a forged or expired token
is distinguishable from a logged-out visitor.
Two credential types reach the same identity shape.
Registry API key, sent as X-Api-Key. Used for service-to-service calls, including the
eagle-api push. Keys are issued through /admin/api-keys, carry their own role set and an optional
project scope, and the plaintext is returned once at creation.
Keycloak bearer token, sent as Authorization: Bearer <token>. Used by the DEMI frontend and by
staff users. Signatures are validated against the realm JWKS at SSO_JWKSURI. Setting
KEYCLOAK_ENABLED=false skips signature validation and is for local work only.
Roles:
| Set | Roles | Grants |
|---|---|---|
| Privileged read |
sysadmin, demi-admin, demi-service-read, demi-service-write
|
Bypasses publication gating; reaches privileged reads |
| Write |
sysadmin, staff, demi-admin, demi-service-write
|
Mutations |
| Admin |
sysadmin, staff, demi-admin
|
/admin/* routes |
staff is not a privileged read role. A staff caller sees rows through the read[] ladder below
and reads fields at level 2.
A missing or invalid credential returns 401. A valid credential without the required role returns
403.
A document is public only if the document and its parent project are both public — a double gate.
Chunk visibility is derived from the parent document at query time and project names are resolved
live, so a rename or a stale chunk ACL cascades nothing. When a project's publication state changes,
DEMI re-derives the denormalised document read[].
Row access uses the ladder tokens in read[] plus the privileged read roles above:
| Level | Token | Reader |
|---|---|---|
| 1 | team |
Project team |
| 2 | staff |
All EAO staff |
| 3 | idir |
Any IDIR user |
| 4 | public |
Anyone |
Levels 2 to 4 nest: a level 3 row carries staff and idir. A row with no ladder token opens to
privileged callers and to the roles it names.
An Eagle record keeps Eagle's own read[], capped under its parent by capRead. The readers of the
stored read[] are always a subset of the readers of the parent's cap.
- A
read[]with no ladder token is never widened, so['sysadmin','inspector']stays as it is. -
capReadnever returns an empty list. The fallback is['sysadmin']. - An empty or blank-only Eagle
read[]is stored as['sysadmin']. - A missing Eagle
read[]is stored at level 2 (staff).
The code is eagleBaseAcl and eagleReadUnder in src/helpers/eagle-acl.js, and capRead,
levelOfRead, readForLevel and noLadderToken in src/helpers/access-sql.js.
eagle-api sends the raw Eagle record, not a DEMI-shaped one, because DEMI owns the merge rules.
Seven kinds always mirror this way: projects, documents, comment periods, comments, organizations,
notifications (Eagle's ProjectNotification), and updates (Eagle's RecentActivity).
PUT {DEMI_API_BASE}/api/eagle/projects/{eagleId}
PUT {DEMI_API_BASE}/api/eagle/documents/{eagleId}
PUT {DEMI_API_BASE}/api/eagle/updates/{eagleId}
PUT {DEMI_API_BASE}/api/eagle/commentperiods/{eagleId}
PUT {DEMI_API_BASE}/api/eagle/comments/{eagleId}
PUT {DEMI_API_BASE}/api/eagle/organizations/{eagleId}
PUT {DEMI_API_BASE}/api/eagle/notifications/{eagleId}
Ocp-Apim-Subscription-Key: {DEMI_APIM_KEY}
{ "doc": { ... raw Eagle document ... } }
body.doc._id must match the :eagleId in the path or the call returns 400. Document pushes also
carry a labels object resolving the List ObjectId references (type, milestone,
projectPhase, documentAuthorType) to names.
Each push makes two attempts with a 10-second timeout. It is disabled unless both DEMI_API_BASE
and DEMI_APIM_KEY are set. The write controller waits for the push before it answers; see
Write response.
Five more kinds push only when DEMI_PUSH_OPT_IN_KINDS names them. The setting is a comma-separated
list of route segments, unset by default:
| Segment | Record | Route |
|---|---|---|
users |
user | /eagle/users/:id |
groups |
group | /eagle/groups/:id |
inspections |
inspection | /eagle/inspections/:id |
inspection-elements |
inspection element | /eagle/inspection-elements/:id |
inspection-items |
inspection item | /eagle/inspection-items/:id |
A kind that is not listed does nothing: no database read and no HTTP call. The setting does not
affect the other kinds above. User pushes leave out the legacy password and salt fields. A
deleted group is pushed with isDeleted: true. DEMI serves all five routes. The setting names all
five kinds on test and prod (values-test.yaml, values-prod.yaml).
DEMI merges the project and document push against the existing record's sources.track block if
there is one, preserves every other sources block, writes an audit event, and cascades document
visibility when the project's publication state flips. An update push stores the record in the
updates Cosmos container keyed by the Eagle id, with projectId, isPublished set from the
Eagle record's active field, and a notifiedAt claim used by the eagle-notify push below.
A DEMI-side narrowing or takedown of a project or document survives later Eagle pushes and
re-seeds. DEMI stamps levelHeldAt on the row and keeps the lower of the stored and pushed read[]
until a widen in DEMI reaches level 4 or Eagle's level. pdfTitle and the extraction fields are
kept on every push as well.
eagle-api hard-deletes records. A document or comment-period delete pushes a delete marker
(isDeleted: true). An update delete pushes the record once more with active: false, so DEMI
unpublishes it the same way an edit would. A project delete (soft delete) pushes the project as well. Every
kind not named here produces no delete push. Reconciling those needs an id-set comparison.
Every write that pushes waits for the push before replying: projects, documents, updates,
comment periods, comments, organizations, notifications, groups, inspections and users, including
the ENGAGE (met-api) and DEMI sync-out calls on /commentperiod and /projectNotification.
| Variable | Default | Meaning |
|---|---|---|
DEMI_PUSH_AWAIT_MS |
25000 |
Ms a request waits |
DEMI_PUSH_QUEUE_MAX |
1000 |
Queue cap per pod; extra pushes drop |
| Case | Response |
|---|---|
| Push lands |
200, the saved record plus "mirrored": true
|
| Push fails |
200, the saved record plus "mirrored": false
|
Pushes off, or kind not in DEMI_PUSH_OPT_IN_KINDS
|
Plain 200, no mirrored field |
A push fails when the push client drops it (retries used up, queue full), DEMI rejects it, the
deadline passes, or the parent is not in DEMI. The Mongo write stays; no 502. The row
gets demiPushPending: true, demiPushFailedAt (last failure) and demiPushError (reason).
Clients must not retry on mirrored: false: a retry duplicates the record and file. eagle-admin shows "Saved. It will appear in search within a few minutes."
Failures log at error: [demiPush] not-mirrored <kind> <id>: <reason>. A later good write
clears the fields.
A hard delete (DELETE /document, DELETE /commentperiod, DELETE /project/{id}/group/{groupId})
leaves no row to flag. A failed delete push writes a row in the demi_push_tombstones collection
instead, and the reply stays 200 with mirrored: false. A soft delete (project) flags the live
row and writes no tombstone. Fields and retry: Eagle DEMI Push Sweep.
DEMI refuses a document, comment period, comment, group or inspection whose parent it lacks. The answer is 404 with the body { "error", "code" }:
| Code | Meaning |
|---|---|
PARENT_NOT_FOUND |
The parent id is well formed; DEMI has no such row |
PARENT_REF_INVALID |
The parent reference is missing or malformed |
Both fail the push, as does a 404 without a code. The Eagle DEMI Push Sweep retries every flagged row until it lands.
eagle-api pushes no List rows. DEMI gets them only from eagle-demi
seed-public-reads.js --only lists. DEMI copies List names into document labels and project fields.
A migration that changes a mirrored record needs a re-push. The rule and the DEMI: note format are
in eagle-api migrations/README.md. A test fails yarn test when a migration names a mirrored type
without the note.
demi-api posts the event to eagle-notify when an update it holds transitions to published. Because DEMI owns updates, DEMI is the side that announces the transition, not eagle-api.
POST {NOTIFY_API_BASE}/api/events
x-functions-key: {NOTIFY_API_KEY}
{
"kind": "project-updated",
"serviceName": "project:{projectId}",
"title": "...",
"url": "...",
"projectName": "...",
"excerpt": "...",
"idempotencyKey": "{update.id}"
}
serviceName is eao:updates when the update has no project. idempotencyKey is the update's id
(the Eagle _id). An atomic notifiedAt claim on the DEMI updates record, set with a conditional
Cosmos patch, stops two concurrent pushes of the same newly published update from notifying twice.
Unpublishing or deleting the update pushes the same idempotencyKey with cancelled: true instead
of the content fields. A failed push releases the claim, so the next push retries.
The push needs both NOTIFY_API_BASE and NOTIFY_API_KEY app settings set; either unset means
pushes are dark. NOTIFY_API_KEY comes from Key Vault secret notify-api-key, sourced from the
OpenShift secret demi-app-secrets by scripts/deploy-infra.sh.
eagle-notify turns the event into a paced campaign after a 10-minute recall window. See the
eagle-notify repo docs/STATUS.md for that side.
- Architecture Overview
- Project Data Models — how a project is modelled in Track, Eagle and DEMI
- Eagle DEMI Push Sweep — sweep of failed pushes and delete tombstones
- Eagle DEMI Re-Push and Backfill — re-push, reverse read backfill, parity check
- Eagle Search — how eagle-public reaches search
- Eagle Search Archive — the retired eagle-search repo and Azure estate
- eagle-demi wiki — architecture, Azure environments, extraction pipeline, search and retrieval, identity model, ADRs
Eagle Documentation
Operations
- Deployment Pipeline
- Helm Charts
- Reverse Proxy
- Rollback Procedures
- Eagle Public Release Lines
- Eagle Search Archive
- Azure Migration Strategy
- Azure Hosting
- Front Door Per-App Split
- Front Door Ban Ladder
- Error Logging
- Secrets Management
Architecture
- Architecture Overview
- API Architecture
- Analytics Architecture
- Angular Patterns
- Eagle Public Page Layout
- Eagle Public Adding A Page
- Extended Project Pages
- ENGAGE–EAGLE Integration
- BCGW Project Export
- Eagle Search
- Document Content Search
- Eagle DEMI
- Eagle DEMI Push Sweep
- Eagle DEMI Re-Push and Backfill
- Eagle Notify
- Project Data Models
- Async Jobs API
- Bulk Document Download
Configuration
Help
Repositories
Environments