Skip to content

User Data

Daniel Truong edited this page Sep 18, 2026 · 5 revisions

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.

Storage

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.

/me routes

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.

Lassos (saved map areas)

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.

Saved search queries

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.

Preferences

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.

Audit

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

See also

  • Architecture — the userdata container 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 personal flag on a short link

DEMI Wiki

Home

Current system

Azure

Domain

ADRs

History

Related

Clone this wiki locally