Identity Model - digitalspace/eagle-demi GitHub Wiki
Identity Model
This page is the authority on which id means what in DEMI. If another page disagrees, this one is right.
The rule
Track project ids are the master id for projects in DEMI.
Track (epictrack-api) is a superset of Eagle. Projects are created in Track, and business does a
manual sync into Eagle. Eagle's MongoDB is being retired.
Projects
| Field | Value | Why |
|---|---|---|
id |
Track project id, as a String | It is also the Cosmos partition key |
trackProjectId |
the same value, as a Number | merge/project.js writes it this way |
eagleId |
the Eagle _id, an attribute |
Keeps existing Eagle URLs resolvable |
sourceSystem |
track | eagle | demi |
Provenance |
In the Cosmos row id is a String and trackProjectId is a Number. The search response emits
trackProjectId as a String on both branches: the Cosmos branch coerces the stored Number, and the
index branch has no trackProjectId field at all, so it reuses the Edm.String key. Both legs are
in src/controllers/search.js, in the Project dataset mapper.
id is the partition key, so it cannot be changed in place. updateProject drops id and
trackProjectId from the request body rather than applying them; reassigning one is a
delete-and-reinsert, and every document under that project moves partition with it.
Eagle projects with no Track counterpart
Retain them, and flag them. Never drop them.
This should not happen β Track is meant to be a superset. It does happen in test, because test
projects are created in Eagle exclusively. Those rows carry the literal id
eagle-<24-hex-ObjectId>, written by mergeEagleOnlyProject in src/merge/project.js, and
sourceSystem: 'eagle'.
GET /api/projects and project search list them beside the Track-sourced rows: what a caller sees
is decided by the row's read[] ACL, not by which system wrote it. GET /api/db/stats reports the
count as unlinkedProjects β the rows that carry no Track counterpart.
Documents
| Field | Value |
|---|---|
id |
the Eagle _id for mirrored documents; a UUID for DEMI-created ones |
projectId |
the DEMI project id β the Track id, not the Eagle project ObjectId |
projectId is the Cosmos partition key (src/repositories/documents.js). Reusing the Eagle
_id as the document id is what makes eagle-api's download URL
/api/public/document/{_id}/download/document resolve.
Chunks
chunks partitions on /documentId, not /projectId (src/repositories/chunks.js,
azure/modules/cosmos-nosql.bicep). projectId rides along as a scope field only
(SCOPE_FIELD), because visibilityFor(access, field) uses its argument for both the partition key
and the project-scope predicate, and passing documentId there would compare project ids against
document ids and silently match nothing.
The practical consequence: re-identifying a project moves no chunks. Document ids are Eagle ObjectIds and do not change.
Chunk ids are deterministic β ${documentId}::p${pageNumber}::c${chunkIndex} β so re-extracting a
document upserts its chunks in place rather than duplicating them.
What crosses the wire
eagle-public receives the Eagle ObjectId as _id, and that is deliberate. It routes
/p/<id>/project-details and then re-fetches the project from eagle-api by that id, so a Track id
there is a link that 404s. The Project dataset mapper in src/controllers/search.js sets _id
to legacyEagleId, falling back to the DEMI id for a Track-only project.
The general rule: one field per id-space, never one derived from the other. A search result row
carries id (DEMI/Track), trackProjectId, legacyEagleId and _id (Eagle), and each has exactly
one meaning. trackProjectId is null β never the DEMI id β on a row whose id starts with eagle-.
Machine callers
| Principal | Direction | Credential | Grants |
|---|---|---|---|
apim:eagle-api |
eagle-api β DEMI | APIM subscription eagle-api |
demi-service-write, no project scope; the Eagle mirror |
apim:engage |
ENGAGE β DEMI | APIM subscription engage |
demi-service-write, no project scope; the ENGAGE ingest only |
demi-sync-out |
DEMI β eagle-api | Keycloak client in realm eao-epic, client credentials |
Realm role staff in eagle-api; secret in Key Vault as eagle-sync-out-client-secret |
Each DEMI route family admits only its own principal. Registry rows and the ingest contract: Connecting-an-Application-to-DEMI. Why DEMI writes to eagle-api: ADR-014-Sync-Out.
The gap
trackProjectId is empty in Eagle. The field exists on Eagle's Project model as
unique, sparse, index (eagle-api/api/helpers/models/project.js:105), and eagle-api already ships
middleware that 301-redirects an ObjectId path segment to it
(eagle-api/api/middleware/projectRedirect.js:16-24). Measured 2026-08-23: it is populated on
0 of 200 sampled projects, in both test and prod.
So the EagleβTrack mapping exists only inside DEMI (via track.epic_guid) and inside Track itself.
By decision, DEMI keeps that mapping and writes nothing into Eagle Mongo. Sync-out sends comment
periods to eagle-api's HTTP API and carries no trackProjectId (ADR-014-Sync-Out). That leaves
eagle-api's redirect middleware dormant. That is the intended state, not an oversight.
The other gap
DEMI's Track data is a static file. src/data/track_projects_enriched.json β 382 records,
snapshot 2026-07-29, read off disk by src/seed/sources.js. There is no live Track feed, so the
master id space comes from a snapshot rather than from Track. ADR-009-Track-Feed states what
closing that would take, and from whom.
See also
- Sync-Architecture β how records get into DEMI, and what happens on delete
- Architecture β the containers, the merge, the ACL model