Skip to main content

On This Page

Mastering Offline Sync: CRDTs and Local-First Architecture in smallstack

2 min read
Share

These articles are AI-generated summaries. Please check the original sources for full details.

CRDTs and Local-First Architecture: How smallstack Handles Offline Conflict Resolution

smallstack utilizes a local-first architecture where the local device serves as the primary data store rather than a cache. This system employs Conflict-free Replicated Data Types (CRDTs) to ensure concurrent modifications merge consistently without coordination or locks.

Why This Matters

Traditional “offline mode” often relies on write queues that cause silent data loss via “last writer wins” or force users to stop working during network outages. In contrast, a local-first model removes perceived latency and loading spinners by treating the server as a replication peer, which is critical for field teams who may work disconnected for hours. Implementing this requires solving the complex problem of merging divergent states without a central authority, moving beyond simple timestamps to commutative and associative operations.

Key Insights

  • Conflict-free Replicated Data Types (CRDTs) ensure consistency by storing operations or state vectors that are commutative and associative.
  • smallstack utilizes Last-Write-Wins (LWW) semantics for scalar fields using logical timestamps rather than wall-clock time to resolve concurrent updates.
  • SignalDB, a reactive local-first database built with TypeScript, enables incremental sync by fetching only document deltas using ‘updatedAt’ timestamps.
  • Reactive UI integration is achieved via Svelte 5 runes, where SignalDB collections drive local $state updates automatically upon sync.
  • The system supports real-time peer replication using Server-Sent Events (SSE) to broadcast local commits to connected clients asynchronously.

Working Examples

SignalDB incremental sync protocol using pull/push deltas.

collection.sync({
  pull: async (lastPulledAt) => {
    const changes = await fetch(`/api/orders?updatedAfter=${lastPulledAt}`);
    return changes.json(); // Only the delta
  },
  push: async (changes) => {
    await fetch('/api/orders', {
      method: 'POST',
      body: JSON.stringify(changes)
    });
  }
});

Reactive binding using Svelte 5 runes and SignalDB.

class OrderService {
  orders = $state<Order[]>([]);
  constructor() {
    // SignalDB collection drives $state reactively
    collection.find().onChange((result) => {
      this.orders = result;
    });
  }
}

Practical Applications

  • Field technician work orders: Multiple users modify the same record offline; CRDTs merge independent notes and status updates without manual intervention. Pitfall: Using simple write queues without conflict handling, which leads to the ‘last writer wins’ data destruction.
  • No-code business platforms: Non-technical users define custom schemas that must be available locally for offline validation. Pitfall: Relying on Server-Side Rendering (SSR) for user data, which makes the application unusable without an active connection.

References:

Continue reading

Next article

Solving Production Cron Failures with Open Source CronManager

Related Content