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.
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:
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:
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.
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.