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