v0.0.1 — local-first sync for TypeScript

The sync engine that drops into your stack

Bidirectional sync between Postgres and any client. Offline-first. One package. Works with your existing Next.js + Drizzle/Prisma + tRPC.

pnpm add bettersync

Everything you need

🔌

Drop-in

Works with Next.js, Express, NestJS, Hono, Elysia, Fastify, Bun. No rewrite.

📦

One package

pnpm add bettersync. 9 subpath exports. Everything included.

🐘

Postgres everywhere

PGlite in browser, Postgres on server. Same SQL dialect, same adapter pattern.

📡

Offline-first

Writes go to local store first. Sync happens in background when online.

⏱️

HLC conflict resolution

Hybrid Logical Clock + Last-Write-Wins. Deterministic across all clients.

🔒

Multi-tenant scope

Per-model scope functions. Client A never sees client B data. Enforced server-side.

⚛️

React hooks

SyncProvider, useSync, useSyncQuery with live mode. SyncDevtools built in.

🛠️

CLI tooling

npx bettersync init scaffolds your project. generate creates SQL migrations.

205+ tests

19-test conformance suite. Drizzle + PGlite adapters tested against real Postgres.

3 files. That's it.

lib/sync.ts
import { betterSync } from 'bettersync'import { drizzleAdapter }  from 'bettersync/adapters/drizzle'export const sync = betterSync({  database: drizzleAdapter(db, {    schema: { project: projects },  }),  models: {    project: {      fields: { id, userId, title, changed },      scope: ctx => ({ userId: ctx.userId }),    },  },})
app/api/sync/route.ts
import { sync } from '@/lib/sync'export const POST = sync.handler
lib/sync-client.ts
import { createSyncClient }  from 'bettersync/client'import { pgliteAdapter }  from 'bettersync/adapters/pglite'export const syncClient = createSyncClient({  database: pgliteAdapter(    new PGlite('idb://app'),  ),  schema: syncSchema,  syncUrl: '/api/sync',})

How it compares

bettersyncElectricSQLZeroPowerSync
Drop into existing stack✅ Yes❌ TanStack Start❌ Own data layer⚠️ Managed service
Extra infrastructure✅ NoneHTTP/2 + Caddyzero-cachePowerSync service
Install✅ 1 packageScaffolded app4+ packages3+ packages
Local DBPGlite / SQLitePGliteCustom storeSQLite
Conflict resolutionHLC LWWCRDT shapesServer authorityCustom
LicenseApache 2.0Apache 2.0MITApache 2.0

Works with your framework

One handler. Standard Web API. Every framework supported.

Next.jsHonoElysiaExpressFastifyNestJSBunNode.js

One package. Many exports.

bettersync

Core + betterSync() + server + client

bettersync/client

Local-first sync client engine

bettersync/server

Handler + hooks

bettersync/next-js

Next.js App Router handler

bettersync/node

Express / Fastify / NestJS adapter

bettersync/react

SyncProvider, useSync, useSyncQuery, SyncDevtools

bettersync/adapters/drizzle

Drizzle + Postgres (better-auth style)

bettersync/adapters/prisma

Prisma + Postgres

bettersync/adapters/kysely

Kysely + Postgres

bettersync/adapters/pg

Raw node-postgres

bettersync/adapters/pglite

PGlite (Postgres WASM in browser)

bettersync/adapters/better-sqlite3

SQLite for Node.js / Electron

bettersync/test

19-test conformance suite

Set up with AI

Copy this prompt into Claude, Cursor, or Copilot. It will analyze your project, ask what to sync, and generate all the code.

Preview prompt (4k chars)
You are integrating bettersync into an existing project. bettersync is a local-first bidirectional sync engine for TypeScript. It syncs data between a server (Postgres) and clients (browser via PGlite, Node via SQLite).

Documentation: https://bettersync.vercel.app/docs
npm: https://www.npmjs.com/package/bettersync
GitHub: https://github.com/beautyfree/bettersync

## Your task

1. **Analyze the project** — detect the framework (Next.js, NestJS, Hono, Express, Fastify, Elysia, Bun), package manager, ORM (Drizzle, Prisma, or none), existing database, auth solution, and directory structure (src/ or not, App Router vs Pages Router for Next.js).

2. **Ask the user:**
   - Which models/tables should be synced? (e.g. "projects and tasks")
   - What is the scope field for multi-tenancy? (e.g. "userId" — each user sees only their data)
   - Should the client use PGlite (browser Postgres WASM) or memory adapter (for prototyping)?
   - What auth method is used? (JWT, session, Clerk, better-auth, etc.)

3. **Install bettersync:**
   ```bash
   pnpm add bettersync
   # If using PGlite for browser client:
   pnpm add @electric-sql/pglite
   ```

4. **Create the sync config file** (e.g. `lib/sync.ts` or `src/lib/sync.ts`):
   ```ts
   import { betterSync } from 'bettersync'
   // Import your adapter (drizzleAdapter for Drizzle, memoryAdapter for prototyping)

   export const sync = betterSync({
     database: yourAdapter,
     models: {
       // Define each model the user wants to sync with:
       // fields (id, userId, title, changed — HLC field required)
       // scope function for multi-tenancy
     },
     auth: async (req) => {
       // Extract userId from the request using the project's auth solution
     },
   })
   export const syncSchema = sync.schema
   ```

5. **Mount the sync handler:**
   - Next.js App Router: `app/api/sync/route.ts` → `export const POST = sync.handler`
   - Next.js Pages Router: `pages/api/sync.ts` → use `parseSyncRequest` + `handleSync`
   - Express/NestJS/Fastify: `import { toNodeHandler } from 'bettersync/node'` → `app.post('/api/sync', toNodeHandler(sync))`
   - Hono: `app.post('/sync', (c) => sync.handler(c.req.raw))`
   - Elysia: `app.mount('/sync', sync.handler)`

6. **Create the client config** (e.g. `lib/sync-client.ts`):
   ```ts
   import { createSyncClient } from 'bettersync/client'
   import { pgliteAdapter } from 'bettersync/adapters/pglite'
   import { PGlite } from '@electric-sql/pglite'
   import { syncSchema } from './sync'

   export const syncClient = createSyncClient({
     database: pgliteAdapter(new PGlite('idb://app-name')),
     schema: syncSchema,
     syncUrl: '/api/sync',   // or full URL for cross-origin
     headers: () => ({       // if auth requires headers
       Authorization: \`Bearer \${getToken()}\`,
     }),
   })
   ```

7. **Generate database migration:**
   ```bash
   npx @bettersync/cli generate --config lib/sync.ts
   # For existing tables:
   npx @bettersync/cli generate --config lib/sync.ts --alter --backfill
   ```

8. **If React** — wrap the app with SyncProvider:
   ```tsx
   import { SyncProvider, SyncDevtools } from 'bettersync/react'
   // Use dynamic import for PGlite to avoid SSR issues in Next.js
   // Use useSyncQuery((s) => s.model('todo').findMany(), [], { live: true }) for reactive queries
   ```

## Important details

- The `changed` field (HLC) MUST be declared in every model's fields. It's how sync tracks versions.
- The Drizzle adapter accepts Drizzle table objects directly: `drizzleAdapter(db, { schema: { project: projectsTable } })`. Column mapping is automatic.
- PGlite in Next.js MUST be dynamically imported (client-only) to avoid SSR crashes.
- `sync.handler` is a standard Web API `(req: Request) => Promise<Response>` handler.
- `toNodeHandler(sync)` converts it for Express/Fastify/NestJS.
- For live reactive queries use `useSyncQuery(fn, deps, { live: true })` — auto-refetches on every local write and sync.
- `sync.on('change', callback)` for event-driven updates outside React.

## After setup

Tell the user to open the app in two browser tabs, make a change in one tab, and verify it appears in the other. That's the magical moment.

Ready to sync?

From zero to two-tab live sync in under 2 minutes.