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.
Conventions
Section titled “Conventions”- Method.
GETfor reads.POSTfor writes, and for reads whose input is large, since aGETcarries its input in the URL.getImportStatusandgetMapRecipearePOSTreads because repeated identicalGETcalls are unreliable in the framework version in use. - Guard. Tenant functions declare
orgMiddleware(orgin the tables). It runsauthMiddleware(auth), which validates the JWT withgetClaims()and redirects to/loginwhen there is none. Neither middleware checks membership. The handler calls a helper fromserver/lib/authz.tsbefore touching data:assertOrgMember,assertOrgRole,assertMapAccess,assertCanEditMaporassertSystemAdmin. A function with no helper in the table relies on RLS alone. - Client.
RLSmeans the caller’s cookie-scoped client (getSupabaseServerClient).ServicemeansgetSupabaseServiceRoleClient, which bypasses RLS. For a service-role call the authz helper is the only boundary. - Change capture. Graph writes take an optional
session_id(fromstartSession) and record anapp.atomic_changerow throughserver/lib/change-capture.ts. Without asession_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 aCODE: messageprefix; 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
Section titled “Manual editing”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:
deleteMapcallsnetwork.delete_mapfirst, becausenetworkrows have no FK toapp.map. It then removes the map’s import files from themap-importsbucket with the service-role client, and deletes theapp.maprow last.getMapSummaries’last_edited_atis the newest captured change or completed (ingested) import, ornull. Opening a map starts a session but captures nothing, so it doesn’t count. Renames and visibility changes show inapp.map.updated_atinstead.createNodeandcreateConnectionaccept a client-generated id (node_id,id) for optimistic inserts.setNodeFieldupsertsnetwork.node_measureon(node_id, measure_id).createConnectionandupdateConnectionacceptdatesas partial ISO strings (2024,2024-06,2024-06-15). InupdateConnection,nullclears a date and an absent key leaves it alone.swap_endpointsreverses the connection.placeNodesrefuses a request that would take the map past 200 placed nodes.createMapwithstarter_tags: true(the home view’s blank map only) adds a Type group with Person and Organisation and makes it the map’sdefault_group_by. A failure there is logged; the map is still returned.setTagOnNodesrecords onenodes.tags_changedchange. Adding a grouped tag first deletes the nodes’ other memberships in that group, and puts them back if the insert fails.moveNodestakes at most 200 moves and only moves nodes that are already placed.placeNodes,moveNodesandunplaceNodeswrite throughnetwork.set_node_positionsand fail if any named node isn’t in the map.getUnplacedNodestakeslimitandoffsetand orders bycreated_at, thenid.
Data upload
Section titled “Data upload”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:
getSignedUploadUrlderives the org frommap_id. It never acceptsorg_id. The path is{orgId}/{mapId}/{uuid}-{filename}.createImportSessionreads the files with the service-role client, since the caller’s session can’t read objects uploaded with a signed token.ingestImportSessionreturns{ started: true }and runs the ingest as a detached task with the service-role client. PollgetImportStatus. A session stuck iningestingis failed by the maintenance sweep.ingestImportSessionneeds statuspreviewingormapping. A GraphML import, or a CSV import with a connections file, needsmapping.
Visualisation
Section titled “Visualisation”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:
getGraphtakes only{ map_id }and returns every node, placed or not. Callers filter to placed nodes themselves. A public map’s graph still needs membership.
Storage and revision
Section titled “Storage and revision”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:
startSessionreturns the caller’s oldestactivesession for the map, or starts one (ensureActiveSession).captureChangechecks access against the session’s map, and refuses an event type that isn’t inDATA_EVENT_TYPESorLAYOUT_EVENT_TYPES.
Workspace and permissions
Section titled “Workspace and permissions”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:
assertOrgRolealso passes for a system admin.my_roleissystem_adminwhenever the caller is one, otherwise their membership role, otherwisenull.- A system admin sees every org through
getOrgs.is_memberis true only where the caller has a membership row, and the home page’s organisation menu lists only those. deleteOrgsetsdeleted_at. The maintenance sweep purges the org’s data after the retention window.createInvitationandresendInvitationbuild the email link with the service-role Admin API (invite, thenmagiclink, then a plain/loginlink). Sending is best-effort. An invitation expires after 7 days.createInvitationrefuses an email that already belongs to a member or has a pending invitation.acceptInvitationchecks the caller’s email is confirmed and matches the invitation. It is idempotent: an existing membership returns{ ok: true }.removeMemberandchangeMemberRolerefuse to leave an org with noorg_admin.
Fields
Section titled “Fields”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:
createFieldandupdateFieldacceptvalue_labelandvalue_unit, for numeric connection types only.- A tag’s
categorynames its tag group. The group is created if it doesn’t exist. updateFieldcan turn a measure into a tag, which discards its values. A tag can’t become a measure.deleteFielddeletes node values and tag memberships with the definition. Connections stay, withmetric_idandvaluecleared.createFieldForMapandupdateFieldForMaphold the logic and are shared with the public API.updateFieldForMaptakes a callback to record a change that rewrote values, so each caller records it its own way.
Tag groups
Section titled “Tag groups”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 (bycategory) does the same check. Memberships move before the tag row, so a refused or failed move changes nothing. deleteTagGrouprelies on the composite FKs’set nullfortagandnode_tag, then clearsdefault_group_byin a separateappwrite.
Map recipe
Section titled “Map recipe”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. saveMapRecipetakesbase_updated_at, theupdated_atthe editor started from (nullfor a new recipe), andoverwrite. It returns{ status, recipe }, wherestatusissaved,unchangedorconflict.copyMapRecipesaves without confirming, credited to the caller.duplicateMapcalls it last and only logs a failure, since the copy is complete without it.
Profiles and setup
Section titled “Profiles and setup”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:
updateSettingsshallow-merges the given keys into the stored settings.getAppStatusreturnsnoSchema: truewhen theappschema isn’t migrated.initializeApprefuses to run once any system admin exists.
Route-local server functions
Section titled “Route-local server functions”| 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
Section titled “Server routes”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/healthis Render’shealthCheckPath./api/maintenance/purgereturns 503 whenMAINTENANCE_PURGE_SECRETis unset and 401 on a wrong secret. The daily Render cron calls it. The sweep removes dead-mapnetworkrows, purges soft-deleted orgs past retention and fails stuck imports (server/lib/maintenance.ts)./api/maps/$mapId/importrejects a body over 10 MB (byContent-Length) with 413. It always creates new rows in an existing map.- These routes answer errors as
{ error: { message } }with 400, 401 or 403.
Public API (v1)
Section titled “Public API (v1)”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:
orgscope isrequireOrgScope;mapscope isrequireMapScope, 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.