Ferry
Examples

Hono

Hono passes web Requests around, so the sync route is one line, on every runtime Hono runs on. It uses the schema and server of the Quick start.

server.tsServer
import { serve } from '@hono/node-server';
import { Hono } from 'hono';

import { syncServer } from './sync-server';

const app = new Hono();

app.all('/api/sync/*', (c) => syncServer.fetch(c.req.raw));

serve({ fetch: app.fetch, port: 3000 });

The client's url is the prefix: http({ url: 'https://api.example.com/api/sync' }).

Other runtimes

RuntimeInstead of serve(…)
Bunexport default { fetch: app.fetch, idleTimeout: 40 }: see Bun
Cloudflare Workersexport default app: see Cloudflare Workers
DenoDeno.serve(app.fetch)

Auth from a middleware

auth gets the raw request, so it can read the session cookie or token itself: see Auth. When your app already authenticates in a Hono middleware, hand the user to auth through the request object:

server.tsServer
import { createSyncServer, memoryStore } from 'ferrysync/server';
import { Hono } from 'hono';

import { schema } from './schema';

/** The user each request was authenticated as, by the middleware. */
const users = new WeakMap<Request, string>();

const syncServer = createSyncServer({
  schema,
  store: memoryStore(),
  auth: (request) => users.get(request) ?? null,
});

const app = new Hono<{ Variables: { userId: string | null } }>();

app.use('*', async (c, next) => {
  c.set('userId', (await getSession(c.req.raw))?.userId ?? null);
  await next();
});

app.all('/api/sync/*', (c) => {
  const userId = c.get('userId');
  if (userId !== null) {
    users.set(c.req.raw, userId);
  }
  return syncServer.fetch(c.req.raw);
});

CORS

When the web app runs on another origin, answer its preflight requests before the sync route. fetch answers only POSTs. Native apps send no preflight requests.

server.tsServer
import { Hono } from 'hono';
import { cors } from 'hono/cors';

import { syncServer } from './sync-server';

export const app = new Hono();

app.use(
  '/api/sync/*',
  cors({ origin: 'https://app.example.com', allowHeaders: ['authorization', 'content-type'] }),
);
app.all('/api/sync/*', (c) => syncServer.fetch(c.req.raw));

Gotchas

  • Mount with a wildcard. The endpoints are …/push, …/pull, …/bootstrap and …/wait.
  • Do not read the body first. fetch reads it (up to maxBodyBytes). A middleware that consumed c.req.raw's body leaves nothing to read: use handle with the parsed body then.
  • Mind the idle timeout. A realtime wait is held up to 30 seconds with nothing sent. Bun closes idle requests after 10 by default: raise its idleTimeout.

On this page