ngSet.delete() updates the local reactive set but does not persist to the broker. Use ng.sparql_update() with SPARQL DELETE WHERE to remove RDF triples directly — the broker sends back a GraphOrmUpdate that reactively removes the item from the ORM set. The two methods must not be combined as they conflict in the CRDT. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.2 KiB
Data Layer
NextGraph-backed local-first data with fallback to local state for demo/disconnected mode.
Overview
The app has two data modes:
- Connected — NextGraph ORM shapes (P2P, encrypted, local-first)
- Disconnected/Demo — Local React state seeded from
seedData.ts
All screens use useFestipodData() hook regardless of mode.
NextGraph Stack
@ng-org/web # Browser WASM runtime
@ng-org/orm # RDF shape-based ORM
@ng-org/shex-orm # SHEX → TypeScript code generation
@ng-org/alien-deepsignals # Reactive signals bridge
Packages installed from local tarballs in .ng-tarballs/.
SHEX Shapes
src/shared/shapes/shex/festipodShapes.shex defines:
- Event — title, description, dates, location, themes, participants
- UserProfile — name, username, bio, city, visibility
- Participation — links event + user, confirmation status
ORM bindings in src/shared/shapes/orm/:
festipodShapes.schema.ts— Schema registrationfestipodShapes.shapeTypes.ts— Shape type constantsfestipodShapes.typings.ts— TypeScript interfaces
Regenerate with bun run build:orm.
NextGraph Read/Write Pattern
The app follows the same pattern as the official expense-tracker-rdf example:
- Scope:
useShape(shapeType,did🆖${session.private_store_id})— opens the private store repo in the verifier - @graph:
did:ng:${session.private_store_id}— writes target the same NURI
This is critical: orm_start_graph with the private store NURI explicitly opens the repo in the verifier's self.repos HashMap. Without this, orm_frontend_update fails with RepoNotFound.
Do NOT use did:ng:i as scope — it subscribes to the entire user site via a special code path that doesn't open individual repos, breaking all writes.
Deleting Objects
ngSet.delete(item) updates the local reactive set but does not persist to the broker. Use ng.sparql_update() with SPARQL DELETE instead:
import { ng } from '@ng-org/web';
import { sessionPromise } from '../utils/ngSession';
const session = await sessionPromise;
await ng.sparql_update(
session.session_id,
`DELETE WHERE { GRAPH <${item["@graph"]}> { <${item["@id"]}> ?p ?o } }`,
item["@graph"],
);
The broker sends back a GraphOrmUpdate with op: "remove" that reactively removes the item from the ORM set. Do NOT combine with ngSet.delete() — the two operations conflict in the CRDT.
See decision record for details.
Key files
src/shared/hooks/useShapeWithDefaults.ts— AcceptsstoreNuriparam, passes touseShapesrc/shared/utils/ngGraph.ts—ensureGraphNuri()returns@graphfor entity creationsrc/shared/utils/ngBootstrap.ts— Seeds test data usingensureGraphNuri()for@graph
See decision record for why.
Context Providers
NextGraphContext (src/shared/context/NextGraphContext.tsx)
- Connection lifecycle:
disconnected→connecting→connected|error - Provides session with store IDs (private, protected, public)
- Conditional auto-init: Only auto-calls
initNg()when running inside the broker iframe (window.self !== window.top). Outside the iframe,initNgWeb()would redirect the page to the broker — so connection waits for explicitconnect()call. connect(): Called by user clicking "Se connecter". When outside broker, triggers the redirect flow.
@ng-org/web redirect behavior
initNgWeb() checks window.self === window.top. If the app is NOT in an iframe, it redirects to nextgraph.net/redir/ with the current URL encoded as a return parameter. The broker then loads the app back in an iframe after auth. This means the app must NOT auto-init NG when loaded standalone.
FestipodDataContext (src/shared/context/FestipodDataContext.tsx)
- Wraps NextGraph shapes with
useShapeWithDefaults()hook - CRUD:
createEvent(),updateEvent(),joinEvent(),leaveEvent(), etc. - Exposes
useFestipodData()hook consumed by all screens selectedEventIdstate for cross-screen event navigationloadTestData(): CallsbootstrapWallet()to seed test data into NG wallet — only triggered by explicit user action- Provider states based on NG status:
disconnected→LocalDataProviderwith seed data (demo mode)connecting→LocalDataProviderwith empty data (avoids flashing seed data before wallet loads)connected→NgDataProviderwith real wallet dataerror→LocalDataProviderwith seed data (graceful fallback)
Data Types
src/shared/data/types.ts:
FpEventData— id, title, date, location, distance, themes, etc.FpUserData— id, name, username, bio, city, countsFpParticipationData— eventId + userId + confirmedFpMeetingPointData— eventId, location, time, host (local-only)FpFriendshipData— userId + friendId (local-only)
Seed Data
src/shared/data/seedData.ts:
- 10 users (Marie Dupont = current user,
user-1) - Multiple events with dates, locations, themes
- Participations, meeting points, friendships
CURRENT_USER_ID = 'user-1'