You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
Niko PLP 1900515eda new build:orm 3 weeks ago
src new build:orm 3 weeks ago
.gitignore moved from nextgraph-rs 1 month ago
LICENSE-APACHE2 moved from nextgraph-rs 1 month ago
LICENSE-MIT moved from nextgraph-rs 1 month ago
README.md fix dependencies 1 month ago
astro.config.ts moved from nextgraph-rs 1 month ago
package.json bump versions of @ng-org dependencies 3 weeks ago
pnpm-lock.yaml bump versions of @ng-org dependencies 3 weeks ago
svelte.config.js moved from nextgraph-rs 1 month ago
tsconfig.json moved from nextgraph-rs 1 month ago

README.md

NextGraph Expense Tracker Example

A complete example app demonstrating the NextGraph Signals 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 and stored locally-first.

This README walks you through all the features of the SDK and how to build your own NextGraph-powered application.


Table of Contents


About NextGraph

NextGraph brings about the convergence of P2P and Semantic Web technologies, towards a decentralized, secure and privacy-preserving cloud, based on CRDTs.

This open source ecosystem provides solutions for end-users (a platform) and software developers (a framework), wishing to use or create decentralized apps featuring: live collaboration on rich-text documents, peer to peer communication with end-to-end encryption, offline-first, local-first, portable and interoperable data, total ownership of data and software, security and privacy.

Centered on repositories containing semantic data (RDF), rich text, and structured data formats like JSON, synced between peers belonging to permissioned groups of users, it offers strong eventual consistency, thanks to the use of CRDTs. Documents can be linked together, signed, shared securely, queried using the SPARQL language and organized into sites and containers.

More info: https://nextgraph.org

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
  • 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

# Clone the repository (if not already done)
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
  • create a wallet at https://nextgraph.eu and log in once with your password.

  • Open in your browser the URL displayed in console. You'll be redirected to NextGraph to authenticate with your wallet, then the 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.

Step 1: Dependencies

Install the required NextGraph packages:

pnpm add @ng-org/web @ng-org/orm @ng-org/shex-orm @ng-org/alien-deepsignals
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

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 Create a utility file to handle initialization, 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 though.

Get started by creating one or more .shex files:

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.

This 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

Step 5: Using Shapes in Components

Import the generated shape type and use the useShape hook:

import { useShape } from "@ng-org/orm/react";  // or /vue, /svelte
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.


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
  • Spread [...expenses] to convert Set to array for .map(), if you your environment does not support iterator objects yet.

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>

For child components, wrap 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)

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 (document ID). Create a new document first:

const session = await sessionPromise;

// Create a new NextGraph document
const docId = await session.ng.doc_create(
    session.session_id,
    "Graph",           // Document type
    "data:graph",      // Content type
    "store",           // Storage location
    undefined          // Options
);

// Add object to the reactive set
expenses.add({
    "@graph": docId,                        // Required: document IRI
    "@type": "http://example.org/Expense",  // Required: RDF type
    "@id": "",                              // Empty = auto-generate
    title: "Groceries",
    totalPrice: 42.50,
    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

Watch Out: If you modify an object so that it does not meet the shape's constraint anymore, e.g. by modifying the @type, the object will "disappear". The other records are not deleted (in RDF all data is stored atomically) but since it does not match the shape, 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 root object, the nested object is not deleted. Only the link from the root object to the nested object is removed.

Working with Sets

For properties with cardinality * or +, you get a reactive Set:

// 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

Link objects 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 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",  // Uses default private store
    undefined
);

// 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.