Ferry
Examples

Express

Express runs on Node's http API, so it takes toNodeHandler(syncServer): a handler that turns the Node request into a web Request for the server, and writes the answer back. It uses the schema and server of the Quick start.

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

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

const app = express();

// Before any global body parser, or with its limit raised: see below.
app.use('/api/sync', toNodeHandler(syncServer));

app.listen(3000);

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

The same handler mounts on Fastify, NestJS and plain node:http: see Node.js.

Auth

auth gets a web Request with the original headers and cookies, so most auth libraries read the session from it as anywhere else: see Auth.

When the user only exists on the Node request (Passport's req.user, a session middleware's req.session), a middleware in front of the handler can put it in a header for auth. It must always remove the header the client sent:

server.tsServer
import express from 'express';
import { createSyncServer, memoryStore, toNodeHandler } from 'ferrysync/server';

import { schema } from './schema';

const syncServer = createSyncServer({
  schema,
  store: memoryStore(),
  auth: (request) => request.headers.get('x-sync-user'),
});

const app = express();

app.use(
  '/api/sync',
  (req, _res, next) => {
    // Never trust the client's own: it would let anyone be anyone.
    delete req.headers['x-sync-user'];
    const userId = userIdOf(req);
    if (userId !== null) {
      req.headers['x-sync-user'] = userId;
    }
    next();
  },
  toNodeHandler(syncServer),
);

Gotchas

  • Body parsers are fine, with a limit that fits. When express.json() already read the body, the handler uses req.body. But it stops at 100 kB by default, and a device that was offline for a while pushes more: mount the sync route before it, or raise its limit.
  • CORS. For a web app on another origin, put your CORS middleware before the route. Native apps send no preflight requests.
  • app.use('/api/sync', …), not app.post('/api/sync', …). The endpoints are paths under the mount: /api/sync/push, /pull, /bootstrap and /wait.
  • Errors go to next, as with any middleware, in the rare case the handler itself fails (a runtime without a global Request). Failures of the sync server are its own 500 answers.
  • Needs Node 18 or later, for the global Request.
  • Realtime and timeouts: see Node.js.

On this page