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.
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
| Runtime | Instead of serve(…) |
|---|---|
| Bun | export default { fetch: app.fetch, idleTimeout: 40 }: see Bun |
| Cloudflare Workers | export default app: see Cloudflare Workers |
| Deno | Deno.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:
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.
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,…/bootstrapand…/wait. - Do not read the body first.
fetchreads it (up tomaxBodyBytes). A middleware that consumedc.req.raw's body leaves nothing to read: usehandlewith the parsed body then. - Mind the idle timeout. A realtime
waitis held up to 30 seconds with nothing sent. Bun closes idle requests after 10 by default: raise itsidleTimeout.
Next.js
The browser keeps the data, and one route handler of the same Next.js app is the server. It uses the schema of the Quick start.
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.