Skip to content

Known constraints & edge cases

  • 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 network data 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 into app.
  • shareMap and cross-org sharing — deferred from the shared-foundations milestone; the function stays NOT_IMPLEMENTED until a sharing model is designed.
  • access_request table and flow — not built; there is no “request access to an org/map” path yet, only admin-issued invitations.
  • Duplicate-pending-invitation preventioncreateInvitation allows inviting the same email to the same org more than once (no unique constraint on (organisation_id, email) where status = 'pending'). A schema change, so deferred rather than worked around in application code.
  • Complete deleted_at IS NULL filteringgetOrgs/getOrg filter soft-deleted organisations; auditing every other read path that touches organisation (directly or via a join) for the same filter is a follow-up.
  • No cross-schema SQL joins between app and network.
  • getClaims() not getSession() for server-side auth.
  • Double-guard every tenant-data server function (route guard and middleware).
  • Service-role writes stamp org_id (and user_id where relevant) on every row, since the service role bypasses RLS.
  • Exact-pin @tanstack/* and visualisation dependencies.
  • EU region for all data (Supabase + Render).
  • A node cannot connect to itself (DB check constraint).
  • A Field’s value type is numeric | boolean only — no text. A free-text observation is a node/connection properties entry, not a Field.
  • Connections have no strength/type scalar any more. A connection references a metric_id (nullable = undefined/TBD) and a value, plus a properties bag (reason is 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 (a properties.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. See isConnectionDefined (apps/app/src/graph/definedness.ts) and the weight modes in apps/app/src/graph/weight.ts.
  • Connections have no direction column 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 — removeMember and changeMemberRole both 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.