Ferry
Examples

Next.js

The browser keeps the data, and one route handler of the same Next.js app is the server. It uses the schema of the Quick start.

The route

One dynamic segment catches the four endpoints (push, pull, bootstrap and wait):

app/api/sync/[endpoint]/route.tsServer
import { syncServer } from '@/lib/sync-server';

export const POST = syncServer.fetch;
// A realtime wait is held up to 30 s: let the function run longer than that.
export const maxDuration = 60;

The server

On your machine a SQLite file is enough. On Vercel, every instance must reach the same database, so use Postgres (Neon, Supabase, Vercel's own), and hand work after the response to after:

lib/sync-server.tsServer
import { createSyncServer, nodeSqlite, sqliteStore } from 'ferrysync/server';
import { DatabaseSync } from 'node:sqlite';

import { schema } from './schema';

export const syncServer = createSyncServer({
  schema,
  store: sqliteStore(nodeSqlite(new DatabaseSync('sync.db'))),
  auth: async (request) => (await getSession(request))?.userId ?? null,
});

Auth

auth gets the route's Request. Read your session from it:

  • Better Auth: (await auth.api.getSession({ headers: request.headers }))?.user.id ?? null.
  • Auth.js: (await auth())?.user?.id ?? null, since auth() reads the request's cookies.
  • A bearer token: see Auth.

The client

The client points at the route, with no endpoint at the end:

lib/sync.tsClient
import { createSync, http, indexedDbStorage } from 'ferrysync';

import { schema } from './schema';

export const sync = createSync({
  schema,
  storage: indexedDbStorage('my-app'),
  backend: http({ url: '/api/sync' }),
});

Components that read it are client components ('use client'). On the server, SyncProvider renders its fallback; in the browser, the local data opens and shows, with no request in between.

Gotchas

  • The Node.js runtime, not Edge. The Postgres and SQLite drivers need Node's sockets and files. Route handlers run on Node.js by default; do not set runtime = 'edge' on this one.
  • Keep the server in its own module (lib/sync-server.ts), imported by the route. Next.js creates it once per instance; in development, editing that module creates it again, which is fine.
  • Several instances on Vercel each hold their own waits: a change wakes the devices waiting on the instance that applied it at once, and the others at their next look, within 15 seconds. To make it instant everywhere, pass a broadcast.
  • Do not put the route behind a middleware that reads the body. fetch reads it.
  • node:sqlite needs Node 22.13 or later. On an older Node, or on Vercel, use Postgres.

On this page