Ferry
Server

Auth

Ferry has no users of its own. auth tells it who sent each request, with whatever your app already uses: a session cookie, a token, an auth library.

Server
import { createSyncServer, memoryStore } from 'ferrysync/server';

import { schema } from './schema';

export const syncServer = createSyncServer({
  schema,
  store: memoryStore(),
  // The user's id, or null for a 401.
  auth: async (request) => (await getSession(request))?.userId ?? null,
});

On the device, the client sends whatever your server reads, on every request:

Client
import { createSync, http } from 'ferrysync';

import { schema } from './schema';

export const sync = createSync({
  schema,
  backend: http({
    url: 'https://api.example.com/sync',
    headers: async () => ({ authorization: `Bearer ${await getToken()}` }),
    onUnauthorized: () => showToast('Please sign in again'),
  }),
});

auth(request)

It returnsThe server
a user idanswers as that user: their private space and the spaces they joined
null (or '')answers 401; the client calls onUnauthorized and retries later
throwsanswers 500, and hands the error to onError
  • It runs once per request, before the body is read, so an anonymous request costs nothing more.
  • request is your runtime's own Request: read headers, cookies and the URL as anywhere else.
  • It may be async.

A bearer token

Server
import { createSyncServer, memoryStore } from 'ferrysync/server';

import { schema } from './schema';

export const syncServer = createSyncServer({
  schema,
  store: memoryStore(),
  auth: async (request) => {
    const header = request.headers.get('authorization');
    if (header === null || !header.startsWith('Bearer ')) {
      return null;
    }
    return verifyToken(header.slice('Bearer '.length)); // your check: the user's id, or null
  },
});

With an auth library, ask it for the session from the request's headers, as Better Auth's auth.api.getSession({ headers: request.headers }) does. For tokens from Clerk, Supabase or your own issuer, verify the bearer token as above.

The user id

The id auth returns is the user, as far as sync goes.

  • Make it stable. It names the user's private space (user:<id>) and their memberships. An email address changes; a user's primary key does not.
  • It is opaque. Ferry never parses it; any non-empty string works.

A device belongs to one user

Each device has a client id, and the first user who syncs with it owns it. When another user signs in on the same device without wiping it, the server answers 403 (forbidden). Sign-out should wipe the device with sync.reset(local()): see Accounts.

Frameworks that parse bodies

fetch reads the body itself. When your framework already parsed it, call handle with the parsed body, the endpoint (the path's last segment) and the request, for auth. It sets no size limit: your framework's body parser does (maxBodyBytes applies to fetch only):

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

export const route = async (request: Request, endpoint: string, body: unknown) => {
  const { status, body: answer } = await syncServer.handle({ endpoint, body, request });
  return Response.json(answer, { status });
};

When the user is known only to your framework (a middleware set it on its own context), pass it along: with Hono, with Express.

Gotchas

  • Keep auth cheap. Besides each write, every device with realtime on sends a request about every 25 seconds. Cache sessions, or verify a signed token without a database read.
  • Allow the header across origins. A web app on another origin sends authorization only if your CORS settings allow it: see Hono's CORS.
  • Refresh tokens in headers. The function runs before every request, so a token that expires is replaced on the next one. onUnauthorized is where the app signs out when it cannot refresh.
  • A deleted account must get null. A token verified only by its signature stays valid until it expires, and the server cannot tell a deleted user from a new one: a device still let in writes their data again. Check revocation, or keep a short list of deleted ids next to the signature check: see Deleting users.

On this page