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
onErrorto 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
| Need | Why | Applies to |
|---|---|---|
| A shared database | Every instance must see the same log | Several instances, serverless |
| Requests of 30 s+ | A realtime wait is held up to 30 s with nothing sent | Every platform: check its limits |
waitUntil | Work after the response must finish before the function sleeps | Serverless |
A broadcast | Waits held by other instances hear of a change at once, not within 15 s | Several instances (optional) |
By platform
| Platform | Store | Settings |
|---|---|---|
| One server or container (a VPS, Fly.io, Railway, Render) | SQLite on a persistent volume, or Postgres | A proxy in front must allow 30 s of silence: nginx's default (60 s) does |
| Several servers behind a load balancer | Postgres | The balancer's idle timeout above 30 s; a broadcast |
| Vercel (Next.js) | Postgres | maxDuration = 60 and waitUntil: after: see Next.js |
| Cloudflare Workers | d1Store behind a durableObjectCoordinator, or Postgres through Hyperdrive | One coordinator for D1, or a store per request: see Workers |
| Cloudflare Durable Objects | sqliteStore(durableObjectSqlite(ctx.storage)): the object's own SQLite | One object per sync authority: see Durable Objects |
| AWS Lambda | Postgres | The function's timeout above 30 s (Lambda's default is 3 s) |
| Bun | SQLite or Postgres | idleTimeout: 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:
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:
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
waitUntilmay be frozen mid-way through work after the response. Retention is then retried later;afterCommithooks withoutwaitUntilrun 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.
Realtime and scaling
Devices hear about each other's changes within a moment, through plain HTTP: no WebSockets, no extra service. With several server instances, one more option keeps it that fast.
Backups
The sync tables live in your database, so you back them up with it: snapshots, pg_dump, point-in- time recovery, VACUUM INTO or Litestream for SQLite. Restoring one takes one more step than usual, because devices may be ahead of the backup.