ADR-001 — Circle Graph Data Model and Database Selection¶
| Field | Value |
|---|---|
| Status | Accepted |
| Date | 2026-04-30 |
| Project | MPowerUP |
| Deciders | Kevin Crump |
Context¶
MPowerUP organises users into trusted Circles — groups of people who share safety information, send help requests, and vouch for each other during recovery and emergency situations. The data model for Circles must support:
- A user belonging to multiple overlapping Circles
- Facilitator roles within specific Circles
- Trust relationships between individual users that exist independently of Circle membership
- Operations that are inherently graph problems: Circle vouch recovery (find 2+ members who can confirm identity), Sybil detection (identify suspiciously clustered fake accounts), shortest trust path between users, and revocation quorum tracking
- A decentralised, offline-first architecture where the social graph must never be centralised on a server
- A relay layer that coordinates cross-device operations but must not persist social-graph data
The existing stack uses WatermelonDB (SQLite) on device and a Node.js relay on Fly.io. Any data store must be open source and runnable without paid managed services.
Decision¶
Model Circles as a hybrid graph: explicit Circle nodes for access control, combined with direct TRUSTS edges between users that exist independently of Circle membership.
Device layer — Yjs-encoded graph + SQLite adjacency tables¶
The social graph on device is encoded in two complementary ways:
- Yjs CRDT graph encoding —
Y.Mapfor nodes (users, circles),Y.MapofY.Arrayfor adjacency lists. Because MPowerUP already uses Yjs for state synchronisation over libp2p, this gives distributed, offline-first graph sync with zero additional dependencies. The sync problem is already solved. - SQLite adjacency tables via WatermelonDB — for queryable persistence. The Yjs state is mirrored into SQLite tables so that graph traversal can be performed with SQL recursive CTEs without loading the full graph into memory. This is the query layer; Yjs is the sync layer.
This combination avoids introducing a new native-module dependency into the React Native / Expo build, which would complicate EAS builds and increase the native surface area.
Kùzu (embedded graph DB, C++ core, MIT licensed, Cypher query language) is identified as the preferred long-term replacement for the SQLite query layer once stable React Native bindings exist. The architecture is designed so that Kùzu can be slotted in as a drop-in replacement for the adjacency-table queries when that time comes.
Relay layer — FalkorDB (Redis module)¶
The relay uses FalkorDB (Redis graph module, Server Side Public License) for structural graph operations that require cross-device coordination: revocation quorum vote tracking, Sybil detection (community detection on invitation graphs), Circle size-cap enforcement, and cross-Circle trust-path queries.
FalkorDB is in-memory by default, which aligns directly with the relay's data-retention policy: no persistent social-graph data, all structural metadata purged after session close. Cypher is shared with the long-term device-side target (Kùzu), reducing the query-language surface area.
Schema¶
Nodes:
User { did, display_name, joined_at, trust_tier }
Circle { id, name, created_at, type }
Org { did, name, verified, badge_level }
Edges:
(User)-[:MEMBER_OF { role, joined_at, invited_by_did }]->(Circle)
(User)-[:TRUSTS { weight, since, source }]->(User)
(User)-[:INVITED { at, accepted }]->(User)
(Org) -[:MEMBER_OF { role: 'facilitator', verified_at }]->(Circle)
(User)-[:CREATED { at }]->(Circle)
The facilitator role is a property on the MEMBER_OF edge, not a separate node type. One user can be a regular member in one Circle and a facilitator in another.
Consequences¶
Positive¶
- Graph traversal operations (vouch recovery, Sybil detection, revocation quorum) are expressed as native graph queries rather than application-layer loops over relational results
- No new native module in the React Native build — Yjs and SQLite are already present
- The relay's in-memory graph naturally enforces the no-persistence policy without additional configuration
- Cypher is used at both layers, reducing the total query-language surface area
- Kùzu provides a clear upgrade path for the device layer when bindings are available
Negative / trade-offs¶
- SQLite recursive CTEs for graph traversal are verbose and harder to read than Cypher; accepted as a temporary measure until Kùzu bindings are available
- FalkorDB's SSPL restricts offering FalkorDB itself as a managed service — not a concern for BNI's use case, but worth noting
- Two representations of the graph on device (Yjs + SQLite) must be kept in sync; a sync bug would produce inconsistent query results
Neutral¶
- Derived Circles (communities computed from the trust graph rather than explicitly declared) are deferred to Phase 3. The explicit
Circlenode model does not prevent this — derived communities would be written back asCirclenodes with atype: 'derived'flag - The relay graph is scoped to structural metadata only (DIDs, edge existence, timestamps); no content, display names, or message data ever enters the relay graph store
Alternatives considered¶
- Neo4j Community (relay) — mature Cypher, large ecosystem. Rejected: JVM memory overhead for a lightweight relay; Community-edition clustering limits. Good if analytics tooling is added later.
- ArangoDB (relay or device) — multi-model, Apache 2.0. Capable but more than the relay's narrow use case needs.
- Apache AGE (PostgreSQL extension) — Cypher on Postgres. Rejected: relay's in-memory requirement is hard to satisfy with Postgres; real-time graph performance trails FalkorDB/Memgraph.
- Memgraph (relay) — in-memory, C++, Cypher. Strong alternative; FalkorDB preferred for the simpler Redis-module operational profile. Memgraph is the preferred fallback if FalkorDB's SSPL becomes a concern.
- SurrealDB (device) — multi-model, Rust, WASM-capable, graph-native. Most credible candidate to replace both WatermelonDB and the Yjs graph encoding. Rejected for now: replaces the entire device data layer (large migration) and the RN wrapper is immature. Revisit at Phase 3.
- Realm / Atlas Device SDK (device) — offline-sync object DB. Rejected: sync requires MongoDB Atlas (commercial dependency); not a graph DB natively.
Review trigger¶
Revisit when: Kùzu releases stable React Native / Expo bindings; SurrealDB's RN wrapper reaches production stability; the relay's data requirements expand beyond structural metadata; or FalkorDB's licensing changes in a way that affects self-hosted use.