PluresDB Integration
January 28, 2026 · View on GitHub
PluresDB is Praxis's local-first reactive datastore. This document explains how to integrate PluresDB with your Praxis application.
Overview
PluresDB provides:
- Local-first storage: Data lives on the device
- Reactive queries: Automatic UI updates on data changes
- Sync: Automatic synchronization when connected
- CRDT-based conflict resolution: Handle concurrent edits
- Event sourcing: Full history of changes
flowchart TB
subgraph App["Praxis App"]
Engine[Logic Engine]
UI[UI Components]
end
subgraph PluresDB["PluresDB"]
Local[(Local Store)]
Sync[Sync Engine]
CRDT[CRDT Resolver]
end
subgraph Cloud["Praxis Cloud"]
Relay[Relay Server]
Remote[(Remote Store)]
end
Engine <--> Local
UI <--> Local
Local <--> Sync
Sync <--> CRDT
Sync <--> Relay
Relay <--> Remote
Setup
Installation
PluresDB is included with Praxis:
npm install @plures/praxis
Configuration
You can choose network-only (previous default) or the new local-first unified API (auto-detects WASM/Tauri/IPC/network).
Network (unchanged):
import { createPluresDB } from '@plures/praxis';
import { PluresNode } from '@plures/pluresdb';
const db = createPluresDB(new PluresNode({ autoStart: true }));
Local-first (auto-detect):
import { createPraxisLocalFirst } from '@plures/praxis';
// Auto mode picks the best backend (WASM in browser, Tauri/IPC on desktop, network fallback)
const db = await createPraxisLocalFirst({ mode: 'auto' });
// Optional: override
// const db = await createPraxisLocalFirst({ mode: 'wasm', dbName: 'my-app' });
// const db = await createPraxisLocalFirst({ mode: 'ipc', channelName: 'my-channel' });
From Schema
Generate PluresDB configuration from your PSF schema:
praxis generate --schema ./schema.psf.json --only pluresdb
Generated configuration:
// generated/pluresdb-config.ts
import { createPluresDB } from '@plures/praxis';
export const dbConfig = {
name: 'my-app',
version: 1,
collections: [
{
name: 'users',
schema: {
id: { type: 'uuid', primary: true },
username: { type: 'string', indexed: true },
email: { type: 'string', indexed: true, unique: true },
createdAt: { type: 'datetime' },
},
},
{
name: 'posts',
schema: {
id: { type: 'uuid', primary: true },
title: { type: 'string' },
content: { type: 'string' },
authorId: { type: 'string', ref: 'users' },
},
},
],
};
export const db = createPluresDB(dbConfig);
CRUD Operations
Create
// Insert a single document
const user = await db.users.insert({
id: crypto.randomUUID(),
username: 'alice',
email: 'alice@example.com',
createdAt: new Date(),
});
// Insert multiple documents
const users = await db.users.insertMany([
{ id: '1', username: 'bob', email: 'bob@example.com' },
{ id: '2', username: 'carol', email: 'carol@example.com' },
]);
Read
// Find by ID
const user = await db.users.findById('user-123');
// Find one by query
const alice = await db.users.findOne({ username: 'alice' });
// Find all matching
const admins = await db.users.find({ role: 'admin' });
// Find with options
const recentUsers = await db.users.find(
{ role: 'user' },
{
sort: { createdAt: -1 },
limit: 10,
skip: 0,
}
);
Update
// Update by ID
await db.users.updateById('user-123', {
$set: { username: 'alice2' },
});
// Update matching documents
await db.users.updateMany({ role: 'guest' }, { $set: { active: false } });
// Replace document
await db.users.replaceById('user-123', {
id: 'user-123',
username: 'newname',
email: 'new@example.com',
});
Delete
// Delete by ID
await db.users.deleteById('user-123');
// Delete matching documents
await db.users.deleteMany({ active: false });
Reactive Queries
PluresDB queries are reactive - they automatically update when data changes.
Using Subscriptions
// Subscribe to all users
const unsubscribe = db.users.subscribe(
{}, // Query
(users) => {
console.log('Users updated:', users);
}
);
// Later: unsubscribe
unsubscribe();
With Svelte
<script lang="ts">
import { db } from '../db';
// Reactive query as a store
const users = db.users.query({});
// Derived query
const activeUsers = db.users.query({ active: true });
</script>
<ul>
{#each $users as user}
<li>{user.username}</li>
{/each}
</ul>
With Praxis Engine
import { usePraxisEngine } from '@plures/praxis/svelte';
import { db } from '../db';
// Subscribe to database changes and dispatch engine events
db.users.subscribe({}, (users) => {
engine.dispatch([UsersUpdated.create({ users })]);
});
// Or integrate directly
const engine = createPraxisEngine({
initialContext: { users: [] },
registry,
plugins: [createPluresDBPlugin(db)],
});
Sync
Enable Sync
const db = createPluresDB({
name: 'my-app',
collections: ['users', 'posts'],
sync: {
enabled: true,
endpoint: 'https://your-relay.com',
authToken: 'user-token',
autoSync: true,
syncInterval: 5000,
},
});
Manual Sync
// Trigger sync manually
await db.sync();
// Sync specific collection
await db.users.sync();
// Check sync status
const status = db.getSyncStatus();
console.log(status);
// { lastSync: Date, pending: 5, syncing: false }
Sync Events
db.on('sync:start', () => {
console.log('Sync started');
});
db.on('sync:complete', (result) => {
console.log('Sync complete:', result);
});
db.on('sync:error', (error) => {
console.error('Sync error:', error);
});
db.on('sync:conflict', (conflicts) => {
console.log('Conflicts detected:', conflicts);
});
Conflict Resolution
PluresDB uses CRDTs for conflict-free synchronization.
Default Strategy: Last-Write-Wins
By default, the most recent change wins:
const db = createPluresDB({
name: 'my-app',
collections: ['notes'],
sync: {
conflictResolution: 'last-write-wins',
},
});
Custom Resolution
const db = createPluresDB({
name: 'my-app',
collections: ['notes'],
sync: {
conflictResolution: 'custom',
resolveConflict: (local, remote, base) => {
// Merge logic
return {
...base,
...remote,
...local,
mergedAt: new Date(),
};
},
},
});
Field-Level CRDTs
For fine-grained merging:
const db = createPluresDB({
collections: [
{
name: 'documents',
schema: {
id: { type: 'uuid' },
title: { type: 'string', crdt: 'lww' }, // Last-write-wins
content: { type: 'string', crdt: 'rga' }, // Sequence CRDT
tags: { type: 'array', crdt: 'or-set' }, // Add-wins set
viewCount: { type: 'number', crdt: 'counter' }, // Counter
},
},
],
});
Offline Support
PluresDB works offline by default:
Checking Connection
// Check if online
const isOnline = db.isOnline();
// Subscribe to connection changes
db.on('online', () => console.log('Back online!'));
db.on('offline', () => console.log('Gone offline'));
Pending Changes
// Get pending changes (not yet synced)
const pending = await db.getPendingChanges();
console.log(`${pending.length} changes waiting to sync`);
// Clear pending (discard local changes)
await db.clearPending();
Offline Queue
Operations are queued when offline:
// Works even when offline
await db.users.insert({ id: '1', name: 'Alice' });
// Check queue
const queue = db.getQueue();
console.log(`${queue.length} operations queued`);
// Queue syncs automatically when back online
Indexing
Define Indexes
const db = createPluresDB({
collections: [
{
name: 'posts',
schema: {
id: { type: 'uuid', primary: true },
title: { type: 'string' },
authorId: { type: 'string' },
createdAt: { type: 'datetime' },
tags: { type: 'array' },
},
indexes: [
{ fields: ['authorId'] },
{ fields: ['createdAt'], sort: 'desc' },
{ fields: ['authorId', 'createdAt'], unique: false },
{ fields: ['tags'], type: 'multikey' },
],
},
],
});
Query with Indexes
// Uses authorId index
const posts = await db.posts.find({ authorId: 'user-123' });
// Uses compound index
const recentPosts = await db.posts.find({ authorId: 'user-123' }, { sort: { createdAt: -1 } });
// Uses multikey index for array
const taggedPosts = await db.posts.find({ tags: 'javascript' });
Relationships
Define Relationships
From your PSF schema:
{
"models": [
{
"name": "Post",
"fields": [{ "name": "authorId", "type": "string" }],
"relationships": [
{
"name": "author",
"type": "many-to-one",
"target": "User",
"foreignKey": "authorId"
}
]
}
]
}
Query with Relationships
// Include related documents
const posts = await db.posts.find({}, { include: ['author'] });
// Each post has author populated
posts.forEach((post) => {
console.log(`${post.title} by ${post.author.username}`);
});
// Nested includes
const posts = await db.posts.find({}, { include: ['author', 'comments.author'] });
Integration with Logic Engine
Event Sourcing
Use PluresDB to persist facts:
import { createPraxisEngine } from '@plures/praxis';
import { db } from './db';
const engine = createPraxisEngine({
initialContext: { ... },
registry,
plugins: [
// Persist facts to PluresDB
{
afterStep: async (result) => {
for (const fact of result.state.facts) {
await db.facts.insert({
id: crypto.randomUUID(),
tag: fact.tag,
payload: fact.payload,
timestamp: Date.now(),
});
}
},
},
],
});
// Replay facts to restore state
async function restoreState() {
const facts = await db.facts.find({}, { sort: { timestamp: 1 } });
for (const fact of facts) {
engine.dispatch([fact]);
}
}
Reactive Context
Sync engine context with database:
// Update context when database changes
db.users.subscribe({}, (users) => {
engine.dispatch([UsersLoaded.create({ users })]);
});
// Persist context changes
engine.subscribe((state) => {
// Persist to database
db.state.replaceById('current', {
id: 'current',
context: state.context,
timestamp: Date.now(),
});
});
Migrations
Schema Migrations
const db = createPluresDB({
name: 'my-app',
version: 2, // Increment version
collections: ['users', 'posts'],
migrations: [
{
version: 2,
up: async (db) => {
// Add new field to existing documents
const users = await db.users.find({});
for (const user of users) {
if (!user.role) {
await db.users.updateById(user.id, {
$set: { role: 'user' },
});
}
}
},
down: async (db) => {
// Rollback migration
await db.users.updateMany(
{},
{
$unset: { role: true },
}
);
},
},
],
});
Best Practices
1. Define Clear Schemas
const db = createPluresDB({
collections: [
{
name: 'users',
schema: {
id: { type: 'uuid', primary: true },
username: { type: 'string', required: true },
email: { type: 'string', required: true, unique: true },
},
validate: true, // Enable validation
},
],
});
2. Use Transactions
await db.transaction(async (tx) => {
const user = await tx.users.insert({ ... });
await tx.profiles.insert({ userId: user.id, ... });
// Both succeed or both fail
});
3. Handle Errors
try {
await db.users.insert({ ... });
} catch (error) {
if (error.code === 'UNIQUE_CONSTRAINT') {
console.log('Email already exists');
} else if (error.code === 'VALIDATION_ERROR') {
console.log('Invalid data:', error.details);
}
}
4. Clean Up
// Close database when done
await db.close();
// Clear all data (development)
await db.clear();
Next: Code ↔ Canvas Sync