Your own auth
Ferry has no users of its own. The server asks one question of every request, auth(request), and your code answers it: a user id, or null for a 401. Whatever already signs your users in works.
import type { SyncServerOptions } from 'ferrysync/server';
type Auth = SyncServerOptions<never>['auth']; // (request) => string | null, or a promise of oneThe id is yours to choose (your database's user id, a provider's sub), as long as it stays the
same for the same person: rows and space memberships are kept under it.
A session cookie
When the app and its backend share a domain, the browser sends the session cookie with every sync
request, so auth reads it like any other route does:
import { createSyncServer, memoryStore } from 'ferrysync/server';
import { schema } from './schema';
export const syncServer = createSyncServer({
schema,
store: memoryStore(),
auth: async (request) => (await getSession(request))?.userId ?? null,
});A bearer token from any provider
Native apps, and web apps on another domain, send a token instead. Verify it against the provider's public keys, with no call to the provider per request:
import { createSyncServer, memoryStore } from 'ferrysync/server';
import { createRemoteJWKSet, jwtVerify } from 'jose';
import { schema } from './schema';
const keys = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'));
/** The token's subject, when its signature, issuer and audience check out. */
const userOf = async (request: Request) => {
const token = request.headers.get('authorization')?.replace(/^Bearer /, '');
if (!token) {
return null;
}
try {
const { payload } = await jwtVerify(token, keys, {
issuer: 'https://auth.example.com',
audience: 'my-app',
});
return payload.sub ?? null;
} catch {
return null;
}
};
export const syncServer = createSyncServer({ schema, store: memoryStore(), auth: userOf });The client sends the token on every request, asking for it each time so a refreshed one is used:
import { createSync, http, indexedDbStorage } from 'ferrysync';
import { schema } from './schema';
export const sync = createSync({
schema,
storage: indexedDbStorage('my-app'),
backend: http({
url: 'https://api.example.com/sync',
headers: async () => ({ authorization: `Bearer ${await getToken()}` }),
}),
});This is the shape of every hosted provider: Clerk, Auth0, WorkOS, Supabase, Firebase. Only the keys' URL and the claims change. See Supabase and Firebase for two of them end to end.
Gotchas
- Answer
null, do not throw, for a missing or invalid token:nullis a clean 401 the client understands; a throw is a 500. - A user id must not change. If a provider can give the same person two ids (two sign-in methods), map them to one of yours before answering.
- Sign-in and sign-out on the device are
sync.connectandsync.reset: see Accounts.