Ferry
Examples

Bun

Bun.serve passes web Requests, so the server's fetch is the handler, and bun:sqlite is a store with nothing to install. It uses the schema of the Quick start.

server.tsServer
import { Database } from 'bun:sqlite';
import { bunSqlite, createSyncServer, sqliteStore } from 'ferrysync/server';

import { schema } from './schema';

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

Bun.serve({
  port: 3000,
  // A realtime wait is held up to 30 s with nothing sent; Bun's default closes it after 10.
  idleTimeout: 40,
  fetch: (request) => syncServer.fetch(request),
});

Run it with bun server.ts, and point the client at http({ url: 'http://localhost:3000/sync' }): every path answers, by its last segment.

To route by path yourself, Bun.serve takes routes instead of fetch: routes: { '/sync/*': { POST: syncServer.fetch } }. With Hono, see Hono.

Gotchas

  • idleTimeout above 30. Without it, realtime waits are cut at 10 seconds: the client then sees a network error every 10 seconds, retries with backoff, and realtime turns slow. 40 covers the 30-second hold.
  • One process per SQLite file. The store takes SQLite's write lock for each write; another process on the same file waits for it, and needs a busy_timeout to wait at all. For several instances, use Postgres: postgresStore with nodePostgres or postgresJs, see Stores.
  • Keep the file on a persistent disk when deploying: see Deploy.

On this page