Grove

CRDT Overview

Conflict-Free Replicated Data Types for offline-first, multi-node, and eventually-consistent applications with Grove.

Grove includes an optional CRDT (Conflict-Free Replicated Data Type) layer that enables offline-first, multi-node, and eventually-consistent use cases. It tracks field-level changes with Hybrid Logical Clocks and merges them automatically during sync.

Why CRDTs?

Traditional databases rely on a single source of truth. When multiple nodes make concurrent edits, you need conflict resolution. CRDTs solve this mathematically: any two nodes that have seen the same set of updates will converge to the same state, regardless of the order updates arrive.

Grove CRDTs are useful for:

  • Offline-first apps that queue changes locally and sync when connectivity returns
  • Multi-node deployments where edge servers process writes independently
  • Real-time collaboration where multiple users edit the same document
  • Event sourcing with guaranteed convergence across replicas

Five CRDT Types

Grove supports five field-level CRDT types. Tag struct fields to opt in:

TypeTagMerge StrategyUse Case
LWW-Registercrdt:lwwHighest HLC timestamp winsStrings, booleans, JSON objects
PN-Countercrdt:counterPer-node max of increments/decrementsView counts, scores, balances
OR-Setcrdt:setAdd-wins union with observed-removeTags, categories, member lists
RGA Listcrdt:listUnion all nodes, DFS traversal sorted by HLCOrdered collections, task lists, kanban boards
Nested Documentcrdt:documentPer-path field merge via MergeEngineJSON-like nested structures, forms, user profiles

Fields without crdt: tags work normally with zero overhead.

How It Works

  Model (struct tags)
       |
  Plugin (Grove hook)
       |
  Shadow Table (_<table>_crdt)
       |
  HLC Clock (Hybrid Logical Clock)
       |
  Syncer (pull/push protocol)
       |
  Transport (HTTP / SSE / WebSocket / custom)
       |
  Rooms / Presence ── Remote Node
       |
  Time-Travel (optional history)
  1. Struct tags declare which fields are CRDT-enabled and their type
  2. Plugin intercepts mutations (insert/update/delete) via Grove hooks and records changes in shadow tables
  3. Shadow tables store per-field CRDT state alongside the primary table without modifying its schema
  4. HLC clock generates causally-ordered timestamps without coordination between nodes
  5. Syncer orchestrates two-phase sync: pull remote changes and merge, then push local changes
  6. Transport handles the network layer (HTTP, SSE streaming, WebSocket, or custom protocols)
  7. Rooms provide scoped collaborative spaces with participant management, cursor tracking, and typing indicators
  8. Time-Travel optionally records field-level history for audit trails, undo, and debugging

Architecture

grove/crdt/
  crdt.go            Core types: CRDTType, State, FieldState, ChangeRecord
  clock.go           HLC implementation
  register.go        LWW-Register merge
  counter.go         PN-Counter merge
  set.go             OR-Set merge (add-wins)
  list.go            RGA List merge (ordered collections)
  document.go        Nested Document merge (per-path heterogeneous)
  merge.go           MergeEngine: field + state merging
  plugin.go          Plugin (Grove hook integration)
  plugin_hooks.go    CRDTPlugin interface + interceptors
  hooks.go           PostMutation/PreQuery hooks
  metadata.go        Shadow table read/write
  sync.go            Syncer: push/pull orchestration + StreamSync
  sync_hooks.go      SyncHook interface for intercepting sync data
  transport.go       Transport interface, HTTPClient, StreamingTransport
  transport_ws.go    WebSocket transport (bidirectional multiplexed)
  server.go          SyncController + HTTPHandler (standalone)
  presence.go        PresenceManager: in-memory ephemeral state with TTL
  presence_types.go  Presence types: PresenceState, PresenceUpdate, PresenceEvent
  room.go            RoomManager: room CRUD, join/leave, participant limits
  timetravel.go      Time-travel: field history, state-at-time queries
  validation.go      Schema + field validation rules
  metrics.go         Sync/merge/presence metric collectors
  options.go         Functional options
  inspect.go         Debug/inspect utilities
  migrations.go      Shadow table DDL

Key Concepts

  • HLC (Hybrid Logical Clock) combines wall-clock time with a logical counter and node ID for total ordering without coordination
  • Shadow tables (_<table>_crdt) store CRDT metadata alongside primary data; your primary table schema is never modified
  • Tombstones mark deleted records so deletions propagate during sync; they expire after a configurable TTL (default: 7 days)
  • Sync hooks intercept changes flowing between nodes for validation, filtering, transformation, or audit logging
  • Transport is pluggable: use HTTP, SSE streaming, WebSocket, or implement your own for gRPC, NATS, etc.
  • Presence provides ephemeral, real-time awareness (typing indicators, cursors, online users) with TTL-based cleanup, SSE broadcast, and React hooks
  • Rooms provide scoped collaborative spaces with participant limits, cursor tracking, typing indicators, and lifecycle hooks
  • Time-Travel optionally records field-level history for querying state at any point in time, audit trails, and undo
  • WebSocket transport enables bidirectional multiplexed sync over a single persistent connection
  • Plugins allow intercepting merge, metadata, presence, room, time-travel, and connection events for custom logic
  • Validation enforces schema and field-level rules before merge operations
  • Metrics collect sync, merge, and presence statistics for observability

Quick Example

import (
    "github.com/xraph/grove/crdt"
    "github.com/xraph/grove/hook"
)

// 1. Define a CRDT-enabled model
type Document struct {
    grove.BaseModel `grove:"table:documents,alias:d"`
    ID        string   `grove:"id,pk"`
    Title     string   `grove:"title,crdt:lww"`
    ViewCount int64    `grove:"view_count,crdt:counter"`
    Tags      []string `grove:"tags,type:jsonb,crdt:set"`
}

// 2. Create plugin and attach to DB
plugin := crdt.New(crdt.WithNodeID("node-1"))
db.Hooks().AddHook(plugin, hook.Scope{Tables: []string{"documents"}})

// 3. Sync with a remote node
syncer := crdt.NewSyncer(plugin,
    crdt.WithTransport(crdt.HTTPTransport("https://cloud.example.com/sync")),
    crdt.WithSyncTables("documents"),
)
report, err := syncer.Sync(ctx)

Next Steps

On this page