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: useShape hooks 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

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
PackagePurpose
@ng-org/webCore NextGraph SDK for web applications
@ng-org/ormReactive ORM with framework adapters
@ng-org/shex-ormSHEX-to-TypeScript code generation
@ng-org/alien-deepsignalsDeep 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

SyntaxMeaningTypeScript Type
prop xsd:stringRequired, exactly onestring
prop xsd:string ?Optional, zero or onestring | undefined
prop xsd:string *Zero or moreSet<string>
prop xsd:string +One or moreSet<string> (non-empty)
prop IRIReference to another objectstring (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 to useShape()
  • *.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.title automatically re-render the component
  • No setState needed β€” 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:

  1. Reads subscribe the component to that specific property
  2. Writes trigger re-renders in all subscribed components
  3. 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

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.