Skip to content

Server functions and routes

An inventory of the app’s server functions (createServerFn) and server routes (routes/api/**). For the guard model, see Auth and permissions. For using the public API, see Public API.

  • Method. GET for reads. POST for writes, and for reads whose input is large, since a GET carries its input in the URL. getImportStatus and getMapRecipe are POST reads because repeated identical GET calls are unreliable in the framework version in use.
  • Guard. Tenant functions declare orgMiddleware (org in the tables). It runs authMiddleware (auth), which validates the JWT with getClaims() and redirects to /login when there is none. Neither middleware checks membership. The handler calls a helper from server/lib/authz.ts before touching data: assertOrgMember, assertOrgRole, assertMapAccess, assertCanEditMap or assertSystemAdmin. A function with no helper in the table relies on RLS alone.
  • Client. RLS means the caller’s cookie-scoped client (getSupabaseServerClient). Service means getSupabaseServiceRoleClient, which bypasses RLS. For a service-role call the authz helper is the only boundary.
  • Change capture. Graph writes take an optional session_id (from startSession) and record an app.atomic_change row through server/lib/change-capture.ts. Without a session_id, the change goes into the caller’s active session. Field writes record a change only when they rewrite or remove values. Saved-view writes record nothing. See Editing internals.
  • Errors. Failures throw Error. Some carry a CODE: message prefix; see Error codes.
  • Stubs. Functions marked stub have their method, validator and middleware but throw NOT_IMPLEMENTED (functions/not-implemented.ts).

Files are grouped by locality, as named in each file’s header comment:

Locality File
01 Manual editing manual-editing.ts
02 Data upload data-upload.ts
03 Visualisation visualisation.ts
04 Storage and revision revision.ts
05 Workspace and permissions workspace.ts
Fields (map catalogue) fields.ts
Tag groups tag-groups.ts
Map recipe map-recipe.ts
Profiles and settings profile.ts
Setup setup.ts

Where the code is: apps/app/src/server/functions/, apps/app/src/server/middleware/, apps/app/src/server/lib/authz.ts.

manual-editing.ts. Every graph write here accepts session_id.

Function Method Guard Client Purpose
getMap GET org + assertMapAccess RLS One map’s row
getMaps GET org + assertOrgMember RLS An org’s maps
getMapSummaries GET org + assertOrgMember RLS Counts and last edit per map
createMap POST org + assertOrgRole(org_admin, member) RLS Create a map; starter_tags adds the Type group
renameMap POST org + assertMapAccess, assertOrgRole(org_admin, member) RLS Rename a map
deleteMap POST org + assertMapAccess, assertOrgRole(org_admin, member) RLS, Service Delete a map and its data
createNode POST org + assertMapAccess RLS Add a node
updateNode POST org + assertMapAccess RLS Move, relabel or set properties
deleteNode POST org + assertMapAccess RLS Delete a node and its connections
setNodeField POST org + assertMapAccess RLS Set a node’s measure value
deleteNodeField POST org + assertMapAccess RLS Clear a node’s measure value
getNodePropertyKeys GET org + assertMapAccess RLS Property keys used in the map
promoteNodeProperty POST org + assertMapAccess RLS Turn a text property into a measure
addNodeTag POST org + assertMapAccess RLS Tag a node
removeNodeTag POST org + assertMapAccess RLS Untag a node
setTagOnNodes POST org + assertMapAccess RLS Add a tag to many nodes, or remove it, as one change
createConnection POST org + assertMapAccess RLS Add a connection
updateConnection POST org + assertMapAccess RLS Change type, value, dates or direction
deleteConnection POST org + assertMapAccess RLS Delete a connection
getUnplacedNodes GET org + assertMapAccess RLS Page of unplaced nodes
placeNodes POST org + assertMapAccess RLS Give unplaced nodes positions
moveNodes POST org + assertMapAccess RLS Reposition placed nodes in one call
unplaceNodes POST org + assertMapAccess RLS Clear nodes’ positions

Notes:

  • deleteMap calls network.delete_map first, because network rows have no FK to app.map. It then removes the map’s import files from the map-imports bucket with the service-role client, and deletes the app.map row last.
  • getMapSummaries’ last_edited_at is the newest captured change or completed (ingested) import, or null. Opening a map starts a session but captures nothing, so it doesn’t count. Renames and visibility changes show in app.map.updated_at instead.
  • createNode and createConnection accept a client-generated id (node_id, id) for optimistic inserts.
  • setNodeField upserts network.node_measure on (node_id, measure_id).
  • createConnection and updateConnection accept dates as partial ISO strings (2024, 2024-06, 2024-06-15). In updateConnection, null clears a date and an absent key leaves it alone. swap_endpoints reverses the connection.
  • placeNodes refuses a request that would take the map past 200 placed nodes.
  • createMap with starter_tags: true (the home view’s blank map only) adds a Type group with Person and Organisation and makes it the map’s default_group_by. A failure there is logged; the map is still returned.
  • setTagOnNodes records one nodes.tags_changed change. Adding a grouped tag first deletes the nodes’ other memberships in that group, and puts them back if the insert fails. moveNodes takes at most 200 moves and only moves nodes that are already placed.
  • placeNodes, moveNodes and unplaceNodes write through network.set_node_positions and fail if any named node isn’t in the map.
  • getUnplacedNodes takes limit and offset and orders by created_at, then id.

data-upload.ts. The browser uploads files straight to Storage; see Import pipeline.

Function Method Guard Client Purpose
getSignedUploadUrl POST org + assertMapAccess Service Storage upload token
createImportSession POST org + assertMapAccess RLS, Service Create a session and scan the files
getImportPreview GET org RLS A session’s scan and mapping
updateImportMapping POST org RLS Save the confirmed mapping
ingestImportSession POST org + assertMapAccess RLS, Service Start the ingest task
getImportStatus POST org RLS Poll a session’s status
removeImportRow POST org — stub

Notes:

  • getSignedUploadUrl derives the org from map_id. It never accepts org_id. The path is {orgId}/{mapId}/{uuid}-{filename}.
  • createImportSession reads the files with the service-role client, since the caller’s session can’t read objects uploaded with a signed token.
  • ingestImportSession returns { started: true } and runs the ingest as a detached task with the service-role client. Poll getImportStatus. A session stuck in ingesting is failed by the maintenance sweep.
  • ingestImportSession needs status previewing or mapping. A GraphML import, or a CSV import with a connections file, needs mapping.

visualisation.ts.

Function Method Guard Client Purpose
getGraph GET org + assertMapAccess RLS Every node, connection and value in a map
getSavedViews GET org + assertMapAccess RLS A map’s saved views, steps first (server/lib/saved-views.ts, shared with the public API)
createSavedView POST org + assertMapAccess RLS Save a new view
updateSavedView POST org + assertMapAccess RLS Overwrite a view’s viewport and config
renameSavedView POST org + assertMapAccess RLS Rename a view
setSavedViewStep POST org + assertMapAccess RLS Set a view’s step in the reveal, or clear it
reorderSavedViews POST org + assertMapAccess RLS Number the map’s views as steps, in the order given
deleteSavedView POST org + assertMapAccess RLS Delete a view
exportVisualisation POST org — stub

Notes:

  • getGraph takes only { map_id } and returns every node, placed or not. Callers filter to placed nodes themselves. A public map’s graph still needs membership.

revision.ts.

Function Method Guard Client Purpose
startSession POST org + assertMapAccess RLS Get or create the caller’s active edit session
captureChange POST org + assertMapAccess RLS Record one change against a session
undoChange POST org — stub
redoChange POST org — stub
collapseSession POST org — stub
discardSession POST org — stub
getRevisionHistory GET org — stub
loadRevision POST org — stub
compareRevision GET org — stub

Notes:

  • startSession returns the caller’s oldest active session for the map, or starts one (ensureActiveSession).
  • captureChange checks access against the session’s map, and refuses an event type that isn’t in DATA_EVENT_TYPES or LAYOUT_EVENT_TYPES.

workspace.ts. Functions acting on the org as a whole use authMiddleware only.

Function Method Guard Client Purpose
createOrg POST auth + assertSystemAdmin Service Create an org; caller becomes org_admin
deleteOrg POST auth + assertOrgRole(org_admin) RLS Soft-delete an org
renameOrg POST auth + assertSystemAdmin RLS Rename an org
getOrgs GET auth RLS Visible orgs with my_role and is_member
getOrg GET auth RLS One org by id
getOrgBySlug GET auth RLS One org by slug, with my_role
createInvitation POST org + assertOrgRole(org_admin) RLS, Service Invite an email and send the link
resendInvitation POST org + assertOrgRole(org_admin) RLS, Service Reset expiry and resend
revokeInvitation POST org + assertOrgRole(org_admin) RLS Delete a pending invitation
getInvitations GET org RLS An org’s pending invitations
listMyInvitations GET auth Service Invitations to the caller’s email
acceptInvitation POST auth Service Join an org from an invitation
getMembers GET org + assertOrgRole(org_admin, member) Service Members with email and display name
removeMember POST org + assertOrgRole(org_admin) RLS Remove a member
changeMemberRole POST org + assertOrgRole(org_admin) RLS Change a member’s role
setMapVisibility POST org + assertMapAccess, assertOrgRole(org_admin) RLS Set private or public
shareMap POST org — stub

Notes:

  • assertOrgRole also passes for a system admin.
  • my_role is system_admin whenever the caller is one, otherwise their membership role, otherwise null.
  • A system admin sees every org through getOrgs. is_member is true only where the caller has a membership row, and the home page’s organisation menu lists only those.
  • deleteOrg sets deleted_at. The maintenance sweep purges the org’s data after the retention window.
  • createInvitation and resendInvitation build the email link with the service-role Admin API (invite, then magiclink, then a plain /login link). Sending is best-effort. An invitation expires after 7 days.
  • createInvitation refuses an email that already belongs to a member or has a pending invitation.
  • acceptInvitation checks the caller’s email is confirmed and matches the invitation. It is idempotent: an existing membership returns { ok: true }.
  • removeMember and changeMemberRole refuse to leave an org with no org_admin.

fields.ts. The map’s catalogue of measures, tags and connection types. A write that rewrites or removes values records a change: field.deleted, field.converted_to_tag or connection_type.value_type_changed. Moving a tag to another group records tag.regrouped. Other field writes record nothing. A tag’s Field also carries tagGroupId, icon and colour, and createField/updateField accept icon and colour for a tag.

Function Method Guard Client Purpose
getFields GET org + assertMapAccess RLS The catalogue, optionally by subject_type
createField POST org + assertMapAccess RLS Create a measure, tag or connection type
updateField POST org + assertMapAccess RLS Change a definition
deleteField POST org + assertMapAccess RLS Delete a definition

Notes:

  • createField and updateField accept value_label and value_unit, for numeric connection types only.
  • A tag’s category names its tag group. The group is created if it doesn’t exist.
  • updateField can turn a measure into a tag, which discards its values. A tag can’t become a measure.
  • deleteField deletes node values and tag memberships with the definition. Connections stay, with metric_id and value cleared.
  • createFieldForMap and updateFieldForMap hold the logic and are shared with the public API. updateFieldForMap takes a callback to record a change that rewrote values, so each caller records it its own way.

tag-groups.ts. A map’s tag groups as objects with ids. None changes a node’s data, so each records a catalogue event (tag_group.created, tag_group.renamed, tag_group.deleted, tag.regrouped) that doesn’t make the recipe stale.

Function Method Guard Client Purpose
getTagGroups GET org + assertMapAccess RLS The map’s groups, oldest first
createTagGroup POST org + assertMapAccess RLS Create a group
renameTagGroup POST org + assertMapAccess RLS Rename a group
deleteTagGroup POST org + assertMapAccess RLS Delete a group, keeping its tags ungrouped
moveTagToGroup POST org + assertMapAccess RLS Move a tag into a group, or out (null)
setDefaultGroupBy POST org + assertCanEditMap RLS Set app.map.default_group_by
copyDefaultGroupBy POST org + assertMapAccess, assertCanEditMap RLS Give a duplicate its original’s default grouping, matched by name

Notes:

  • Group names are unique within a map, ignoring case (TAG_GROUP_NAME_TAKEN), because imports and the API still find a group by name (ensureTagGroup).
  • A move that would give a node two of a group’s tags is refused with TAG_GROUP_CLASH, naming up to five nodes. updateField’s regroup (by category) does the same check. Memberships move before the tag row, so a refused or failed move changes nothing.
  • deleteTagGroup relies on the composite FKs’ set null for tag and node_tag, then clears default_group_by in a separate app write.

map-recipe.ts. A map’s recipe and whether it’s out of date. The logic is in server/lib/map-recipe.ts, shared with the public API. See Editing internals.

Function Method Guard Client Purpose
getMapRecipe POST org + assertMapAccess RLS, Service The recipe, stale, and whether the caller can_edit
saveMapRecipe POST org + assertCanEditMap RLS, Service Save from the About panel; always confirms
confirmMapRecipe POST org + assertCanEditMap RLS, Service Confirm without changing the text (Still accurate)
copyMapRecipe POST org + assertMapAccess (source), assertCanEditMap (copy) RLS Copy a recipe to a duplicated map, unconfirmed

Notes:

  • The service-role client only looks up people’s names (loadUserIdentities), after the access check.
  • saveMapRecipe takes base_updated_at, the updated_at the editor started from (null for a new recipe), and overwrite. It returns { status, recipe }, where status is saved, unchanged or conflict.
  • copyMapRecipe saves without confirming, credited to the caller. duplicateMap calls it last and only logs a failure, since the copy is complete without it.

profile.ts and setup.ts.

Function Method Guard Client Purpose
getProfile GET auth RLS Caller’s display name and settings
updateProfile POST auth RLS Set display name
updateSettings POST auth RLS Merge keys into settings
getAppStatus GET none Service Whether a system admin exists
signUpAdmin POST none RLS Create the first auth account
initializeApp POST signed-in check, no admin yet Service Create first org, admin and membership

Notes:

  • updateSettings shallow-merges the given keys into the stored settings.
  • getAppStatus returns noSchema: true when the app schema isn’t migrated.
  • initializeApp refuses to run once any system admin exists.
Function File Method Purpose
getUser lib/auth.ts GET Caller from JWT claims, or null; used by the root beforeLoad
signInWithPasswordFn routes/login.tsx POST Password sign-in
sendMagicLinkFn routes/login.tsx POST Send a magic link; always reports success
logoutFn routes/logout.tsx GET Sign out and redirect to /
confirmEmailFn routes/auth/confirm.tsx GET verifyOtp, then redirect to /dashboard
exchangeCodeFn routes/auth/callback.tsx GET Exchange an OAuth code, then redirect

None of these use middleware. All use the RLS client. On failure, confirmEmailFn and exchangeCodeFn redirect to /login.

Server routes return a raw Response and bypass the root route’s setup redirect. Function middleware can’t attach to them, so each handler checks auth inline.

Route Method Guard Client Purpose
/api/health GET none — Health check; always { ok: true }
/api/maintenance/purge POST x-maintenance-secret header Service Run purgeOrphanedData
/api/maps/$mapId/export GET getClaims + assertMapAccess RLS Download ?format=graphml|csv|json
/api/maps/$mapId/import POST getClaims + assertMapAccess RLS Import a raw body, ?format=graphml|json

Notes:

  • /api/health is Render’s healthCheckPath.
  • /api/maintenance/purge returns 503 when MAINTENANCE_PURGE_SECRET is unset and 401 on a wrong secret. The daily Render cron calls it. The sweep removes dead-map network rows, purges soft-deleted orgs past retention and fails stuck imports (server/lib/maintenance.ts).
  • /api/maps/$mapId/import rejects a body over 10 MB (by Content-Length) with 413. It always creates new rows in an existing map.
  • These routes answer errors as { error: { message } } with 400, 401 or 403.

Every v1 route authenticates an API key and uses the service-role client. Handlers live in server/api/v1/. v1Handlers answers any other method with 405.

Route Method Scope Handler Purpose
/api/v1/maps GET org handleListMaps List the key’s org’s maps
/api/v1/maps POST org handleEnsureMap Find or create a map by name
/api/v1/maps/$mapId PATCH map handleRenameMap Rename a map
/api/v1/maps/$mapId/changes POST map handleChanges Create or update nodes and connections
/api/v1/maps/$mapId/import POST map handleImport Import a document
/api/v1/maps/$mapId/export.graphml GET map handleExportGraphml Export GraphML
/api/v1/maps/$mapId/fields GET map handleListFields List the catalogue
/api/v1/maps/$mapId/fields POST map handleEnsureField Find or create a field
/api/v1/maps/$mapId/fields/$fieldId PATCH map handleUpdateField Update a field
/api/v1/maps/$mapId/recipe GET map handleGetRecipe Read the recipe
/api/v1/maps/$mapId/recipe PUT map handlePutRecipe Write the recipe
/api/v1/maps/$mapId/recipe/validate POST map handleValidateRecipe Confirm the recipe

Notes:

  • org scope is requireOrgScope; map scope is requireMapScope, which also checks the map is in the key’s org (server/api/v1/auth.ts).
  • JSON bodies are capped at 1 MB. Import and changes bodies are capped at 10 MB.
  • Errors use { error: { code, message, details? } }; see Error codes.