@ahmetskilinc/sync-server
v0.4.0
Published
Server for sync-engine: source-of-truth apply loop, global sync-ID log, WebSocket fan-out
Maintainers
Readme
@ahmetskilinc/sync-server
The server half of sync-engine: applies client transactions to your database, stamps each confirmed mutation with a global sync ID, and fans it out to every connected client.
npm install @ahmetskilinc/sync-server @ahmetskilinc/sync-coreimport { WebSocketServer } from "ws";
import {
SyncServer,
attachWebSocketGuarded,
createConnectionGuard,
getCookie,
} from "@ahmetskilinc/sync-server";
import { schema } from "./schema.js";
const server = new SyncServer({
schema,
database: new MyPostgresAdapter(), // MemoryDatabase by default
requireContext: true,
validateTransaction: (txn, context) => null, // write rules
authorizeRead: (record, model, context) => // read rules
record.tenantId === context.tenantId,
onError: (error, where) => logger.error({ error, where }),
});
const guard = createConnectionGuard({
origin: ["https://app.example.com"],
authenticate: async (request) => {
const token = getCookie(request, "session");
return token ? await lookupUser(token) : null; // null ⇒ refuse
},
});
const wss = new WebSocketServer({ port: 8080, maxPayload: 256 * 1024 });
wss.on("connection", (socket, request) => {
void attachWebSocketGuarded(server, socket, request, guard);
});Security defaults worth understanding
Browsers do not apply the same-origin policy to WebSockets. Any page on any origin can open a socket to your server, and the browser attaches the user's cookies. Authenticating from a cookie without checking Origin means every logged-in user is one visited page away from a full session (cross-site WebSocket hijacking). createConnectionGuard checks Origin by default and refuses requests carrying none.
authorizeRead governs what a connection may see — applied to the bootstrap snapshot, catch-up, and live fan-out. Without it, every client receives every record, even with perfect authentication.
Pair both with requireContext: true, so a connection that somehow skipped the guard cannot write.
Persistence
Implement DatabaseAdapter: get, put, delete, getAll, plus an optional but strongly recommended applyBatch that commits one transaction's writes atomically. Without applyBatch, a failure partway through leaves earlier writes committed while the client rolls everything back — the database and sync log then disagree permanently.
Deployment
Single long-lived process by default. For serverless or multi-instance, pass distributed (shared sync-ID sequence + action table) and a stable epoch, and feed ingestActions from that table.
server.stats reports connections, queue depth, log size and pending acks for monitoring; maxConnections bounds fan-out cost.
Zero runtime dependencies beyond @ahmetskilinc/sync-core — the WebSocket type is structural, so ws is not required.
Full documentation: github.com/ahmetskilinc/sync-engine · Upgrading? See MIGRATION.md
MIT
