sveltekit-sync Svelte Themes

Sveltekit Sync

Local-first sync-engine library for SvelteKit

๐Ÿ”„ SvelteKit Sync

An experimental, local-first sync engine for SvelteKit with optimistic updates, real-time synchronization, and support for any database.

โœจ Features

  • ๐Ÿš€ Instant UI Updates - Optimistic updates for zero-latency UX
  • ๐Ÿ”„ Real-time Sync - Changes appear instantly across all devices
  • ๐Ÿ“ก Offline-First - Works seamlessly without internet connection
  • ๐Ÿ—„๏ธ Database Agnostic - Works with any client DB (IndexedDB, SQLite, PGlite) and server DB (Postgres, MongoDB, MySQL, etc.)
  • โšก Powered by Remote Functions - Uses SvelteKit's new Remote Functions API
  • ๐ŸŽฏ Type-Safe - Full TypeScript support with excellent IntelliSense
  • ๐Ÿ” Secure - Built-in row-level security and data filtering
  • ๐ŸŽจ Ergonomic API - Simple, intuitive developer experience
  • ๐Ÿ”€ Conflict Resolution - Multiple strategies for handling conflicts
  • ๐Ÿ“ฆ Modular - Install only what you need

๐Ÿ“ฆ Installation


npm install sveltekit-sync # or your favorite package manager

๐Ÿš€ Quick Start

1. Set Up Database Schema

// src/lib/server/database/schema.ts
import { pgTable, text, boolean, timestamp, integer, uuid } from 'drizzle-orm/pg-core';

// All synced tables must include these columns
export const syncMetadata = {
  _version: integer('_version').notNull().default(1),
  _updatedAt: timestamp('_updated_at').notNull().defaultNow(),
  _clientId: text('_client_id'),
  _isDeleted: boolean('_is_deleted').default(false)
};

export const todos = pgTable('todos', {
  id: uuid('id').primaryKey().defaultRandom(),
  userId: text('user_id').notNull(),
  text: text('text').notNull(),
  completed: boolean('completed').default(false),
  ...syncMetadata
});

// Sync log table - tracks all changes for efficient delta sync
export const syncLog = pgTable('sync_log', {
  id: uuid('id').primaryKey().defaultRandom(),
  tableName: text('table_name').notNull(),
  recordId: text('record_id').notNull(),
  operation: text('operation').notNull(), // 'insert', 'update', 'delete'
  data: jsonb('data'),
  timestamp: timestamp('timestamp').notNull().defaultNow(),
  clientId: text('client_id'),
  userId: text('user_id').notNull()
});

// Client state table - track last sync for each client
export const clientState = pgTable('client_state', {
  clientId: text('client_id').primaryKey(),
  userId: text('user_id').notNull(),
  lastSync: timestamp('last_sync').notNull().defaultNow(),
  lastActive: timestamp('last_active').notNull().defaultNow()
});

2. Configure Server Sync

// src/lib/server/sync-schema.ts
import { sql } from 'drizzle-orm';

export const syncSchema = {
  tables: {
    todos: {
      table: 'todos',
      columns: ['id', 'text', 'completed', 'userId', '_version', '_updatedAt'],
      // Row-level security - only sync user's own data
      where: (userId: string) => sql`user_id = ${userId}`,
      conflictResolution: 'last-write-wins'
    }
  }
};

3. Create Remote Functions

// src/lib/sync.remote.ts
import { query, command } from '$app/server';
import * as v from 'valibot';
import { ServerSyncEngine } from '$lib/server/sync-engine';
import { getUser } from '$lib/server/auth';

const syncEngine = new ServerSyncEngine();

export const pushChanges = command(
  v.array(SyncOperationSchema),
  async (operations, { request }) => {
    const user = await getUser(request);
    return await syncEngine.push(operations, user.id);
  }
);

export const pullChanges = query(
  v.object({ lastSync: v.number(), clientId: v.string() }),
  async ({ lastSync, clientId }, { request }) => {
    const user = await getUser(request);
    return await syncEngine.pull(lastSync, clientId, user.id);
  }
);

4. Initialize Client

// src/lib/db.ts
import { SyncEngine, IndexedDBAdapter } from 'sveltekit-sync';
import { pushChanges, pullChanges } from '$lib/sync.remote';

const adapter = new IndexedDBAdapter('myapp-db', 1);

export const syncEngine = new SyncEngine({
  local: { db: null, adapter },
  remote: { 
    push: data => pushChanges(data),
    pull: (lastSync: number, clientId: string) => pullChanges({ lastSync, clientId }) 
  },
  syncInterval: 30000, // Sync every 30 seconds
  conflictResolution: 'last-write-wins'
});

export async function initDB() {
  await adapter.init({ todos: 'id', notes: 'id' });
  await syncEngine.init();
}

// Create typed collection stores
export const todosStore = syncEngine.collection('todos');

5. Initialize in Root Layout

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { initDB, syncEngine } from '$lib/db';
  import { browser } from '$app/environment';

  onMount(async () => {
    if (browser) {
      await initDB();
    }
    return () => syncEngine.destroy();
  });

  const syncState = $derived(syncEngine.state);
</script>

<div class="app">
  {#if syncState.isSyncing}
    <div class="sync-indicator">Syncing...</div>
  {/if}
  <slot />
</div>

6. Use in Components

<!-- src/routes/todos/+page.svelte -->
<script lang="ts">
  import { onMount } from 'svelte';
  import { todosStore } from '$lib/db';

  let newTodo = $state('');

  onMount(() => todosStore.load());

  async function addTodo() {
    await todosStore.create({
      text: newTodo,
      completed: false,
      createdAt: Date.now()
    });
    newTodo = '';
  }

  async function toggleTodo(id: string) {
    const todo = todosStore.find(t => t.id === id);
    if (todo) {
      await todosStore.update(id, { completed: !todo.completed });
    }
  }
</script>

<input bind:value={newTodo} on:keydown={(e) => e.key === 'Enter' && addTodo()} />

<ul>
  {#each todosStore.data as todo (todo.id)}
    <li>
      <input 
        type="checkbox" 
        checked={todo.completed}
        onchange={() => toggleTodo(todo.id)}
      />
      {todo.text}
      <button onclick={() => todosStore.delete(todo.id)}>Delete</button>
    </li>
  {/each}
</ul>

๐Ÿ“š Core Concepts

Optimistic Updates

All CRUD operations apply changes immediately to the local database and UI, then sync in the background:

await todosStore.create({ text: 'Buy milk' }); 
// โœ… UI updates instantly
// ๐Ÿ”„ Syncs to server in background

Collection Stores

Collection stores provide a reactive, ergonomic API:

const todosStore = syncEngine.collection('todos');

// Reactive state
todosStore.data         // Current data array
todosStore.isLoading    // Loading state
todosStore.error        // Error state
todosStore.count        // Item count
todosStore.isEmpty      // Empty check

// CRUD operations
await todosStore.create(data)
await todosStore.update(id, data)
await todosStore.delete(id)
await todosStore.findOne(id)

// Utility methods
todosStore.find(predicate)
todosStore.filter(predicate)
todosStore.sort(compareFn)

Conflict Resolution

Built-in strategies for handling conflicts:

  • client-wins - Client changes always win
  • server-wins - Server changes always win
  • last-write-wins - Most recent change wins (default)
  • manual - Custom resolution logic
export const syncEngine = new SyncEngine({
  conflictResolution: 'last-write-wins',
  onConflict: (conflict) => {
    console.log('Conflict detected:', conflict);
  }
});

๐Ÿ—„๏ธ Database Adapters

Client Adapters

  • IndexedDB (built-in) - Browser storage
  • SQLite - Coming soon
  • PGlite - Coming soon

Server Adapters

  • Drizzle ORM - sveltekit-sync/adapters/drizzle
  • Prisma - Coming soon
  • Postgres - Coming soon
  • MongoDB - Coming soon

๐ŸŽฏ Advanced Features (WIP/To be Implemented)

Query Builder

const active = await todosStore
  .query()
  .where('completed', false)
  .orderBy('createdAt', 'desc')
  .limit(10)
  .get();

Relationships

const projectsStore = syncEngine.collection('projects', {
  relations: {
    tasks: { type: 'hasMany', collection: 'tasks', key: 'projectId' }
  }
});

const project = await projectsStore.withRelations(['tasks']).findOne(id);

Middleware/Hooks

todosStore.before('create', (data) => ({
  ...data,
  createdBy: currentUser.id
}));

todosStore.after('update', (data) => {
  analytics.track('todo_updated', data);
});

Batch Operations

await todosStore.batch()
  .create({ text: 'Task 1' })
  .create({ text: 'Task 2' })
  .update(id, { completed: true })
  .commit();

Real-time Subscriptions

const unsubscribe = todosStore.subscribe((todos) => {
  console.log('Todos updated:', todos);
});

๐Ÿ” Security

Row-Level Security

Control what each user can access:

export const syncSchema = {
  tables: {
    todos: {
      where: (userId: string) => sql`user_id = ${userId}`
    }
  }
};

Data Transformation

Remove sensitive fields before syncing:

export const syncSchema = {
  tables: {
    users: {
      transform: (user) => {
        const { password, internalNotes, ...safe } = user;
        return safe;
      }
    }
  }
};

๐Ÿ“Š Performance

  • Delta Sync - Only changed records are synced
  • Batch Operations - Multiple changes sent in single request
  • Intelligent Caching - Frequently accessed data cached in memory
  • Connection Pooling - Efficient resource usage
  • Compression - Automatic payload compression

๐Ÿงช Testing

npm test                 # Run all tests
npm run test:unit        # Unit tests
npm run test:integration # Integration tests
npm run test:e2e         # End-to-end tests

๐Ÿ“– API Reference

Full API documentation available at sveltekit-sync.mudiageo.me

๐Ÿค Contributing

We welcome contributions! Please see CONTRIBUTING.md for guidelines.

๐Ÿ“„ License

MIT ยฉ Mudiaga Arharhire

๐Ÿ™ Acknowledgments

  • Built with SvelteKit
  • Inspired by LiveStore and other prior sync and local-first libraries

Top categories

Loading Svelte Themes