Ferry
Examples

Node.js

No framework at all: node:http, with toNodeHandler. It uses the schema of the Quick start. Frameworks on Node's http API take the same handler; Express has its own page, Fastify and Nest are below.

server.tsServer
import { createSyncServer, nodeSqlite, sqliteStore, toNodeHandler } from 'ferrysync/server';
import { createServer } from 'node:http';
import { DatabaseSync } from 'node:sqlite';

import { schema } from './schema';

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

// Every path answers: the endpoint is the last segment, so /sync/push and /push both work.
createServer(toNodeHandler(syncServer)).listen(3000);

Run it with npx tsx server.ts and point the client at http({ url: 'http://localhost:3000/sync' }).

Next to your own routes

When the same process answers other requests, hand only the sync path to the handler:

server.tsServer
import { toNodeHandler } from 'ferrysync/server';
import { createServer } from 'node:http';

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

const sync = toNodeHandler(syncServer);

createServer((request, response) => {
  if (request.url?.startsWith('/sync/')) {
    void sync(request, response);
    return;
  }
  response.writeHead(404).end();
}).listen(3000);

Fastify

Fastify parses bodies itself and sends its own replies. Hand the parsed body on, and take the reply over:

server.tsServer
import { toNodeHandler } from 'ferrysync/server';
import Fastify from 'fastify';

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

const fastify = Fastify();
const handler = toNodeHandler(syncServer);

fastify.post('/sync/:endpoint', async (request, reply) => {
  reply.hijack();
  await handler(Object.assign(request.raw, { body: request.body }), reply.raw);
});

await fastify.listen({ port: 3000 });

Fastify's bodyLimit is 1 MiB by default; raise it on this route if devices push more.

Nest

Nest runs on Express by default: mount the handler on the app, before listen.

Server
const app = await NestFactory.create(AppModule);
app.use('/sync', toNodeHandler(syncServer));
await app.listen(3000);

Nest's own body parser runs first, so the handler takes req.body. Guards and interceptors do not see this route: authenticate in auth.

Realtime and timeouts

A realtime wait stays open up to 30 seconds with nothing sent. Node's defaults allow it (requestTimeout is 5 minutes). When the client goes away, the handler ends its held request.

Proxies in front of Node (nginx, a load balancer) must allow it too: nginx's proxy_read_timeout defaults to 60 seconds, which is enough; some load balancers default to 30, which is not. Set theirs to 60.

On this page