Known constraints & edge cases
Out of scope for V1
Section titled “Out of scope for V1”- Real-time collaborative editing (multiple users editing one map at once).
- Universal Nodes (cross-org node aliasing) — described conceptually but no table is created in V1.
- Anonymous read of
networkdata on public maps — public map metadata is readable, but node/connection RLS currently requires org membership; the public path is a tracked follow-up that must avoid crossing intoapp. shareMapand cross-org sharing — deferred from the shared-foundations milestone; the function staysNOT_IMPLEMENTEDuntil a sharing model is designed.access_requesttable and flow — not built; there is no “request access to an org/map” path yet, only admin-issued invitations.- Duplicate-pending-invitation prevention —
createInvitationallows inviting the same email to the same org more than once (no unique constraint on(organisation_id, email)wherestatus = 'pending'). A schema change, so deferred rather than worked around in application code. - Complete
deleted_at IS NULLfiltering —getOrgs/getOrgfilter soft-deleted organisations; auditing every other read path that touchesorganisation(directly or via a join) for the same filter is a follow-up.
Hard rules
Section titled “Hard rules”- No cross-schema SQL joins between
appandnetwork. getClaims()notgetSession()for server-side auth.- Double-guard every tenant-data server function (route guard and middleware).
- Service-role writes stamp
org_id(anduser_idwhere relevant) on every row, since the service role bypasses RLS. - Exact-pin
@tanstack/*and visualisation dependencies. - EU region for all data (Supabase + Render).
Domain rules
Section titled “Domain rules”- A node cannot connect to itself (DB check constraint).
- A Field’s value type is
numeric | booleanonly — notext. A free-text observation is a node/connectionpropertiesentry, not a Field. - Connections have no
strength/typescalar any more. A connection references ametric_id(nullable = undefined/TBD) and avalue, plus apropertiesbag (reasonis the reserved, always-offered key); multiple connections may exist between the same node pair (no uniqueness constraint on(map_id, source_node_id, target_node_id)), each observing a different field, or reason-only. Any scalar a layout/centrality algorithm needs is derived at render time from the set of defined connections — see the weight function in the API surface docs. - Connection “definedness” is a property OR a field-value, not just
metric_id != null. A reason-only connection (aproperties.reason, no field) counts as defined: it renders solid, is excluded from the “needs defining” nudge, and counts normally under the Presence and Count weight modes — but contributes nothing under Filter or Score (no field to match/sum against), so it’s dropped from those views like any zero-weight pair. SeeisConnectionDefined(apps/app/src/graph/definedness.ts) and the weight modes inapps/app/src/graph/weight.ts. - Connections have no
directioncolumn either. Direction lives on the field definition (metric.directionality:undirected | directed | mutual) and is derived per-connection from whichever field is assigned, never chosen per instance — see Database schema. This is deliberate: the same field must read the same way on every connection it’s used on, so there’s no per-connection override to reintroduce a mixed directed/undirected graph for one field. - Deleting a node removes its placement and all connections touching it on that map, however many there are; the node itself is removed only if it no longer appears on any map.
- Invitations expire after 7 days; accepting requires a verified email that case-insensitively matches the invitation, and is idempotent.
- An organisation can never be left with zero
org_admins —removeMemberandchangeMemberRoleboth refuse a change that would demote or remove the last one (application logic, not an RLS policy). - CSV/Excel imports: max 10 MB; at least one column must map to the node label; ingestion is atomic; duplicate rows and possible personal data are non-blocking warnings.
- GET server functions have a 1 MB payload limit — use POST for large inputs.