Ferry
Server

Deploy

Deploying Ferry is deploying your backend: the sync route ships with it, on its database. There is no service, worker or cron job to add. A few platforms need a setting or two, listed below.

What stays the same

  • One route in the app you already deploy, on the platform you already use.
  • Your database. The store creates its tables the first time it runs (or your migrations do: see Stores). With several instances starting at once, Postgres makes them take turns.
  • Your auth, your environment variables, your logs: pass onError to your logger.
  • Retention runs by itself, after requests: see Retention.
  • Backups are your database's. After restoring one, run afterRestore: see Backups.

What a platform may need

NeedWhyApplies to
A shared databaseEvery instance must see the same logSeveral instances, serverless
Requests of 30 s+A realtime wait is held up to 30 s with nothing sentEvery platform: check its limits
waitUntilWork after the response must finish before the function sleepsServerless
A broadcastWaits held by other instances hear of a change at once, not within 15 sSeveral instances (optional)

By platform

PlatformStoreSettings
One server or container (a VPS, Fly.io, Railway, Render)SQLite on a persistent volume, or PostgresA proxy in front must allow 30 s of silence: nginx's default (60 s) does
Several servers behind a load balancerPostgresThe balancer's idle timeout above 30 s; a broadcast
Vercel (Next.js)PostgresmaxDuration = 60 and waitUntil: after: see Next.js
Cloudflare Workersd1Store behind a durableObjectCoordinator, or Postgres through HyperdriveOne coordinator for D1, or a store per request: see Workers
Cloudflare Durable ObjectssqliteStore(durableObjectSqlite(ctx.storage)): the object's own SQLiteOne object per sync authority: see Durable Objects
AWS LambdaPostgresThe function's timeout above 30 s (Lambda's default is 3 s)
BunSQLite or PostgresidleTimeout: 40: see Bun

Behind API Gateway, whose limit is 29 seconds, realtime still works: devices ask for 25-second holds, which answer in time.

SQLite in production

SQLite is a fine production store for one server process: fast, and nothing to run.

  • The file must outlive deploys: a mounted volume, not the container's own disk.
  • One instance. Two processes can share the file only on one machine, and take turns writing. To run more instances, move to Postgres.
  • Back it up like any database: VACUUM INTO, Litestream, or your volume's snapshots. Restoring one takes a step more: see Backups.

Restarts and deploys

On shutdown, end held requests so devices reconnect right away, to the new instance:

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

process.on('SIGTERM', () => {
  void syncServer.close().then(() => process.exit(0));
});

close() answers every held wait, refuses new requests with a 503 (devices retry), and resolves once background work (hooks run after the response, retention) has finished.

Shipping a new schema

The server and the apps may run different schema versions for a while: app stores take days, and some users never update. Deploy the server first, accepting both versions:

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

import { schema } from './schema'; // now at version 2

export const syncServer = createSyncServer({
  schema,
  store: memoryStore(),
  auth: async (request) => (await getSession(request))?.userId ?? null,
  versions: { min: 1 }, // still accept version 1 apps; max defaults to schema.version
});

When you raise min, older apps are told to update: they keep working offline and sync once updated. See Evolving a schema.

Gotchas

  • A 504 every 30 seconds in your logs means something in front of the server cuts held requests short. Devices recover, but realtime turns slow: raise that timeout.
  • Serverless functions without waitUntil may be frozen mid-way through work after the response. Retention is then retried later; afterCommit hooks without waitUntil run before the response, so they are safe either way.
  • Several instances without a broadcast work: changes reach other instances' devices within 15 seconds instead of at once.

On this page