Client SDK
API reference for @lakesync/client — LocalDB, SyncCoordinator, transports, and queues.
The @lakesync/client package provides the browser-side SDK for building local-first applications with LakeSync.
createClient
One-call factory that opens a database, registers schemas, creates an HTTP transport, and starts auto-sync.
function createClient(config: CreateClientConfig): Promise<LakeSyncClient>;CreateClientConfig
interface CreateClientConfig {
name: string;
schemas: TableSchema[];
clientId: string;
gateway: {
url: string;
gatewayId: string;
token?: string;
};
autoSyncMs?: number; // default 10000, 0 to disable
backend?: "idb" | "memory";
queue?: SyncQueue;
coordinatorConfig?: Omit<SyncCoordinatorConfig, "queue" | "clientId" | "autoSyncIntervalMs">;
}LakeSyncClient
interface LakeSyncClient {
coordinator: SyncCoordinator;
db: LocalDB;
transport: TransportWithCapabilities;
destroy(): Promise<void>;
}Example:
import { createClient } from "lakesync/client";
const client = await createClient({
name: "my-app",
clientId: "client-1",
schemas: [{ table: "todos", columns: [{ name: "title", type: "string" }] }],
gateway: { url: "https://gw.example.com", gatewayId: "gw-1", token: "jwt..." },
});
await client.coordinator.tracker.insert("todos", "todo-1", { title: "Hello" });
await client.destroy();LocalDB
SQLite database running in the browser via sql.js WASM, with optional IndexedDB snapshot persistence.
LocalDB.open(config)
class LocalDB {
static open(config: DbConfig): Promise<Result<LocalDB, DbError>>;
readonly name: string;
readonly backend: "idb" | "memory";
exec(sql: string, params?: unknown[]): Promise<Result<void, DbError>>;
query<T>(sql: string, params?: unknown[]): Promise<Result<T[], DbError>>;
transaction<T>(fn: (tx: Transaction) => T): Promise<Result<T, DbError>>;
save(): Promise<Result<void, DbError>>;
close(): Promise<void>;
}open(config)— Initialise sql.js WASM and create a database. When backend is"idb", loads any existing snapshot from IndexedDB.exec(sql, params?)— Execute a SQL statement (DDL or DML).query(sql, params?)— Execute a SQL query and return rows as typed objects.transaction(fn)— Execute a function within a BEGIN/COMMIT transaction with automatic rollback on error.save()— Persist the database snapshot to IndexedDB. No-op for"memory"backend.close()— Save (if IDB) and release resources.
DbConfig
interface DbConfig {
name: string;
backend?: "idb" | "memory";
}When backend is omitted, auto-detects: uses "idb" if indexedDB is available, otherwise "memory".
DbError
class DbError extends LakeSyncError {
constructor(message: string, cause?: Error);
}SyncTracker
Tracks local mutations (insert, update, delete) and produces column-level deltas that are pushed to a sync queue.
class SyncTracker {
constructor(
db: LocalDB,
queue: SyncQueue,
hlc: HLC,
clientId: string,
);
insert(
table: string,
rowId: string,
data: Record<string, unknown>,
): Promise<Result<void, LakeSyncError>>;
update(
table: string,
rowId: string,
data: Record<string, unknown>,
): Promise<Result<void, LakeSyncError>>;
delete(
table: string,
rowId: string,
): Promise<Result<void, LakeSyncError>>;
query<T>(
sql: string,
params?: unknown[],
): Promise<Result<T[], DbError>>;
}Each mutation applies the change to SQLite, extracts a RowDelta, and pushes it to the queue for eventual upstream delivery.
SyncCoordinator
Orchestrates push/pull sync between the local database and a remote gateway.
Constructor
class SyncCoordinator {
constructor(db: LocalDB, transport: TransportWithCapabilities, config?: SyncCoordinatorConfig);
readonly tracker: SyncTracker;
readonly clientId: string;
readonly lastSyncTime: Date | null;
syncOnce(): Promise<void>;
pushToGateway(): Promise<void>;
pullFromGateway(source?: string): Promise<number>;
pullFrom(source: string): Promise<number>;
executeAction(params: { connector: string; actionType: string; params: Record<string, unknown>; idempotencyKey?: string }): Promise<void>;
describeActions(): Promise<Result<ActionDiscovery, LakeSyncError>>;
startAutoSync(): void;
stopAutoSync(): void;
queueDepth(): Promise<number>;
on<K extends keyof SyncEvents>(event: K, listener: SyncEvents[K]): void;
off<K extends keyof SyncEvents>(event: K, listener: SyncEvents[K]): void;
}syncOnce()— Perform a single sync cycle (pull then push, depending onsyncMode). Includes initial checkpoint download on first sync. Also processes pending actions.pushToGateway()— Push pending deltas from the queue to the gateway.pullFromGateway(source?)— Pull remote deltas and apply them locally. Pass asourcename for adapter-sourced pull.pullFrom(source)— Convenience wrapper forpullFromGateway(source).executeAction(params)— Queue an imperative action for execution. GeneratesactionIdand HLC automatically. Requires anactionQueuein config. EmitsonActionCompletewhen the gateway responds.describeActions()— Discover available connectors and their supported action types from the gateway. Returns empty connectors when the transport doesn't support discovery.startAutoSync()— Start periodic sync + visibility change handler. Connects realtime transport if available.stopAutoSync()— Stop periodic sync, remove listeners, disconnect transport.
SyncCoordinatorConfig
interface SyncCoordinatorConfig {
queue?: SyncQueue;
hlc?: HLC;
clientId?: string;
maxRetries?: number;
syncMode?: SyncMode;
autoSyncIntervalMs?: number; // default 10000 (10s)
realtimeHeartbeatMs?: number; // default 60000 (60s)
actionQueue?: ActionQueue; // required for executeAction()
maxActionRetries?: number; // default 5
}autoSyncIntervalMs— Polling interval for HTTP transports. Default 10 seconds.realtimeHeartbeatMs— Polling interval when a realtime transport is active. Default 60 seconds. Acts as a safety net for deltas missed during brief disconnects.actionQueue— Queue for imperative actions. Required to useexecuteAction(). UseMemoryActionQueuefor development orIDBActionQueuefor persistence.maxActionRetries— Maximum retries before dead-lettering an action. Default 5.
SyncMode
type SyncMode = "full" | "pushOnly" | "pullOnly";full— Pull then push on each sync cycle (default).pushOnly— Only push local deltas. Useful for write-only producers.pullOnly— Only pull remote deltas. Useful for read-only consumers.
SyncEvents
interface SyncEvents {
onChange: (count: number) => void;
onSyncComplete: () => void;
onError: (error: Error) => void;
onActionComplete: (actionId: string, result: ActionResult | ActionErrorResult) => void;
}onActionComplete— Fired when an action succeeds or fails non-retryably. Check"data" in resultto distinguishActionResultfromActionErrorResult.
Transports
Transport Interfaces
The transport layer is split into focused capability interfaces. Only SyncTransport (push/pull) is required — checkpoint, real-time, and action capabilities are opt-in.
SyncTransport
interface SyncTransport {
push(msg: SyncPush): Promise<Result<{ serverHlc: HLCTimestamp; accepted: number }, LakeSyncError>>;
pull(msg: SyncPull): Promise<Result<SyncResponse, LakeSyncError>>;
}Core sync transport — push and pull deltas. Every transport must implement this.
CheckpointTransport
interface CheckpointTransport {
checkpoint(): Promise<Result<CheckpointResponse | null, LakeSyncError>>;
}Checkpoint downloads for initial sync. Returns null if no checkpoint is available.
RealtimeTransport
interface RealtimeTransport {
readonly supportsRealtime: boolean;
onBroadcast(callback: (deltas: RowDelta[], serverHlc: HLCTimestamp) => void): void;
connect(): void;
disconnect(): void;
}Real-time server-initiated broadcast support. Implemented by WebSocketTransport.
ActionTransport
interface ActionTransport {
executeAction(msg: ActionPush): Promise<Result<ActionResponse, LakeSyncError>>;
describeActions(): Promise<Result<ActionDiscovery, LakeSyncError>>;
listConnectorTypes(): Promise<Result<ConnectorDescriptor[], LakeSyncError>>;
}Imperative action execution and connector discovery. See Actions for usage.
TransportWithCapabilities
type TransportWithCapabilities = SyncTransport &
Partial<CheckpointTransport> &
Partial<RealtimeTransport> &
Partial<ActionTransport>;Union type used by SyncCoordinator. Transports must implement push/pull; other capabilities are detected at runtime.
LocalTransport
In-process transport for development and testing. Connects directly to a gateway instance.
class LocalTransport implements SyncTransport, CheckpointTransport, ActionTransport {
constructor(gateway: LocalGateway);
}
interface LocalGateway {
handlePush(msg: SyncPush): Result<{ serverHlc: HLCTimestamp; accepted: number }, LakeSyncError>;
handlePull(msg: SyncPull): Result<SyncResponse, LakeSyncError>;
handleAction?(msg: ActionPush, context?: AuthContext): Promise<Result<ActionResponse, ActionValidationError>>;
describeActions?(): ActionDiscovery;
}HttpTransport
Remote transport that communicates with a gateway over HTTP.
class HttpTransport implements SyncTransport, CheckpointTransport, ActionTransport {
constructor(config: HttpTransportConfig);
}
interface HttpTransportConfig {
baseUrl: string;
gatewayId: string;
token: string;
fetch?: typeof globalThis.fetch;
}WebSocketTransport
Real-time transport using a persistent WebSocket connection. Supports server-initiated broadcast for sub-second sync latency.
class WebSocketTransport implements SyncTransport, RealtimeTransport, CheckpointTransport {
constructor(config: WebSocketTransportConfig);
/** Whether the WebSocket is currently connected. */
readonly connected: boolean;
/** Always true — this transport supports real-time. */
readonly supportsRealtime: boolean;
connect(): void;
disconnect(): void;
onBroadcast(callback: (deltas: RowDelta[], serverHlc: HLCTimestamp) => void): void;
}
interface WebSocketTransportConfig {
url: string;
token: string;
onBroadcast?: (deltas: RowDelta[], serverHlc: HLCTimestamp) => void;
reconnectBaseMs?: number; // default 1000
reconnectMaxMs?: number; // default 30000
httpConfig?: HttpTransportConfig;
}url— WebSocket endpoint, e.g."wss://gateway.example.com/sync/gw-1/ws".token— JWT token, passed as?token=query parameter (browser WebSocket cannot set headers).httpConfig— When provided,checkpoint()delegates to an internalHttpTransport. Without it, checkpoint returnsnull.- Auto-reconnect — On disconnect, reconnects with exponential backoff (1s base, 30s max). Resets on successful connection.
See Real-Time Sync for a full guide.
Queues
SyncQueue (interface)
interface SyncQueue {
push(delta: RowDelta): Promise<Result<QueueEntry, LakeSyncError>>;
peek(limit: number): Promise<Result<QueueEntry[], LakeSyncError>>;
markSending(ids: string[]): Promise<Result<void, LakeSyncError>>;
ack(ids: string[]): Promise<Result<void, LakeSyncError>>;
nack(ids: string[]): Promise<Result<void, LakeSyncError>>;
depth(): Promise<Result<number, LakeSyncError>>;
clear(): Promise<Result<void, LakeSyncError>>;
}
interface QueueEntry {
id: string;
delta: RowDelta;
status: "pending" | "sending" | "acked";
createdAt: number;
retryCount: number;
retryAfter?: number;
}MemoryQueue
In-memory queue for development and testing. Deltas are lost on page refresh.
class MemoryQueue implements SyncQueue {
// All SyncQueue methods
}IDBQueue
IndexedDB-backed queue for production use. Persists deltas across page refreshes.
class IDBQueue implements SyncQueue {
constructor(dbName?: string);
// All SyncQueue methods
}Note: IDBQueue serialises HLCTimestamp to string internally because IndexedDB's structuredClone cannot handle bigint.
ActionQueue (interface)
Outbox-pattern queue for imperative actions. Required by SyncCoordinator.executeAction().
interface ActionQueue {
push(action: Action): Promise<Result<ActionQueueEntry, LakeSyncError>>;
peek(limit: number): Promise<Result<ActionQueueEntry[], LakeSyncError>>;
markSending(ids: string[]): Promise<Result<void, LakeSyncError>>;
ack(ids: string[]): Promise<Result<void, LakeSyncError>>;
nack(ids: string[]): Promise<Result<void, LakeSyncError>>;
depth(): Promise<Result<number, LakeSyncError>>;
clear(): Promise<Result<void, LakeSyncError>>;
}MemoryActionQueue
In-memory action queue for development and testing.
class MemoryActionQueue implements ActionQueue {}IDBActionQueue
IndexedDB-backed action queue for production use. Persists actions across page refreshes.
class IDBActionQueue implements ActionQueue {
constructor(dbName?: string);
}Schema Synchronisation
SchemaSynchroniser
Handles client-side schema migrations by comparing local table schemas against the gateway's schema version.
class SchemaSynchroniser {
constructor(db: LocalDB);
synchronise(
table: string,
serverSchema: TableSchema,
serverVersion: number,
): Promise<Result<void, LakeSyncError>>;
}Compares the locally stored schema version against the server's version and applies additive migrations (ALTER TABLE ... ADD COLUMN) when the client is behind.