Repository navigation
User Data
Per-user storage for saved map areas, saved search queries and interface preferences. There is no account entity — a
Keycloak session token is the account, and the owner of every row is derived from the verified
token, never from a client-supplied id. Routes: src/http/routes.js. Controller:
src/controllers/nosql/userdata.js. Repository: src/repositories/userdata.js.
Cosmos NoSQL container userdata in database demi, partition key /userId — one logical
partition per user, so every read and write is single-partition. userId is the caller's Keycloak
preferred_username, lowercased, so casing differences can never split one user's rows across two
owners. Item id is <type>:<key>: a saved map area is lasso:<slug>, a saved search query is
query:<slug>, preferences are the single row prefs. The type field carries the same discriminator so GET /me/data can group in one
query. Polygon vertices (ring) are excluded from the indexing policy — they are stored, never
filtered on. Declared in azure/modules/cosmos-nosql.bicep; the account-scoped Cosmos DB Data
Contributor role assignment covers it like every other container, no extra grant needed.
Container table and the other seven containers: Architecture.
All six require a Keycloak bearer token (authMiddleware) and nothing more — there is no
requireWrite gate, unlike /api/links or /api/projects. Any signed-in staff account can read
and write its own rows; there is no separate read-only tier here because the data a call can reach
is already scoped to the caller by the partition key.
| Method | Path | Body | Response |
|---|---|---|---|
| GET | /me/data |
— |
{ prefs, lassos, queries } — bootstrap read, one query across the caller's partition |
| PUT | /me/lassos |
{ name, ring } |
the saved lasso: { slug, name, ring, updatedAt }
|
| DELETE | /me/lassos/:slug |
— |
{ message }; 404 if the slug is not the caller's |
| PUT | /me/queries |
{ name, params } |
the saved query: { slug, name, params, savedAt }
|
| DELETE | /me/queries/:slug |
— |
{ message }; 404 if the slug is not the caller's |
| PUT | /me/prefs |
{ landing, perPage } |
the stored prefs: { landing, perPage }
|
Client-supplied user ids are never accepted on any of these routes — the owner is always the
token's preferred_username.
PUT /me/lassos upserts by a slug the API derives from name (lowercased, non-alphanumeric runs
collapsed to -), so saving under the same name again replaces the existing area rather than
creating a duplicate. ring must be 3–500 [longitude, latitude] pairs, matching the GeoJSON order
used everywhere else in DEMI. A user may hold at most 50 saved areas; the cap is checked only when
the slug is new, so re-saving an existing area is never refused at the limit.
PUT /me/queries upserts by the same slug rule as lassos, so saving under a name already used
replaces that query. params is a search query string with the leading ? removed, for example
record=documents&keywords=wildlife&scope=inside; it may be at most 2000 characters and may only
carry A-Z a-z 0-9 _ . % & = + , : * -, which is what a search URL built by the frontend uses.
A user may hold at most 50 saved queries, counted separately from lassos and checked only when the
slug is new.
Nothing here validates that the parameters still name a real record type or filter. A saved query
is replayed by the frontend against the search routes, and a stale one simply returns whatever the
current search engine makes of it. Both the Search screen's "Save this query" and the Overview
screen's saved-query list build the same search URL from one helper,
frontend/src/app/search/saved-query.ts.
prefs default to { landing: 'map', perPage: 6 } when no row exists yet. landing must be one of
the screen keys in frontend/src/app/shell/screens.ts; perPage must be one of 6, 12, 24.
The frontend reads and writes localStorage first, so a preference change holds immediately and
works signed out. When signed in, the same change is pushed to the account through PUT /me/prefs,
so it follows the user to another browser or device. See Frontend.
Reads are unaudited. Every write leaves one row through auditEvent:
| Action | Detail logged |
|---|---|
userdata.lasso.save |
slug, vertex count, whether it replaced an existing area — never the ring itself |
userdata.lasso.delete |
slug |
userdata.query.save |
slug, query string length, whether it replaced an existing query — never the query string itself |
userdata.query.delete |
slug |
userdata.prefs.update |
the new landing/perPage values |
-
Architecture — the
userdatacontainer alongside the other seven - Frontend — Map Explorer, the Search screen and the Overview screen, the clients of these routes
-
Short-Links — the other per-user distinction in DEMI, the
personalflag on a short link
- Environment Reality & Gotchas
- Architecture
- Seeding — filling Cosmos from Track and Eagle
- Sources and Status — every source, one state
- Search & Retrieval — overview
- Search Index Reference
- Search Query Construction
- Search Measurements
- Extraction Pipeline
- Extraction Quality — coverage, word-joining, tiling
- Identity Model — which id means what
- Attribute-Level Access — field-visibility levels, catalog
- Sync Architecture — how records get in
- Eagle Mirror Reference — mirrored kinds, refusals, drift classes
- ENGAGE Rows Reference — ENGAGE-owned comment periods, reconcile
- Public-Read Backfill — filling and repairing the mirror containers
-
Short Links —
/s/{code}redirect and/api/links -
Recent Uploads —
/api/documents/recent-uploads, eagle-public homepage panel - Reports — comment export and BCGW CSV reads
-
User Data — saved map areas, preferences,
/meroutes -
Bulk Download —
/api/bulk-downloads, zip jobs - Bulk Download: Operations — enable, poison-queue runbook
- Document Intake — upload link, malware scan, release
- Connecting an Application to DEMI
- Azure Environments
- Document Backup — archive copy of originals, run and check
- Document Backup: Revert and Drill — put documents back from the archive
- PDF Titles — public name written into stored PDFs, run, restore
- Secret Sync — Key Vault, secret-sync Function app, required names
-
Operating DEMI in Azure — deploy, verify
- Operating the DEMI Data Plane — devbox access, probes, audit and cost reads
- Edge ban detector — hourly timer, modes, kill switch, soak and promote
-
CI Workflows — every GitHub Actions workflow, trigger, identity, roles
- CI Deploy Workflows — eagle-demi staging and production deploys
- CI Admin Frontend Workflows — eagle-demi-admin checks and deploys
- Release Process — cutting, tagging, shipping and rolling back a version
- Runbook: Search Outage — index/app schema drift, diagnose, fix, roll back
- BC Gov Landing Zone
- Business Rules
- EPIC.search Business Rules — where DEMI differs from EPIC.search
- 001: Typesense for DFL Search — superseded
- 002: Async Processing (MongoDB Queue) — superseded
- 003: Rule-Based Classification
- 004: Read ACL Authorization Model
- 005: Cosmos Full-Text Search Ruled Out
- 006: AI Summarization over BM25
- 007: Service-to-Service Credentials
- 008: Audit and Usage Analytics Store
- 009: Track Feed — proposed
- 010: Attribute-Level Access Control
- 011: Public Read Surface
- 012: Public Config Surface
- 013: Stored Project Summaries — proposed
- 014: Sync-out to Eagle
- 015: Malware scanning on intake