Object CRUD
create, read, list, update, remove over P2P replication.
dignity.js is a REST-like P2P object API for decentralized JavaScript applications. Synchronize shared objects across browsers with owner authorization, scoped broadcast encryption, and built-in anti-abuse controls.
create, read, list, update, remove over P2P replication.
Signing, encryption, Sloth VDF proof-of-work, and automatic peer bans.
IIFE, ESM, and CJS bundles. Optional IndexedDB persistence and React hooks.
npm install dignity.js
For React hooks, install React 18+ as a peer dependency:
npm install dignity.js react react-dom
Create a node, join a room, and replicate an object between two peers:
const {
DignityP2P,
InMemoryNetworkHub,
InMemoryNetworkAdapter
} = require('dignity.js');
const hub = new InMemoryNetworkHub();
const alice = new DignityP2P({
nodeId: 'alice',
networkAdapter: new InMemoryNetworkAdapter(hub),
security: {
appPassword: 'shared-out-of-band-password',
powSteps: 22
}
});
const bob = new DignityP2P({
nodeId: 'bob',
networkAdapter: new InMemoryNetworkAdapter(hub),
security: {
appPassword: 'shared-out-of-band-password',
powSteps: 22
}
});
await alice.start();
await bob.start();
await alice.joinDiscovery('main', { metadata: { nickname: 'alice' } });
await bob.joinDiscovery('main', { metadata: { nickname: 'bob' } });
await alice.create('notes', { title: 'hello decentralized world' }, {
id: 'note-1',
broadcastScope: 'main'
});
console.log(bob.read('notes', 'note-1'));
await alice.leaveDiscovery('main');
await bob.leaveDiscovery('main');
await alice.stop();
await bob.stop();
InMemoryNetworkAdapter is for tests and local demos. Production browser apps use a WebRTC network adapter (see project issues for the browser transport roadmap).
Pre-built bundles are published to npm and available via CDN:
DignityJS)
<script src="https://unpkg.com/dignity.js/dist/dignity.min.js"></script>
<script>
const { DignityP2P } = DignityJS;
</script>
| Bundle | Path | Format |
|---|---|---|
| Minified browser | dist/dignity.min.js |
IIFE |
| ES modules | dist/dignity.esm.js |
ESM |
| Node / CommonJS | dist/dignity.cjs.js |
CJS |
The core API mirrors REST semantics. Each object belongs to a collection. The peer that creates an object becomes its owner — only the owner may update or delete it.
| Method | Description | Authorization |
|---|---|---|
create(collection, data, options?) |
Create a new object. Returns the normalized record. | Creator becomes owner |
read(collection, id) |
Read one object, or null if missing/deleted. |
— |
list(collection, options?) |
List objects. Set includeDeleted: true for tombstones. |
— |
update(collection, id, patch, options?) |
Merge patch into object data and increment version. |
Owner only |
updateWithRetry(collection, id, patchFn, options?) |
Read-modify-write with automatic retry on version conflicts. | Owner only |
remove(collection, id, options?) |
Soft-delete the object (tombstone). | Owner only |
Pass broadcastScope on create/update to select a team or room password namespace:
await node.create('matches', { mode: 'coop' }, {
id: 'm-1',
broadcastScope: 'coop:red'
});
Configure per-scope passwords via security.broadcastPasswords.
Discover active peers in a named scope (room, team, raid, etc.):
await node.joinDiscovery('team:red', {
metadata: { nickname: 'alice' },
heartbeatIntervalMs: 15000,
ttlMs: 45000
});
const peers = node.listPeers('team:red', { includeSelf: false });
await node.leaveDiscovery('team:red');
Send end-to-end encrypted messages to a specific peer:
alice.registerPeerPublicKey('bob', bob.getPublicKey());
bob.registerPeerPublicKey('alice', alice.getPublicKey());
await alice.sendDirectMessage('bob', 'dm', { text: 'private payload' });
Every update carries a monotonic version. Stale operations are rejected when
baseVersion does not match. Listen for conflict events or use
built-in helpers:
node.on('conflict', (event) => {
// event.phase: 'local' | 'remote'
console.log(event.expectedVersion, event.currentVersion);
});
// Fail fast on stale local writes
await node.update('games', 'g1', { score: 10 }, { expectedVersion: 3 });
// Automatic retry for read-modify-write loops
await node.updateWithRetry('games', 'g1', (current) => ({
score: current.data.score + 1
}));
All security features are enabled by default. Configure via the security constructor option.
| Feature | Default | Details |
|---|---|---|
| Signing | On | Ed25519 signature on every message |
| Broadcast encryption | On | AES secretbox; PBKDF2-SHA256 key from appPassword (100k iterations) |
| Direct encryption | On | NaCl box (X25519) — true E2E to recipient public key |
| Proof-of-work | On | Sloth VDF; default powSteps: 22 (~1s on reference hardware) |
| Peer bans | On | Invalid signature or PoW → 48h ban (configurable) |
const node = new DignityP2P({
nodeId: 'alice',
networkAdapter,
security: {
appPassword: 'shared-out-of-band-password',
broadcastPasswords: {
'coop:red': 'red-team-secret',
'coop:blue': 'blue-team-secret'
},
powSteps: 22,
kdfIterations: 100000,
banDurationMs: 48 * 60 * 60 * 1000
}
});
Survive page reloads by persisting replicated object state to IndexedDB:
const { DignityP2P, IndexedDBPersistence } = require('dignity.js');
const node = new DignityP2P({ nodeId, networkAdapter, security });
const persistence = new IndexedDBPersistence({
dbName: 'my-app',
collections: ['games', 'matches'] // omit to persist all collections
});
await node.start();
await persistence.attach(node);
// Later
await persistence.detach();
Optional integration via dignity.js/react (requires React ≥ 18):
import { useDignity, useCollection, usePeers } from 'dignity.js/react';
function Room() {
const { node, status, error } = useDignity(config);
const games = useCollection(node, 'games');
const peers = usePeers(node, 'room:chess', { includeSelf: false });
if (status === 'starting') return <p>Connecting…</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<pre>{JSON.stringify({ status, games, peers }, null, 2)}</pre>
);
}
| Hook | Returns |
|---|---|
useDignity(config) |
{ node, status, error } — starts/stops node on mount/unmount |
useCollection(node, name) |
Reactive array from node.list(name) |
usePeers(node, scope, options?) |
Reactive peer list from discovery |
Default PeerJS-compatible signaling endpoints are included. Customize with createDefaultSignalingPool:
const {
createDefaultSignalingPool,
WebSocketSignalingProvider
} = require('dignity.js');
const pool = createDefaultSignalingPool({
cloudflareUrls: ['wss://your-endpoint.example/peerjs?key=peerjs'],
fallbackUrls: ['wss://relay-a.example', 'wss://relay-b.example'],
customProviders: [
new WebSocketSignalingProvider({
id: 'local-dev',
url: 'ws://localhost:3001',
priority: 99
})
]
});
Default public endpoints:
wss://peerjs.92k.de/peerjs?key=peerjswss://0.peerjs.com/peerjs?key=peerjsPrimary class: DignityP2P
new DignityP2P({
nodeId, // required — unique peer identifier
networkAdapter, // required — transport implementation
security, // optional — MessageSecurityService options
now, // optional — clock injection (tests)
idGenerator // optional — custom operation id factory
})
| Method | Description |
|---|---|
start() | Connect adapter and begin receiving messages |
stop() | Leave discovery scopes and disconnect |
getPublicKey() | Return this node's public key bundle |
registerPeerPublicKey(id, key) | Trust a remote peer's keys |
Machine-readable metadata: openapi-like.json
DignityP2P extends EventEmitter. Common events:
| Event | Payload |
|---|---|
| change | { kind, collection, id } — object created, updated, or deleted |
| conflict | { kind, collection, id, expectedVersion, currentVersion, phase } |
| peerdiscovered | { scope, peerId, metadata } |
| peerleft | { scope, peerId, reason } |
| message | { senderId, targetId, type, payload } — custom decrypted messages |
| securityerror | { senderId, error } |
| warning | { type, ... } — non-fatal issues (heartbeat, persistence, etc.) |
| Import path | Exports |
|---|---|
dignity.js |
DignityP2P, IndexedDBPersistence, signaling providers, in-memory adapters, security utilities |
dignity.js/react |
useDignity, useCollection, usePeers |
| Script | Description | Run |
|---|---|---|
examples/decentralized-tictactoe.js |
Replicated board state and owner authorization | npm run example:tictactoe |
examples/decentralized-chess-lite.js |
Replicated move history with compact board model | npm run example:chess |
# Run tests (177+ passing, ~99.5% line coverage)
npm test
# Build browser + Node bundles
npm run build
# Serve docs locally
npm run docs:serve
# Run examples
npm run example:tictactoe
npm run example:chess