Local-first data toolkit for TypeScript.
Docs coming soon
Groundstate is a local-first data toolkit for TypeScript. Define your data model with schemas, get type-safe CRDTs with automatic conflict resolution, pluggable sync transports, offline-first storage, and framework bindings for React, Vue, and Svelte. It is a complete alternative to yjs and Automerge with a schema-first approach.
Schema-first CRDTs
- Type-safe schemas with full TypeScript inference
- 5 CRDT primitives: LWW Register, PN-Counter, RGA List, LWW Map, PeriText
- Declarative field types:
Field.string,Field.number,Counter,List.of,Field.text, and more
Sync
- 5 transports: WebSocket, WebRTC, HTTP polling, BLE, Filesystem
- Real-time presence and cursor tracking
- JWT authentication and role-based ACL
- Selective sync and bandwidth throttling
Storage
- 4 adapters: Memory, IndexedDB, SQLite, Filesystem
- At-rest encryption with key management
- Schema migrations with rollback support
- Query engine for filtering and sorting documents
Conflict Resolution
- 6 built-in strategies:
lww,pick,longest,sum,union,custom - Visual conflict diff and timeline viewer
- Immutable audit log of every resolution
Framework Bindings
- React:
useDoc,useField,usePresence,useSyncStatus - Vue:
useDoc,useField,usePresence,useSyncStatus - Svelte:
docStore,fieldStore,presenceStore,syncStatusStore
Developer Tools
- Document inspector with CRDT metadata
- Live operation stream
- Network simulator for convergence testing
- OpenTelemetry-compatible metrics
Ecosystem
- Cloudflare Durable Objects adapter
- SQLite-backed relay server
- yjs and Automerge import/export compatibility layer
| Package | Description | Version |
|---|---|---|
@groundstate/crdt |
Schema-defined CRDTs with TypeScript inference | |
@groundstate/sync |
Pluggable sync engine with 5 transports | |
@groundstate/store |
Offline-first storage layer | |
@groundstate/resolve |
Conflict detection and resolution | |
@groundstate/react |
React hooks and components | |
@groundstate/vue |
Vue 3 composables | |
@groundstate/svelte |
Svelte stores | |
@groundstate/devtools |
Inspector, metrics, and network simulator | |
@groundstate/server |
SQLite-backed relay server | |
@groundstate/cloudflare |
Cloudflare Durable Objects adapter | |
@groundstate/compat |
yjs and Automerge compatibility | |
groundstate |
All-in-one umbrella package |
# npm
npm install @groundstate/crdt @groundstate/sync @groundstate/store
# yarn
yarn add @groundstate/crdt @groundstate/sync @groundstate/store
# pnpm
pnpm add @groundstate/crdt @groundstate/sync @groundstate/storeimport { Doc, Field, Counter, List } from '@groundstate/crdt';
import { SyncEngine, WebSocketTransport } from '@groundstate/sync';
import {
PersistenceEngine,
IndexedDBAdapter,
} from '@groundstate/store';
// 1. Define a schema
const TodoSchema = {
title: Field.string(),
done: Field.boolean(false),
priority: Field.enum(['low', 'medium', 'high']),
votes: Counter(),
tags: List.of(Field.string()),
};
// 2. Create a document
const doc = Doc.create(TodoSchema, { id: 'todo-1' });
doc.title = 'Ship v1';
doc.priority = 'high';
doc.votes.increment(1);
doc.tags.push('release');
// 3. Persist offline
const store = new PersistenceEngine(new IndexedDBAdapter('my-app'));
await store.save(doc);
// 4. Sync with peers
const sync = new SyncEngine(doc);
const transport = new WebSocketTransport('wss://sync.example.com');
sync.connect(transport);# npm
npm install @groundstate/crdt @groundstate/react
# yarn
yarn add @groundstate/crdt @groundstate/react
# pnpm
pnpm add @groundstate/crdt @groundstate/reactimport { Doc, Field } from '@groundstate/crdt';
import {
GroundstateProvider,
useDoc,
useField,
usePresence,
} from '@groundstate/react';
const ProfileSchema = {
name: Field.string(),
bio: Field.text(),
};
function App() {
return (
<GroundstateProvider>
<ProfileEditor id="profile-1" />
</GroundstateProvider>
);
}
function ProfileEditor({ id }: { id: string }) {
const doc = useDoc(ProfileSchema, id);
const [name, setName] = useField(doc, 'name');
const peers = usePresence(doc);
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<p>{peers.length} peer(s) online</p>
</div>
);
}| Field | CRDT | TypeScript Type | Description |
|---|---|---|---|
Field.string() |
LWW Register | string |
Last-writer-wins string |
Field.number() |
LWW Register | number |
Last-writer-wins number |
Field.boolean() |
LWW Register | boolean |
Last-writer-wins boolean |
Field.text() |
PeriText | string |
Collaborative rich text |
Field.binary() |
LWW Register | Uint8Array |
Binary blob |
Field.timestamp() |
LWW Register | HLCTimestamp |
Hybrid logical clock timestamp |
Field.enum([...]) |
LWW Register | T |
Constrained string enum |
Field.register(val) |
LWW Register | T |
Generic LWW value |
Field.map() |
LWW Map | Record<string, unknown> |
String-keyed map |
Field.ref(schema) |
LWW Register | string | null |
Document reference |
Field.refList(schema) |
RGA List | string[] |
List of document references |
Counter() |
PN-Counter | number |
Increment/decrement counter |
List.of(def) |
RGA List | T[] |
Ordered list of items |
| Transport | Use Case | Requires Server |
|---|---|---|
WebSocketTransport |
Real-time, server-mediated | Yes |
WebRTCTransport |
Peer-to-peer, low latency | Signaling only |
HttpTransport |
Polling, corporate firewalls | Yes |
BLETransport |
Local mesh, mobile/IoT | No |
FilesystemTransport |
Shared folder (Dropbox, NAS) | No |
import {
SyncEngine,
WebSocketTransport,
WebRTCTransport,
} from '@groundstate/sync';
// WebSocket — real-time, server-mediated
const ws = new WebSocketTransport('wss://sync.example.com');
// WebRTC — peer-to-peer, no server required
const rtc = new WebRTCTransport({
signalingUrl: 'wss://signal.example.com',
});
// HTTP — polling, works behind corporate firewalls
const http = new HttpTransport({
url: 'https://api.example.com/sync',
interval: 5000,
});
// BLE — local mesh sync for mobile/IoT
const ble = new BLETransport();
// Filesystem — sync via shared folder (Dropbox, NAS)
const fs = new FilesystemTransport({ dir: '/shared/sync' });
// Connect any transport to the sync engine
const sync = new SyncEngine(doc);
sync.connect(ws);import { PresenceManager } from '@groundstate/sync';
const presence = new PresenceManager(sync);
presence.setLocal({ cursor: { x: 100, y: 200 }, name: 'Alice' });
presence.on('update', (peers) => {
console.log(
'Online:',
peers.map((p) => p.name),
);
});import { ACLManager } from '@groundstate/sync';
const acl = new ACLManager({
rules: [
{ role: 'editor', path: '*', permission: 'write' },
{ role: 'viewer', path: '*', permission: 'read' },
],
});| Adapter | Environment | Best For |
|---|---|---|
MemoryAdapter |
Any | Testing, ephemeral data |
IndexedDBAdapter |
Browser | Web apps, PWAs |
SQLiteAdapter |
Node / Electron | Desktop apps, servers |
FSAdapter |
Node | CLI tools, edge functions |
import {
PersistenceEngine,
IndexedDBAdapter,
EncryptionManager,
} from '@groundstate/store';
// IndexedDB (browser)
const store = new PersistenceEngine(new IndexedDBAdapter('my-app'));
// With encryption
const encrypted = new PersistenceEngine(
new IndexedDBAdapter('my-app'),
{ encryption: new EncryptionManager({ key: myKey }) },
);
// Save and load
await store.save(doc);
const loaded = await store.load('todo-1', TodoSchema);import { MigrationEngine } from '@groundstate/store';
const migrations = new MigrationEngine([
{
version: 2,
up: (doc) => {
doc.priority = doc.priority ?? 'medium';
},
},
]);
await migrations.run(store);import { QueryEngine } from '@groundstate/store';
const query = new QueryEngine(store);
const results = await query.find(TodoSchema, {
filter: { done: false },
sort: { field: 'priority', order: 'desc' },
limit: 20,
});import {
ConflictDetector,
PolicyEngine,
lww,
pick,
sum,
custom,
} from '@groundstate/resolve';
const policy = new PolicyEngine({
// Per-field strategies
rules: [
{ path: 'title', strategy: lww() },
{ path: 'votes', strategy: sum() },
{ path: 'tags', strategy: union() },
{ path: 'status', strategy: pick('server') },
{
path: 'priority',
strategy: custom((a, b) => (a.value === 'high' ? a : b)),
},
],
});
const detector = new ConflictDetector(doc);
const conflicts = detector.detect();
policy.resolveAll(conflicts);// Before (yjs)
import * as Y from 'yjs';
const ydoc = new Y.Doc();
const ymap = ydoc.getMap('todo');
ymap.set('title', 'Ship v1');
ymap.set('done', false);
// After (groundstate)
import { Doc, Field } from '@groundstate/crdt';
const doc = Doc.create(
{ title: Field.string(), done: Field.boolean() },
{ id: 'todo-1' },
);
doc.title = 'Ship v1';
doc.done = false;Or import existing yjs data:
import { fromYjsJSON } from '@groundstate/compat';
const doc = fromYjsJSON(yjsExport, TodoSchema);// Before (Automerge)
import * as Automerge from '@automerge/automerge';
let doc = Automerge.init();
doc = Automerge.change(doc, (d) => {
d.title = 'Ship v1';
d.done = false;
});
// After (groundstate)
import { Doc, Field } from '@groundstate/crdt';
const doc = Doc.create(
{ title: Field.string(), done: Field.boolean() },
{ id: 'todo-1' },
);
doc.title = 'Ship v1';
doc.done = false;Or import existing Automerge data:
import { fromAutomergeJSON } from '@groundstate/compat';
const doc = fromAutomergeJSON(automergeExport, TodoSchema);┌─────────────────────────────────────────────────────────┐
│ Your Application │
├──────────┬──────────┬───────────────────────────────────┤
│ react │ vue │ svelte (framework layer) │
├──────────┴──────────┴───────────────────────────────────┤
│ resolve (conflict resolution) │
├─────────────────────┬───────────────────────────────────┤
│ sync │ store (I/O layer) │
│ ┌───────────────┐ │ ┌─────────────────────────────┐ │
│ │ WebSocket │ │ │ IndexedDB SQLite FS Mem │ │
│ │ WebRTC │ │ │ Encryption Migrations │ │
│ │ HTTP BLE FS │ │ │ Queries Compaction │ │
│ └───────────────┘ │ └─────────────────────────────┘ │
├─────────────────────┴───────────────────────────────────┤
│ crdt (core data layer) │
│ Schema · LWW Register · PN-Counter · RGA List │
│ LWW Map · PeriText · HLC · Binary Encoding │
├─────────────────────────────────────────────────────────┤
│ devtools │ server / cloudflare │ compat │
│ Inspector │ Relay server │ yjs import │
│ Metrics │ Durable Objects │ AM import │
│ Simulator │ SQLite database │ │
└──────────────────┴───────────────────────┴───────────────┘
import { groundstateServer } from '@groundstate/server';
const server = groundstateServer({ port: 3000, dbPath: './data.db' });
server.listen();import { GroundstateDurableObject } from '@groundstate/cloudflare';
export class SyncDO extends GroundstateDurableObject {
// Runs on Cloudflare's edge, one instance per document
}# Clone and install
git clone https://github.com/nicholasgriffintn/groundstate.git
cd groundstate
pnpm install
# Build all packages
pnpm build
# Run tests
pnpm test
# Typecheck
pnpm lintThis is a monorepo managed with pnpm workspaces and Nx. Each package lives in packages/ and can be built independently.
Note: Published packages work with any package manager (npm, yarn, pnpm, bun). Local development of the monorepo uses pnpm.
MIT