@sveltebase/sync 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,525 +1,203 @@
1
1
  # @sveltebase/sync
2
2
 
3
- Reactive, local-first database synchronization library built for **Svelte 5** and **Cloudflare Workers / Durable Objects**.
3
+ Reactive, local-first database synchronization for Svelte 5 using a separate Cloudflare Worker for realtime sync.
4
4
 
5
- ## Features
5
+ ## Architecture
6
6
 
7
- - **Local Persistence**: Powered by [Dexie.js](https://dexie.org/) (IndexedDB). Zero-WASM, instant load times, persistent across page refreshes.
8
- - **Optimistic Updates**: Client mutations update the local database instantly (~1ms), sync with the server in the background, and roll back automatically on failures.
9
- - **Real-Time Sync**: Single multiplexed WebSocket connection fanning out updates to all active subscribers.
10
- - **Last-Write-Wins (LWW)**: Timestamps prevent out-of-order write conflicts.
11
- - **Delta Syncing (Incremental Load)**: Automatically pulls only modified records since the last sync time to conserve network bandwidth.
12
- - **Hibernate Friendly**: Client-initiated heartbeats allow Cloudflare Durable Objects to sleep when idle, cutting active execution costs down to near zero.
13
- - **Vite Integration**: Custom dev plugin simulating Durable Objects and bindings proxy locally without full worker compilation loops.
14
- - **Database Agnostic**: Completely decoupled from the underlying storage. You are not locked into Cloudflare D1; the sync handler hooks (`fetch`, `create`, `update`, `delete`) are simple, asynchronous JavaScript callbacks where you can connect to PostgreSQL, MySQL, Supabase, Neon, MongoDB, or any database of your choice.
7
+ `@sveltebase/sync` now uses two Workers:
15
8
 
16
- ---
17
-
18
- ## Architecture & Forked Adapter
19
-
20
- To bind custom Cloudflare Workers features (like **Durable Objects**, **Queues**, and **Email Handlers**) directly within a SvelteKit application, you **must use** the forked adapter:
21
-
22
- 👉 **`@joshthomas/sveltekit-adapter-cloudflare`**
23
-
24
- ### Why this adapter?
25
- The official `@sveltejs/adapter-cloudflare` owns the final worker entrypoint (`_worker.js`) and does not natively allow you to declare custom class exports (like Durable Objects) in the same worker.
26
-
27
- The **Josh Thomas fork** introduces a platform entrypoint (`src/platform.cloudflare.ts`) which SvelteKit bundles into the worker wrapper, allowing you to export Durable Objects while SvelteKit continues to manage Svelte routing and page rendering.
28
-
29
- ---
30
-
31
- ## 1. Cloudflare Configuration (`wrangler.jsonc`)
32
-
33
- Define D1 database and Durable Object namespace configurations in your `wrangler.jsonc` (or `wrangler.toml`):
34
-
35
- ```json
36
- {
37
- "compatibility_date": "2026-06-07",
38
- "compatibility_flags": ["nodejs_compat"],
39
- "main": ".svelte-kit/cloudflare/_worker.js",
40
- "d1_databases": [
41
- {
42
- "binding": "DB",
43
- "database_name": "sveltebase-sync",
44
- "database_id": "YOUR_DATABASE_ID",
45
- "migrations_dir": "drizzle/migrations"
46
- }
47
- ],
48
- "durable_objects": {
49
- "bindings": [
50
- {
51
- "name": "SYNC_ENGINE",
52
- "class_name": "SyncEngine"
53
- }
54
- ]
55
- },
56
- "migrations": [
57
- {
58
- "tag": "v1",
59
- "new_sqlite_classes": ["SyncEngine"]
60
- }
61
- ]
62
- }
9
+ ```txt
10
+ browser
11
+ -> SvelteKit app Worker
12
+ /api/sync -> env.SYNC_WORKER.fetch(request)
13
+ -> sync Worker
14
+ owns SyncEngine Durable Object
15
+ owns websocket upgrades
16
+ owns broadcasts
17
+ owns sync database/runtime bindings
18
+ owns sync auth verification
63
19
  ```
64
20
 
65
- ---
21
+ The SvelteKit app Worker does not export sync Durable Objects. This lets apps use the official `@sveltejs/adapter-cloudflare`.
66
22
 
67
- ## 2. Setup Guide
23
+ ## Imports
68
24
 
69
- ### Step 1: Client Schema & Client Creation
25
+ ```ts
26
+ import { SyncClient, createLiveQuery } from "@sveltebase/sync/client";
27
+ import { defineSync, createPublisher } from "@sveltebase/sync/server";
28
+ import { syncProxy } from "@sveltebase/sync/sveltekit";
29
+ import { defineSyncWorker, SyncEngine } from "@sveltebase/sync/cloudflare";
30
+ ```
70
31
 
71
- Configure your client-side IndexedDB database using `SyncClient`.
32
+ ## Client
72
33
 
73
- ```typescript
34
+ ```ts
74
35
  // src/lib/sync-client.ts
75
36
  import { SyncClient } from "@sveltebase/sync/client";
76
- import type { Todo } from "$lib/server/db/schema";
77
37
 
78
- // Map table name to row type
79
- type AppDatabaseSchema = {
80
- todos: Todo;
38
+ type AppSchema = {
39
+ todos: {
40
+ id: string;
41
+ title: string;
42
+ completed: boolean;
43
+ updatedAt: string;
44
+ };
81
45
  };
82
46
 
83
- export const sync = new SyncClient<AppDatabaseSchema>({
84
- name: "sveltebase-sync", // Local IndexedDB name
85
- url: "/api/sync", // WebSocket endpoint
47
+ export const sync = new SyncClient<AppSchema>({
48
+ name: "app-sync",
49
+ url: "/api/sync",
86
50
  tables: {
87
51
  todos: {
88
- indexes: "id, completed, createdAt", // IndexedDB indexes
89
- channel: "todos", // Sync channel
52
+ indexes: "id, completed, updatedAt",
53
+ channel: "todos",
90
54
  },
91
55
  },
92
56
  });
93
57
  ```
94
58
 
95
- Use it in your Svelte 5 components (using your preferred Dexie reactivity hook, such as `liveQuery` from `dexie` or `dexie-svelte-query`):
96
- ```svelte
97
- <script lang="ts">
98
- import { sync } from "$lib/sync-client";
99
- import { liveQuery } from "dexie";
100
- import { Check, Trash } from "lucide-svelte";
101
-
102
- // Standard Dexie liveQuery updates instantly on mutations & remote syncs
103
- const todos = liveQuery(() => sync.todos.orderBy("createdAt").reverse().toArray());
104
-
105
- let title = "";
106
-
107
- async function addTodo() {
108
- if (!title.trim()) return;
109
- await sync.todos.add({
110
- id: crypto.randomUUID(),
111
- title,
112
- completed: false,
113
- createdAt: new Date().toISOString(),
114
- updatedAt: new Date().toISOString(),
115
- });
116
- title = "";
117
- }
118
- </script>
119
-
120
- <input bind:value={title} onkeydown={(e) => e.key === 'Enter' && addTodo()} />
121
-
122
- {#each ($todos || []) as todo (todo.id)}
123
- <div>
124
- <button onclick={() => sync.todos.update(todo.id, { completed: !todo.completed })}>
125
- <Check class={todo.completed ? "text-emerald-500" : ""} />
126
- </button>
127
- <span>{todo.title}</span>
128
- <button onclick={() => sync.todos.delete(todo.id)}><Trash /></button>
129
- </div>
130
- {/each}
131
- ```
132
-
133
- ### Synced Database Operations
134
-
135
- Under the hood, `@sveltebase/sync` intercepts native Dexie table writes to capture and propagate mutations to the backend. The following methods automatically sync with the server:
59
+ ## Sync Handlers
136
60
 
137
- * **`.add(row)`**: Triggers a `"create"` sync mutation.
138
- * **`.put(row)` or `.put(id, changes)`**: Computes a diff of changed properties (for updates) or initiates a `"create"` mutation (for new rows) and sends it to the server.
139
- * **`.update(id, changes)`**: Performs a local partial update and propagates the changes to the server as an `"update"` mutation.
140
- * **`.delete(id)`**: Locally deletes the row and propagates a `"delete"` mutation to the server.
61
+ Handlers run in the sync Worker. Use `ctx.platform.env` for Cloudflare bindings, `ctx.auth` for verified auth data, and `ctx.identity` for ownership/scoped fanout.
141
62
 
142
- > [!NOTE]
143
- > **Bulk methods** (such as `.bulkAdd()`, `.bulkPut()`, and `.bulkDelete()`) bypass backend syncing entirely. They write directly to IndexedDB, which is useful for performing offline seeding or local-only updates.
144
-
145
- ---
146
-
147
- ### Step 2: Define Sync Handlers (Server)
148
-
149
- > [!NOTE]
150
- > **Database Agnostic (No Lock-In):**
151
- > While the examples below connect to **Cloudflare D1 SQLite** (using Drizzle ORM), `@sveltebase/sync` is completely database-agnostic. The `fetch`, `create`, `update`, and `delete` handlers are standard asynchronous functions. You can fetch, save, or delete data using **any database** of your choice (PostgreSQL, MySQL, Supabase, Neon, MongoDB, etc.) by writing the appropriate database connection logic inside these hooks.
152
-
153
- Define the handlers that translate IndexedDB operations (fetch, create, update, delete) to database queries:
154
-
155
- ```typescript
156
- // src/lib/server/sync-todos.ts
157
- import { defineSync } from "@sveltebase/sync";
158
- import { getDB } from "$lib/server/db/index.js";
159
- import { todos } from "$lib/server/db/schema";
160
- import { desc, eq, gt } from "drizzle-orm";
161
- import type { Todo } from "$lib/server/db/schema";
63
+ ```ts
64
+ // src/lib/server/sync-handlers.ts
65
+ import { defineSync } from "@sveltebase/sync/server";
162
66
 
163
- export const todoSync = defineSync<Todo>({
67
+ export const todoSync = defineSync({
164
68
  channel: "todos",
165
69
 
166
70
  fetch: async (ctx, since) => {
167
- const db = getDB(ctx.platform);
168
- if (since) {
169
- return await db
170
- .select()
171
- .from(todos)
172
- .where(gt(todos.updatedAt, since))
173
- .orderBy(desc(todos.createdAt));
174
- }
175
- return await db.select().from(todos).orderBy(desc(todos.createdAt));
176
- },
177
-
178
- create: async (ctx, data) => {
179
- const db = getDB(ctx.platform);
180
- const [created] = await db
181
- .insert(todos)
182
- .values(data)
183
- .onConflictDoUpdate({
184
- target: todos.id,
185
- set: {
186
- title: data.title,
187
- completed: data.completed,
188
- updatedAt: new Date().toISOString(),
189
- },
190
- })
191
- .returning();
192
- return created;
71
+ const db = ctx.platform.env.DB;
72
+ // Query any database here.
73
+ return [];
193
74
  },
194
75
 
195
- update: async (ctx, key, changes) => {
196
- const db = getDB(ctx.platform);
197
- const [updated] = await db
198
- .update(todos)
199
- .set({ ...changes, updatedAt: new Date().toISOString() })
200
- .where(eq(todos.id, key))
201
- .returning();
202
- return updated;
76
+ authorize: async (ctx) => {
77
+ if (!ctx.auth) {
78
+ throw new Error("Unauthorized");
79
+ }
203
80
  },
204
81
 
205
- delete: async (ctx, key) => {
206
- const db = getDB(ctx.platform);
207
- await db.delete(todos).where(eq(todos.id, key));
82
+ scope: (ctx) => {
83
+ return ctx.identity ? [ctx.identity] : [];
208
84
  },
209
85
  });
210
- ```
211
-
212
- Export handlers from a single list:
213
- ```typescript
214
- // src/lib/server/sync-handlers.ts
215
- import { todoSync } from "./sync-todos.js";
216
86
 
217
87
  export const handlers = [todoSync];
218
88
  ```
219
89
 
220
- ---
90
+ ## Sync Worker
221
91
 
222
- ### Step 3: SvelteKit WebSocket Server Route
92
+ Create a standalone Worker entrypoint that owns the Durable Object:
223
93
 
224
- Set up the upgrade endpoint to forward SvelteKit HTTP upgrades to Durable Objects.
94
+ ```ts
95
+ // src/worker/sync.ts
96
+ import { jwtCookieAuth } from "@sveltebase/auth/sync";
97
+ import { defineSyncWorker, SyncEngine } from "@sveltebase/sync/cloudflare";
98
+ import { handlers } from "$lib/server/sync-handlers";
225
99
 
226
- ```typescript
227
- // src/routes/api/sync/+server.ts
228
- import { handleUpgrade } from "@sveltebase/sync";
229
- import type { RequestEvent, RequestHandler } from "@sveltejs/kit";
230
-
231
- export const GET: RequestHandler = (event: RequestEvent) => {
232
- return handleUpgrade(event.request, event.platform);
233
- };
234
- ```
235
-
236
- ---
237
-
238
- ### Step 4: Svelte Config & Cloudflare Platform Entrypoint
239
-
240
- Configure `@joshthomas/sveltekit-adapter-cloudflare` in your `svelte.config.js`:
241
-
242
- ```javascript
243
- // svelte.config.js
244
- import adapter from "@joshthomas/sveltekit-adapter-cloudflare";
100
+ export default defineSyncWorker({
101
+ handlers,
102
+ auth: jwtCookieAuth(),
103
+ });
245
104
 
246
- export default {
247
- kit: {
248
- adapter: adapter({
249
- platform: "src/platform.cloudflare.ts" // Platform config file
250
- })
251
- }
252
- };
105
+ export { SyncEngine };
253
106
  ```
254
107
 
255
- Create `src/platform.cloudflare.ts` to export your Durable Object `SyncEngine` class:
256
-
257
- ```typescript
258
- // src/platform.cloudflare.ts
259
- import { SyncEngineBase } from "@sveltebase/sync/server";
260
- import { handlers } from "./lib/server/sync-handlers.js";
261
-
262
- // Export the Durable Object class compiled into the worker
263
- export class SyncEngine extends SyncEngineBase {
264
- constructor(ctx: DurableObjectState, env: Env) {
265
- super(ctx, env, handlers);
266
- }
267
- }
268
- ```
108
+ `defineSyncWorker()` handles:
269
109
 
270
- ---
110
+ - `GET /api/sync`: public websocket upgrade endpoint
111
+ - `POST /broadcast`: publish one external change
112
+ - `POST /broadcast-batch`: publish a batch of external changes
271
113
 
272
- ### Step 5: Vite Dev Plugin Setup
114
+ `GET /websocket` is internal to the sync Worker and Durable Object.
273
115
 
274
- In Vite development mode, Durable Objects are not natively available. We provide a Vite plugin (`syncDevPlugin`) that intercepts upgrades and emulates the DO synchronization broker locally in Node.js.
116
+ ## SvelteKit Proxy Route
275
117
 
276
- Configure `vite.config.ts`:
118
+ Keep browsers connecting to the app origin so existing cookies are sent:
277
119
 
278
- ```typescript
279
- // vite.config.ts
280
- import { sveltekit } from "@sveltejs/kit/vite";
281
- import { defineConfig } from "vite";
282
- import { syncDevPlugin } from "@sveltebase/sync/vite";
120
+ ```ts
121
+ // src/routes/api/sync/+server.ts
122
+ import { SYNC_WORKER_URL } from "$env/static/private";
123
+ import { syncProxy } from "@sveltebase/sync/sveltekit";
283
124
 
284
- export default defineConfig({
285
- plugins: [
286
- syncDevPlugin({
287
- // Path to your sync handlers. Uses ssrLoadModule so SvelteKit
288
- // path aliases (like $lib) resolve perfectly at runtime.
289
- handlersPath: "$lib/server/sync-handlers"
290
- }),
291
- sveltekit()
292
- ]
125
+ export const { GET, POST } = syncProxy({
126
+ fallbackUrl: SYNC_WORKER_URL,
293
127
  });
294
128
  ```
295
129
 
296
- ---
297
-
298
- ## 3. Local Development Features
299
-
300
- ### Automatic Bindings Proxy
301
- During development (`vite dev`), the dev engine uses Wrangler's programmatic Node API `getPlatformProxy()` under the hood. It caches the proxy on `globalThis` to survive Vite HMR reloads.
302
-
303
- Both SvelteKit and the dev WebSocket server share the **exact same emulated D1 database instance** automatically.
304
-
305
- ### Message Buffering
306
- Vite's module loading is asynchronous. When upgrading WebSocket connections, the plugin buffers incoming WebSocket frames during the module import phase. Once modules have fully loaded and handlers are registered, it replays the buffered messages to avoid connection race conditions.
130
+ In production, configure a Cloudflare service binding named `SYNC_WORKER`. In local development, use `fallbackUrl` such as `http://localhost:8788/api/sync`.
307
131
 
308
- ---
132
+ ## Publishing Server Events
309
133
 
310
- ## 4. Security, Authorization & Scoping
134
+ Publishing is explicit. It never reads SvelteKit request context implicitly.
311
135
 
312
- ### Handshake HTTP Context (Cookies & Headers)
313
- When the WebSocket connection is established, the HTTP upgrade request's headers, cookies, and query parameters are captured.
136
+ ```ts
137
+ import { createPublisher } from "@sveltebase/sync/server";
314
138
 
315
- This context is preserved and passed to every sync handler execution (`fetch`, `create`, `update`, `delete`, `authorize`, `scope`) via the **`ctx.request`** object. Developers can parse session cookies or credentials inside mutations and queries:
316
-
317
- ```typescript
318
- // Helper to extract session profile from handshake request
319
- async function getSession(ctx: SyncContext) {
320
- const cookie = ctx.request.headers.get("Cookie");
321
- const db = getDB(ctx.platform);
322
- // Perform session verification/DB lookup...
323
- return { userId: "usr_123", role: "admin" };
324
- }
325
- ```
326
-
327
- ---
139
+ type AppSchema = {
140
+ todos: { id: string; title: string; updatedAt: string };
141
+ };
328
142
 
329
- ### The `authorize` Hook
330
- The `authorize` hook acts as a guard. It runs synchronously on the server when a client attempts to **subscribe** to a channel or submit a **mutation** (create, update, delete). If it throws an error, the operation is rejected and rolled back.
143
+ const publish = createPublisher<AppSchema>({
144
+ platform: ctx.platform,
145
+ binding: "SYNC_WORKER",
146
+ fallbackUrl: env.SYNC_WORKER_URL,
147
+ });
331
148
 
332
- ```typescript
333
- authorize: async (ctx) => {
334
- const user = await getSession(ctx);
335
- if (!user) {
336
- throw new Error("Unauthorized access to channel");
337
- }
338
- }
149
+ await publish("todos", "update", todo.id, todo);
339
150
  ```
340
151
 
341
- ---
342
-
343
- ### Throwing & Filtering in Handlers (CRUD Operations)
344
-
345
- Beyond the global `authorize` hook, you can enforce security directly inside your query (`fetch`) and mutation (`create`, `update`, `delete`) handlers:
152
+ Inside the sync Worker, `createPublisher()` publishes directly to `platform.env.SYNC_ENGINE`. Inside the app Worker, it publishes through `platform.env.SYNC_WORKER.fetch()`. Without a binding, it uses `fallbackUrl`.
346
153
 
347
- #### 1. Filtering on Read (`fetch`)
348
- Use the handshake HTTP request (`ctx.request`) to dynamically filter the records fetched from the database, preventing users from pulling unauthorized rows.
154
+ ## Cloudflare Configuration
349
155
 
350
- ```typescript
351
- fetch: async (ctx, since) => {
352
- const db = getDB(ctx.platform);
353
- const user = await getSession(ctx);
156
+ App Worker:
354
157
 
355
- let query = db.select().from(todos);
356
- const conditions = [];
357
-
358
- // Enforce read boundaries
359
- if (user.role !== "admin") {
360
- conditions.push(eq(todos.published, true)); // Non-admins only read published todos
361
- }
362
- if (since) {
363
- conditions.push(gt(todos.updatedAt, since)); // Apply delta sync timestamp
364
- }
365
-
366
- if (conditions.length > 0) {
367
- query = query.where(and(...conditions));
368
- }
369
- return await query;
370
- }
371
- ```
372
-
373
- #### 2. Write & Delete Handlers (Optional)
374
- The `create`, `update`, and `delete` handlers are optional. If you omit any of these handlers, Sveltebase Sync treats the channel as read-only for that operation and will automatically reject any incoming client mutations.
375
-
376
- If you *do* define them, you can throw regular JavaScript/TypeScript errors inside your mutation handlers. When an error is thrown:
377
- 1. The server catches the error and rejects the mutation.
378
- 2. The server sends a rejection response back to the client.
379
- 3. The client receives the rejection, triggers the `rollback` function, and reverts the optimistic UI change in IndexedDB.
380
-
381
- ```typescript
382
- create: async (ctx, data) => {
383
- const user = await getSession(ctx);
384
-
385
- // Guard write action
386
- if (user.role !== "editor" && user.role !== "admin") {
387
- throw new Error("You do not have permission to create items.");
388
- }
389
-
390
- const db = getDB(ctx.platform);
391
- const [created] = await db.insert(todos).values(data).returning();
392
- return created;
393
- },
394
-
395
- update: async (ctx, key, changes) => {
396
- const user = await getSession(ctx);
397
- const db = getDB(ctx.platform);
398
-
399
- // Fetch target record to verify ownership
400
- const [record] = await db.select().from(todos).where(eq(todos.id, key));
401
- if (record.ownerId !== user.userId && user.role !== "admin") {
402
- throw new Error("You cannot update a record owned by someone else.");
403
- }
404
-
405
- const [updated] = await db.update(todos).set(changes).where(eq(todos.id, key)).returning();
406
- return updated;
407
- },
408
-
409
- delete: async (ctx, key) => {
410
- const user = await getSession(ctx);
411
-
412
- // Guard delete action
413
- if (user.role !== "admin") {
414
- throw new Error("Only admins can delete items.");
415
- }
416
-
417
- const db = getDB(ctx.platform);
418
- await db.delete(todos).where(eq(todos.id, key));
158
+ ```jsonc
159
+ {
160
+ "name": "my-app",
161
+ "main": ".svelte-kit/cloudflare/_worker.js",
162
+ "compatibility_date": "2026-06-07",
163
+ "compatibility_flags": ["nodejs_compat"],
164
+ "services": [
165
+ {
166
+ "binding": "SYNC_WORKER",
167
+ "service": "my-app-sync"
168
+ }
169
+ ]
419
170
  }
420
171
  ```
421
172
 
422
- ---
423
-
424
- ### The `scope` Hook (Row-Level Broadcast Filtering)
425
- The `scope` hook determines which of the connected and subscribed clients should receive real-time notifications when a database record is modified. It runs asynchronously after a mutation succeeds on the database.
426
-
427
- > [!CAUTION]
428
- > **Security Warning:** If you omit the `scope` hook, Sveltebase Sync defaults to broadcasting mutations to `"all"` subscribed connections.
429
- > If your channel contains user-private data (meaning you filter by user ID inside the `fetch` handler), you **must** also define a `scope` hook that returns the user ID of the owner: `scope: (ctx, action, data) => [data.userId]`. Otherwise, a user's private updates will be broadcast to all connected users in real time.
430
-
431
- * Return **`"all"`** to broadcast the change to every client subscribed to the channel.
432
- * Return an **array of user IDs** (`string[]`) to restrict the broadcast. The broker will match these IDs against the connection's registered identity and skip broadcasting to everyone else.
433
-
434
- ```typescript
435
- export const todoSync = defineSync<Todo>({
436
- channel: "todos",
173
+ Sync Worker:
437
174
 
438
- // Runs when a todo changes. Returns list of user IDs allowed to see this update
439
- scope: async (ctx, action, data) => {
440
- const db = getDB(ctx.platform);
441
-
442
- // 1. Public records are broadcasted to all subscribers
443
- if (data.published) {
444
- return "all";
175
+ ```jsonc
176
+ {
177
+ "name": "my-app-sync",
178
+ "main": "./src/worker/sync.ts",
179
+ "compatibility_date": "2026-06-07",
180
+ "compatibility_flags": ["nodejs_compat"],
181
+ "durable_objects": {
182
+ "bindings": [
183
+ {
184
+ "name": "SYNC_ENGINE",
185
+ "class_name": "SyncEngine"
186
+ }
187
+ ]
188
+ },
189
+ "migrations": [
190
+ {
191
+ "tag": "v1",
192
+ "new_sqlite_classes": ["SyncEngine"]
445
193
  }
446
-
447
- // 2. Draft/Private records are only broadcasted to admins
448
- const admins = await db
449
- .select({ id: users.id })
450
- .from(users)
451
- .where(eq(users.role, "admin"));
452
-
453
- return admins.map((admin) => admin.id);
454
- }
455
- });
456
- ```
457
-
458
- #### How Connection Identities are Registered
459
- The broker matches the user IDs returned by `scope` to each active client's socket state. The server registers the connection's identity during the handshake using query parameters or the `x-user-id` header:
460
- ```typescript
461
- const userId = url.searchParams.get("userId") || request.headers.get("x-user-id");
462
- ```
463
-
464
- ##### Authenticating using SvelteKit Sessions & Cookies (Recommended)
465
- Instead of exposing user IDs in client-side WebSocket URLs, you can resolve the user session on the server inside your SvelteKit route (`+server.ts`) and inject the verified `x-user-id` header before calling `handleUpgrade()`:
466
-
467
- ```typescript
468
- // src/routes/api/sync/+server.ts
469
- import { handleUpgrade } from "@sveltebase/sync";
470
- import type { RequestEvent, RequestHandler } from "@sveltejs/kit";
471
-
472
- export const GET: RequestHandler = (event: RequestEvent) => {
473
- // 1. Get user identity from your custom server-side session/cookies
474
- const user = event.locals.user; // e.g., set by your auth hook middleware
475
- if (!user) {
476
- return new Response("Unauthorized", { status: 401 });
477
- }
478
-
479
- // 2. Clone the request and inject the verified user ID header
480
- const request = new Request(event.request);
481
- request.headers.set("x-user-id", user.id);
482
-
483
- // 3. Hand off to the sync engine
484
- return handleUpgrade(request, event.platform);
485
- };
194
+ ]
195
+ }
486
196
  ```
487
- This approach keeps WebSocket URLs clean of private IDs and ensures all active sockets are automatically authenticated with their verified session roles/IDs.
488
-
489
- ---
490
-
491
- ## 5. Type-Safe Backend Event Publishing (`createPublisher`)
492
-
493
- When publishing backend events (e.g. from standard API routes, message queues, or cron triggers) to push updates to connected clients, you can create a type-safe publisher matched to your application's database schema. This checks channels (including dynamic channel patterns like `"todos:user_123"`), actions, and payloads at compile-time:
494
-
495
- ```typescript
496
- import { createPublisher } from "@sveltebase/sync";
497
- import type { Todo } from "$lib/server/db/schema";
498
-
499
- // Define schema matching channel names to model types
500
- type AppSyncSchema = {
501
- todos: Todo;
502
- };
503
-
504
- // Create typed publish function (Option A: Explicit Schema)
505
- const publish = createPublisher<AppSyncSchema>();
506
-
507
- // Create typed publish function (Option B: Automatically inferred from Sync Handlers)
508
- import { handlers } from "./lib/server/sync-handlers.js";
509
- const publish = createPublisher(handlers);
510
-
511
- // 1. Publish a create event (expects full Todo payload)
512
- await publish("todos", "create", todo.id, todo);
513
197
 
514
- // 2. Publish an update event (expects Partial<Todo> payload)
515
- await publish("todos", "update", todo.id, { completed: true });
198
+ Both Workers need the same session secret when using `@sveltebase/auth/sync`:
516
199
 
517
- // 3. Publish a delete event (expects optional { updatedAt: string } metadata)
518
- await publish("todos", "delete", todo.id, undefined);
519
-
520
- // 4. Supports scoped/dynamic channels (e.g. "channelName:scopeId")
521
- await publish("todos:user_123", "update", todo.id, { title: "New Title" });
200
+ ```bash
201
+ wrangler secret put JWT_SECRET --config wrangler.jsonc
202
+ wrangler secret put JWT_SECRET --config wrangler.sync.jsonc
522
203
  ```
523
-
524
-
525
-
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,KAAK,KAAK,EAAE,MAAM,OAAO,CAAC;AAI1C,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACrC,CAAC;AAaF,cAAM,eAAe,CACnB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CACzD,SAAQ,KAAK;IACb,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,cAAc,CAA4C;IAClE,OAAO,CAAC,YAAY,CAA6C;IACjE,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,cAAc,CAAqB;IAG3C,OAAO,CAAC,YAAY,CAA0B;IAG9C,OAAO,CAAC,gBAAgB,CAAsC;IAE9D,OAAO,CAAC,aAAa,CAMb;gBAEI,OAAO,EAAE;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,EAAE,WAAW,CAAC,CAAC;KACrD;IAoBD,IAAW,MAAM,gDAEhB;IAED,OAAO,CAAC,cAAc;YAiJR,OAAO;IAuFd,SAAS;IAgBhB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,aAAa;YAOP,kBAAkB;IAoBhC,OAAO,CAAC,kBAAkB;YAkBZ,UAAU;YAiBV,aAAa;YAqBb,mBAAmB;IAyFjC,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,eAAe;IAqChB,UAAU;CAelB;AAED,MAAM,MAAM,UAAU,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,eAAe,CAAC,OAAO,CAAC,GAAG;KAC5G,CAAC,IAAI,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;CACxC,GAAG;IACF,CAAC,SAAS,EAAE,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KACvB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzD,OAAO,EAAE;IACT,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,EAAE,WAAW,CAAC,CAAC;CACrD,KAAK,UAAU,CAAC,OAAO,CAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,KAAK,KAAK,EAAE,MAAM,OAAO,CAAC;AAI1C,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACrC,CAAC;AAaF,cAAM,eAAe,CACnB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CACzD,SAAQ,KAAK;IACb,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,cAAc,CAA4C;IAClE,OAAO,CAAC,YAAY,CAA6C;IACjE,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,cAAc,CAAqB;IAG3C,OAAO,CAAC,YAAY,CAA0B;IAG9C,OAAO,CAAC,gBAAgB,CAAsC;IAE9D,OAAO,CAAC,aAAa,CAMb;gBAEI,OAAO,EAAE;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,EAAE,WAAW,CAAC,CAAC;KACrD;IAoBD,IAAW,MAAM,gDAEhB;IAED,OAAO,CAAC,cAAc;YAiJR,OAAO;IAuFd,SAAS;IAgBhB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,aAAa;YAOP,kBAAkB;IAoBhC,OAAO,CAAC,kBAAkB;YAkBZ,UAAU;YAiBV,aAAa;YAqBb,mBAAmB;IAyFjC,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,eAAe;IAqChB,UAAU;CAelB;AAED,MAAM,MAAM,UAAU,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,eAAe,CAAC,OAAO,CAAC,GAAG;KAC5G,CAAC,IAAI,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;CACxC,GAAG;IACF,CAAC,SAAS,EAAE,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KACvB,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACzD,OAAO,EAAE;IACT,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,MAAM,EAAE,MAAM,CAAC,MAAM,OAAO,GAAG,MAAM,EAAE,WAAW,CAAC,CAAC;CACrD,KAAK,UAAU,CAAC,OAAO,CAA0B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"live-query.svelte.d.ts","sourceRoot":"","sources":["../../src/client/live-query.svelte.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAC9B,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,GAAG,CAAC;CACb,CAAC;AAEF,wBAAgB,eAAe,CAAC,CAAC,EAC/B,OAAO,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAC7B,YAAY,CAAC,EAAE,MAAM,OAAO,EAAE,GAC7B,cAAc,CAAC,CAAC,CAAC,CA6BnB"}
1
+ {"version":3,"file":"live-query.svelte.d.ts","sourceRoot":"","sources":["live-query.svelte.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAC9B,IAAI,CAAC,EAAE,CAAC,CAAC;IACT,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,GAAG,CAAC;CACb,CAAC;AAEF,wBAAgB,eAAe,CAAC,CAAC,EAC/B,OAAO,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAC7B,YAAY,CAAC,EAAE,MAAM,OAAO,EAAE,GAC7B,cAAc,CAAC,CAAC,CAAC,CA6BnB"}