An experimental, local-first sync engine for SvelteKit with optimistic updates, real-time synchronization, and support for any database.
npm install sveltekit-sync # or your favorite package manager
// 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()
});
// 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'
}
}
};
// 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);
}
);
// 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');
<!-- 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>
<!-- 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>
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 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)
Built-in strategies for handling conflicts:
client-wins - Client changes always winserver-wins - Server changes always winlast-write-wins - Most recent change wins (default)manual - Custom resolution logicexport const syncEngine = new SyncEngine({
conflictResolution: 'last-write-wins',
onConflict: (conflict) => {
console.log('Conflict detected:', conflict);
}
});
sveltekit-sync/adapters/drizzleconst active = await todosStore
.query()
.where('completed', false)
.orderBy('createdAt', 'desc')
.limit(10)
.get();
const projectsStore = syncEngine.collection('projects', {
relations: {
tasks: { type: 'hasMany', collection: 'tasks', key: 'projectId' }
}
});
const project = await projectsStore.withRelations(['tasks']).findOne(id);
todosStore.before('create', (data) => ({
...data,
createdBy: currentUser.id
}));
todosStore.after('update', (data) => {
analytics.track('todo_updated', data);
});
await todosStore.batch()
.create({ text: 'Task 1' })
.create({ text: 'Task 2' })
.update(id, { completed: true })
.commit();
const unsubscribe = todosStore.subscribe((todos) => {
console.log('Todos updated:', todos);
});
Control what each user can access:
export const syncSchema = {
tables: {
todos: {
where: (userId: string) => sql`user_id = ${userId}`
}
}
};
Remove sensitive fields before syncing:
export const syncSchema = {
tables: {
users: {
transform: (user) => {
const { password, internalNotes, ...safe } = user;
return safe;
}
}
}
};
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
Full API documentation available at sveltekit-sync.mudiageo.me
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
MIT ยฉ Mudiaga Arharhire