Getting started with NextGraph Framework
A complete example app demonstrating the NextGraph RDF ORM SDK with React, Vue, and Svelte frontends running side-by-side. Changes made in one framework instantly sync to the others β all data is encrypted.
This guide walks you through all the features of the SDK and how to build your own NextGraph-powered application.
Key Benefits for Developers
- Local-First: Data lives on the userβs device first, synced when online
- End-to-End Encrypted: All data is encrypted before leaving the device
- Real-Time Sync: Changes propagate instantly across devices and users
- Semantic Data: RDF-based data model enables powerful queries and interoperability
- Framework Agnostic: Works with React, Vue, Svelte, and plain JavaScript
What This Example Shows
This expense tracker demonstrates:
- β Schema Definition: Using SHEX to define typed data models
- β
Reactive Data Binding:
useShapehooks for automatic UI updates and reactivity - β Cross-Framework Sync: React, Vue, and Svelte sharing the same data
- β CRUD Operations: Creating, reading, updating expenses and categories
- β Relationships: Linking expenses to categories via IRIs
- β Sets & Collections: Managing collections of typed objects
- β Automatic Persistence: Changes saved instantly to NextGraph storage
Quick Start
- create a wallet at https://nextgraph.eu and log in once with your password.
You can find an example application called Expense Tracker in the following git repo.
# Clone the repository
git clone https://git.nextgraph.org/NextGraph/expense-tracker.git
cd expense-tracker
# Install dependencies
pnpm install
# Generate TypeScript types from SHEX schemas
pnpm build:orm
# Run the development server
pnpm dev
In Chrome, you have to go to chrome://flags/#local-network-access-check and select Disabled, because otherwise, the localhost iframe cannot be loaded from https://nextgraph.eu. We will fix that soon so your developer experience is smoother. This dev env issue has no impact on your production app deployed on your own domain, specially if you host your app with TLS.
- Open in your browser the URL displayed in console. Youβll be redirected to NextGraph to authenticate with your wallet, then your app loads inside NextGraphβs secure iframe.
Project Structure
src/
βββ shapes/ # Data model definitions
β βββ shex/
β β βββ expenseShapes.shex # SHEX schema (source of truth)
β βββ orm/
β βββ expenseShapes.typings.ts # Generated TypeScript interfaces
β βββ expenseShapes.shapeTypes.ts # Generated shape type objects
β βββ expenseShapes.schema.ts # Generated schema metadata
βββ frontends/
β βββ react/ # React components
β βββ vue/ # Vue components
β βββ svelte/ # Svelte components
βββ utils/
β βββ ngSession.ts # NextGraph session initialization
βββ app-wrapper/ # Astro app shell (hosts all frameworks)
Building Your Own App
If you want to create your own app, you can walk through the following steps.
You should use Vite as we havenβt tested any other bundler.
Step 1: Dependencies
Install the required NextGraph packages:
pnpm add @ng-org/web@latest @ng-org/orm@latest @ng-org/shex-orm@latest @ng-org/alien-deepsignals@latest
| Package | Purpose |
|---|---|
@ng-org/web | Core NextGraph SDK for web applications |
@ng-org/orm | Reactive ORM with framework adapters |
@ng-org/shex-orm | SHEX-to-TypeScript code generation |
@ng-org/alien-deepsignals | Deep reactivity primitives |
Unlike in the expense tracker demo app above, you probably wonβt need to load 3 different frontend frameworks (React, Vue, Svelte) nor Astro in your app. Just choose one of those framework when you create your Vite app.
Step 2: NextGraph Initialization
Your app runs inside a NextGraph-controlled iframe. To make things easier for you, we created a utility file that handles this, see src/utils/ngSession.ts.
The file exports an init() function. Call this as early as possible.
Step 3: Defining Data Shapes (Schema)
NextGraph uses SHEX (Shape Expressions) to define your data model. SHEX is a language to define RDF shapes. RDF (Resource Description Framework) is a way to represent data in a format that makes application interoperability easier. Under the hood, NextGraph comes with an RDF graph database. The ORM handles all interaction with the RDF database for you.
Get started by creating one or more .shex files, by example, if your data model is about Expense Tracking:
src/shapes/shex/expenseShapes.shex:
PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
ex:Expense {
a [ex:Expense] ; # Required type "Expense"
ex:title xsd:string ; # Required string
ex:description xsd:string ? ; # Optional string
ex:totalPrice xsd:float ; # Required number
ex:amount xsd:integer ; # Required integer
ex:dateOfPurchase xsd:date ; # Date as ISO string
ex:expenseCategory IRI * ; # Set of category IRIs
ex:isRecurring xsd:boolean ; # Boolean flag
ex:paymentStatus [ex:Paid] OR [ex:Pending] OR [ex:Overdue] ; # Enum
}
# In the same or another file...
ex:ExpenseCategory EXTRA a {
a [ ex:ExpenseCategory ] ;
ex:categoryName xsd:string ;
ex:description xsd:string ;
}
SHEX Cardinality Reference
| Syntax | Meaning | TypeScript Type |
|---|---|---|
prop xsd:string | Required, exactly one | string |
prop xsd:string ? | Optional, zero or one | string | undefined |
prop xsd:string * | Zero or more | Set<string> |
prop xsd:string + | One or more | Set<string> (non-empty) |
prop IRI | Reference to another object | string (IRI) |
Step 4: Generating TypeScript Types
Run the code generator. Itβs best to create a script in your package.json like:
"build:orm": "rdf-orm build --input ./src/shapes/shex --output ./src/shapes/orm"
Note: you need to have @ng-org/shex-orm installed.
Running this command creates:
*.typings.ts: TypeScript interfaces for your shapes*.shapeTypes.ts: Shape type objects to pass touseShape()*.schema.ts: Metadata used internally by the ORM
Donβt touch those files. If you need to change anything, just edit your .shex file and run npm build:orm again.
Step 5: Using Shapes in Components
Import the generated shape type and use the useShape hook:
import { useShape } from "@ng-org/orm/react";
// or @ng-org/orm/vue, @ng-org/orm/svelte depending on the frontend framework you want to use.
import { ExpenseShapeType } from "./shapes/orm/expenseShapes.shapeTypes";
// In your component:
const expenses = useShape(ExpenseShapeType);
The returned object behaves like a reactive Set<Expense> with special properties.
useShape(shape, scope) can also take an optional scope argument, that lets you restrict the scope of the ORM mechanism. If left empty, all the objects matching the shape, from the whole dataset of the user(union of all the stores) will be returned. If a specific document Nuri (IRI) is passed as scope, then only the objects found in that document will be returned.
Framework-Specific Guides
React
import { useShape } from "@ng-org/orm/react";
import { ExpenseShapeType } from "../../shapes/orm/expenseShapes.shapeTypes";
export function Expenses() {
const expenses = useShape(ExpenseShapeType);
// Iterate like a Set
return (
<ul>
{[...expenses].map((expense) => (
<li key={expense["@id"]}>
{/* Direct property access - changes trigger re-render */}
<input
value={expense.title}
onChange={(e) => (expense.title = e.target.value)}
/>
</li>
))}
</ul>
);
}
Key Points:
- Changes to
expense.titleautomatically re-render the component - No
setStateneeded β just mutate the object directly - If your environment does not support iterator objects yet, you can use the spread
[...expenses]to convert Set to array for.map().
Vue
<script setup lang="ts">
import { useShape } from "@ng-org/orm/vue";
import { ExpenseShapeType } from "../../shapes/orm/expenseShapes.shapeTypes";
const expenses = useShape(ExpenseShapeType);
</script>
<template>
<ul>
<li v-for="expense in expenses" :key="expense['@id']">
<input v-model="expense.title" />
</li>
</ul>
</template>
If you are passing the reactive data objects to child components, you must wrap the props with useDeepSignal:
<script setup lang="ts">
import { useDeepSignal } from "@ng-org/alien-deepsignals/vue";
const props = defineProps<{ expense: Expense }>();
const expense = useDeepSignal(props.expense); // Required for reactivity!
</script>
Svelte
<script lang="ts">
import { useShape } from "@ng-org/orm/svelte";
import { ExpenseShapeType } from "../../shapes/orm/expenseShapes.shapeTypes";
const expenses = useShape(ExpenseShapeType);
</script>
<ul>
{#each [...$expenses] as expense (expense['@id'])}
<li>
<input bind:value={expense.title} />
</li>
{/each}
</ul>
Key Points:
- Access the reactive store with
$expenses - Standard Svelte binding works (
bind:value) - FOr now we are still using Svelte 3 syntax. We will upgrade to Svelte 5 / Runes soon.
Understanding Reactivity
The Signals ORM uses deep reactive proxies. When you access or modify any property:
- Reads subscribe the component to that specific property
- Writes trigger re-renders in all subscribed components
- Nested objects are automatically proxied for deep reactivity
// All of these trigger appropriate re-renders:
expense.title = "New Title"; // Direct property
expense.nested.value = 42; // Nested object
expense.categories.add("food"); // Set mutation
delete expense.optionalField; // Deletion
Under the hood, the ORM listens for those changes and propagates the updates to the database immediately.
Cross-Component Updates
When you call useShape(ExpenseShapeType) in multiple components, they all share the same reactive data. A change in one component instantly appears in all others.
Working with Data
Creating Objects
To add a new object, you need a @graph IRI (NextGraph document Nuri).
reate a new document first:
import { sessionPromise } from "../../utils/ngSession";
const session = await sessionPromise;
// Create a new NextGraph document
const docNuri = await session.ng.doc_create(
session.session_id,
"Graph", // Document type (it could also be Automerge or Yjs)
"data:graph", // Content type (for now, always use data:graph)
"store", // Storage location
undefined // will create the document in the private store.
);
// Add object to the reactive set
expenses.add({
"@graph": docNuri, // Required: document IRI
"@type": "http://example.org/Expense", // Required: RDF type
"@id": "", // Empty string = auto-generate
title: "Groceries",
totalPrice: 42.5,
amount: 1,
dateOfPurchase: new Date().toISOString(),
paymentStatus: "http://example.org/Paid",
isRecurring: false,
expenseCategory: new Set(),
});
Modifying Objects
Simply assign new values:
expense.title = "Updated Title";
expense.totalPrice = 99.99;
expense.isRecurring = true;
Changes are:
- Immediately reflected in the UI
- Automatically persisted to NextGraph storage
- Synced to other connected clients in real-time (if there is network connectivity)
Watch Out: If you modify an object in a way that breaks any of the shapeβs constraints, e.g. by modifying the
@type, the object will βdisappearβ from ORM perspective. The data is not deleted (in RDF all data is stored atomically) but since it does not match the shape anymore, it is not shown in the frontend. You can still modify the data with SPARQL.The ORM supports nested objects as well. When you delete a nested object from a parent object, the nested object is not deleted. Only the link from the parent object to the nested object is removed.
Working with Sets
RDF is based on Sets, which are not available in JSON and programmers might not be used to them.
For properties with cardinality * or +, you get a reactive Set as there can be multiple values for the same property. We donβt use arrays in this case because those values are not ordered.
// Add a category reference
expense.expenseCategory.add("http://example.org/category:food");
// Remove a category
expense.expenseCategory.delete("http://example.org/category:food");
// Check membership
if (expense.expenseCategory.has(categoryIri)) { ... }
// Iterate
for (const iri of expense.expenseCategory) {
console.log(iri);
}
Relationships Between Objects
You can link objects between each other by storing the targetβs @id IRI:
// In ExpenseCard, link to a category:
expense.expenseCategory.add(category["@id"]);
// Later, resolve the relationship:
const linkedCategory = categories.find((c) => c["@id"] === categoryIri);
Advanced Features
SPARQL Queries
Access the full power of SPARQL through the NG session:
const session = await sessionPromise;
const results = await session.ng.sparql_query(
session.session_id,
`
SELECT ?expense ?title ?price
WHERE {
?expense a <http://example.org/Expense> ;
<http://example.org/title> ?title ;
<http://example.org/totalPrice> ?price .
FILTER (?price > 100)
}
`
);
Document Creation
Create documents with different privacy levels:
// Private document (only you can access)
const privateDoc = await session.ng.doc_create(
session.session_id,
"Graph",
"data:graph",
"store",
undefined // Uses default private store
);
// You can also specify store IDs for protected/public stores
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) at your option.
SPDX-License-Identifier: Apache-2.0 OR MIT
NextGraph received funding through the NGI Assure Fund and the NGI Zero Commons Fund, both funds established by NLnet Foundation with financial support from the European Commissionβs Next Generation Internet programme, under the aegis of DG Communications Networks, Content and Technology under grant agreements No 957073 and No 101092990, respectively.