|
|
3 weeks ago | |
|---|---|---|
| src | 3 weeks ago | |
| .gitignore | 1 month ago | |
| LICENSE-APACHE2 | 1 month ago | |
| LICENSE-MIT | 1 month ago | |
| README.md | 1 month ago | |
| astro.config.ts | 1 month ago | |
| package.json | 3 weeks ago | |
| pnpm-lock.yaml | 3 weeks ago | |
| svelte.config.js | 1 month ago | |
| tsconfig.json | 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
- NextGraph Expense Tracker Example
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:
useShapehooks 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 touseShape()*.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.titleautomatically re-render the component - No
setStateneeded — 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:
- 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 (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
- 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.