Documentation

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.

Object CRUD

create, read, list, update, remove over P2P replication.

Security default-on

Signing, encryption, Sloth VDF proof-of-work, and automatic peer bans.

Browser-first

IIFE, ESM, and CJS bundles. Optional IndexedDB persistence and React hooks.

Installation

npm
npm install dignity.js

For React hooks, install React 18+ as a peer dependency:

npm install dignity.js react react-dom

Quick start

Create a node, join a room, and replicate an object between two peers:

Node.js / ESM
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();
In-memory adapter InMemoryNetworkAdapter is for tests and local demos. Production browser apps use a WebRTC network adapter (see project issues for the browser transport roadmap).

Browser usage

Pre-built bundles are published to npm and available via CDN:

IIFE (global 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

Object API

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

Scoped broadcast

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.

Room / team discovery

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');

Direct secure messaging

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' });

Optimistic concurrency

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
}));

Security model

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)
Broadcast mode is not E2E All peers that know the scope password can decrypt broadcast traffic. Use direct mode for sensitive per-peer data.

Security configuration

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
  }
});

IndexedDB persistence

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();

React hooks

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

Signaling

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=peerjs
  • wss://0.peerjs.com/peerjs?key=peerjs

API reference

Primary class: DignityP2P

Constructor

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
})

Lifecycle

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

Events

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

Package exports

Import path Exports
dignity.js DignityP2P, IndexedDBPersistence, signaling providers, in-memory adapters, security utilities
dignity.js/react useDignity, useCollection, usePeers

Examples

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

Development

# 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