Skip to content

SmashJaw/groundstate

Repository files navigation

@groundstate

Local-first data toolkit for TypeScript.

npm License: MIT TypeScript Build

[SmashJaw

Docs coming soon


What is Groundstate?

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.

Features

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

Packages

Package Description Version
@groundstate/crdt Schema-defined CRDTs with TypeScript inference npm
@groundstate/sync Pluggable sync engine with 5 transports npm
@groundstate/store Offline-first storage layer npm
@groundstate/resolve Conflict detection and resolution npm
@groundstate/react React hooks and components npm
@groundstate/vue Vue 3 composables npm
@groundstate/svelte Svelte stores npm
@groundstate/devtools Inspector, metrics, and network simulator npm
@groundstate/server SQLite-backed relay server npm
@groundstate/cloudflare Cloudflare Durable Objects adapter npm
@groundstate/compat yjs and Automerge compatibility npm
groundstate All-in-one umbrella package npm

Quick Start

# 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/store
import { 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);

React Quick Start

# npm
npm install @groundstate/crdt @groundstate/react

# yarn
yarn add @groundstate/crdt @groundstate/react

# pnpm
pnpm add @groundstate/crdt @groundstate/react
import { 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>
  );
}

Schema Types

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

Sync Transports

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

Presence

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

Authentication and ACL

import { ACLManager } from '@groundstate/sync';

const acl = new ACLManager({
  rules: [
    { role: 'editor', path: '*', permission: 'write' },
    { role: 'viewer', path: '*', permission: 'read' },
  ],
});

Storage Adapters

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

Migrations

import { MigrationEngine } from '@groundstate/store';

const migrations = new MigrationEngine([
  {
    version: 2,
    up: (doc) => {
      doc.priority = doc.priority ?? 'medium';
    },
  },
]);
await migrations.run(store);

Queries

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

Conflict Resolution

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

Migration from yjs

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

Migration from Automerge

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

Architecture

┌─────────────────────────────────────────────────────────┐
│                    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      │               │
└──────────────────┴───────────────────────┴───────────────┘

Deploying

Relay Server (Node)

import { groundstateServer } from '@groundstate/server';

const server = groundstateServer({ port: 3000, dbPath: './data.db' });
server.listen();

Cloudflare Durable Objects

import { GroundstateDurableObject } from '@groundstate/cloudflare';

export class SyncDO extends GroundstateDurableObject {
  // Runs on Cloudflare's edge, one instance per document
}

Contributing

# 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 lint

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

License

MIT

About

Groundstate npm package

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors