jazz-tools 2.0.0-alpha.21 → 2.0.0-alpha.22

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.
Files changed (225) hide show
  1. package/bin/docs-index.db +0 -0
  2. package/bin/docs-index.txt +1624 -542
  3. package/bin/jazz-tools.js +19 -40
  4. package/bin/native/jazz-tools-darwin-arm64 +0 -0
  5. package/bin/native/jazz-tools-darwin-x64 +0 -0
  6. package/bin/native/jazz-tools-linux-arm64 +0 -0
  7. package/bin/native/jazz-tools-linux-x64 +0 -0
  8. package/bin/native/jazz-tools-windows-x64.exe +0 -0
  9. package/dist/backend/create-jazz-context.d.ts +31 -6
  10. package/dist/backend/create-jazz-context.d.ts.map +1 -1
  11. package/dist/backend/create-jazz-context.js +35 -5
  12. package/dist/backend/create-jazz-context.js.map +1 -1
  13. package/dist/backend/create-jazz-context.test.js +61 -6
  14. package/dist/backend/create-jazz-context.test.js.map +1 -1
  15. package/dist/cli.d.ts +29 -2
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +648 -246
  18. package/dist/cli.js.map +1 -1
  19. package/dist/cli.test.js +512 -297
  20. package/dist/cli.test.js.map +1 -1
  21. package/dist/codegen/schema-reader.d.ts.map +1 -1
  22. package/dist/codegen/schema-reader.js +6 -1
  23. package/dist/codegen/schema-reader.js.map +1 -1
  24. package/dist/dev-tools/dev-tools.d.ts.map +1 -1
  25. package/dist/dev-tools/dev-tools.js +61 -13
  26. package/dist/dev-tools/dev-tools.js.map +1 -1
  27. package/dist/dev-tools/dev-tools.test.js +166 -0
  28. package/dist/dev-tools/dev-tools.test.js.map +1 -1
  29. package/dist/dev-tools/extension-panel.d.ts.map +1 -1
  30. package/dist/dev-tools/extension-panel.js +30 -7
  31. package/dist/dev-tools/extension-panel.js.map +1 -1
  32. package/dist/dev-tools/protocol.d.ts +49 -1
  33. package/dist/dev-tools/protocol.d.ts.map +1 -1
  34. package/dist/dev-tools/protocol.js +3 -0
  35. package/dist/dev-tools/protocol.js.map +1 -1
  36. package/dist/drivers/index.d.ts +1 -1
  37. package/dist/drivers/index.d.ts.map +1 -1
  38. package/dist/drivers/schema-wire.d.ts.map +1 -1
  39. package/dist/drivers/schema-wire.js +12 -1
  40. package/dist/drivers/schema-wire.js.map +1 -1
  41. package/dist/drivers/schema-wire.test.d.ts +2 -0
  42. package/dist/drivers/schema-wire.test.d.ts.map +1 -0
  43. package/dist/drivers/schema-wire.test.js +31 -0
  44. package/dist/drivers/schema-wire.test.js.map +1 -0
  45. package/dist/drivers/types.d.ts +2 -0
  46. package/dist/drivers/types.d.ts.map +1 -1
  47. package/dist/dsl.d.ts +139 -95
  48. package/dist/dsl.d.ts.map +1 -1
  49. package/dist/dsl.js +64 -8
  50. package/dist/dsl.js.map +1 -1
  51. package/dist/dsl.test.js +78 -8
  52. package/dist/dsl.test.js.map +1 -1
  53. package/dist/index.d.ts +32 -3
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +16 -3
  56. package/dist/index.js.map +1 -1
  57. package/dist/magic-columns.d.ts +3 -1
  58. package/dist/magic-columns.d.ts.map +1 -1
  59. package/dist/magic-columns.js +20 -4
  60. package/dist/magic-columns.js.map +1 -1
  61. package/dist/mcp/build-index.test.js +1 -1
  62. package/dist/migrations.d.ts +126 -0
  63. package/dist/migrations.d.ts.map +1 -0
  64. package/dist/migrations.js +112 -0
  65. package/dist/migrations.js.map +1 -0
  66. package/dist/permissions/index.test.js +35 -0
  67. package/dist/permissions/index.test.js.map +1 -1
  68. package/dist/react-native/create-jazz-client.test.js +62 -42
  69. package/dist/react-native/create-jazz-client.test.js.map +1 -1
  70. package/dist/react-native/jazz-rn-runtime-adapter.d.ts +18 -3
  71. package/dist/react-native/jazz-rn-runtime-adapter.d.ts.map +1 -1
  72. package/dist/react-native/jazz-rn-runtime-adapter.js +110 -6
  73. package/dist/react-native/jazz-rn-runtime-adapter.js.map +1 -1
  74. package/dist/react-native/jazz-rn-runtime-adapter.test.js +149 -4
  75. package/dist/react-native/jazz-rn-runtime-adapter.test.js.map +1 -1
  76. package/dist/reconcile-array.d.ts +29 -0
  77. package/dist/reconcile-array.d.ts.map +1 -0
  78. package/dist/reconcile-array.js +110 -0
  79. package/dist/reconcile-array.js.map +1 -0
  80. package/dist/reconcile-array.test.d.ts +2 -0
  81. package/dist/reconcile-array.test.d.ts.map +1 -0
  82. package/dist/reconcile-array.test.js +118 -0
  83. package/dist/reconcile-array.test.js.map +1 -0
  84. package/dist/runtime/client.d.ts +24 -20
  85. package/dist/runtime/client.d.ts.map +1 -1
  86. package/dist/runtime/client.for-request.test.js +8 -8
  87. package/dist/runtime/client.for-request.test.js.map +1 -1
  88. package/dist/runtime/client.js +58 -25
  89. package/dist/runtime/client.js.map +1 -1
  90. package/dist/runtime/client.mutations.test.js +72 -1
  91. package/dist/runtime/client.mutations.test.js.map +1 -1
  92. package/dist/runtime/cloud-server.integration.test.js +145 -88
  93. package/dist/runtime/cloud-server.integration.test.js.map +1 -1
  94. package/dist/runtime/db.d.ts +3 -7
  95. package/dist/runtime/db.d.ts.map +1 -1
  96. package/dist/runtime/db.js +16 -14
  97. package/dist/runtime/db.js.map +1 -1
  98. package/dist/runtime/db.schema-order.test.js +8 -8
  99. package/dist/runtime/db.schema-order.test.js.map +1 -1
  100. package/dist/runtime/index.d.ts +1 -1
  101. package/dist/runtime/index.d.ts.map +1 -1
  102. package/dist/runtime/index.js +1 -1
  103. package/dist/runtime/index.js.map +1 -1
  104. package/dist/runtime/napi.integration.test.js +113 -136
  105. package/dist/runtime/napi.integration.test.js.map +1 -1
  106. package/dist/runtime/query-adapter.d.ts.map +1 -1
  107. package/dist/runtime/query-adapter.js +22 -2
  108. package/dist/runtime/query-adapter.js.map +1 -1
  109. package/dist/runtime/query-adapter.test.js +81 -5
  110. package/dist/runtime/query-adapter.test.js.map +1 -1
  111. package/dist/runtime/row-transformer.js +2 -2
  112. package/dist/runtime/row-transformer.js.map +1 -1
  113. package/dist/runtime/row-transformer.test.js +9 -9
  114. package/dist/runtime/row-transformer.test.js.map +1 -1
  115. package/dist/runtime/schema-fetch.d.ts +103 -1
  116. package/dist/runtime/schema-fetch.d.ts.map +1 -1
  117. package/dist/runtime/schema-fetch.js +106 -0
  118. package/dist/runtime/schema-fetch.js.map +1 -1
  119. package/dist/runtime/sync-transport.d.ts.map +1 -1
  120. package/dist/runtime/sync-transport.js +15 -0
  121. package/dist/runtime/sync-transport.js.map +1 -1
  122. package/dist/runtime/sync-transport.test.js +33 -0
  123. package/dist/runtime/sync-transport.test.js.map +1 -1
  124. package/dist/runtime/value-converter.d.ts +9 -6
  125. package/dist/runtime/value-converter.d.ts.map +1 -1
  126. package/dist/runtime/value-converter.js +22 -9
  127. package/dist/runtime/value-converter.js.map +1 -1
  128. package/dist/runtime/value-converter.test.js +32 -26
  129. package/dist/runtime/value-converter.test.js.map +1 -1
  130. package/dist/schema-loader.d.ts +14 -0
  131. package/dist/schema-loader.d.ts.map +1 -0
  132. package/dist/schema-loader.js +217 -0
  133. package/dist/schema-loader.js.map +1 -0
  134. package/dist/schema-permissions.d.ts +8 -0
  135. package/dist/schema-permissions.d.ts.map +1 -0
  136. package/dist/schema-permissions.js +266 -0
  137. package/dist/schema-permissions.js.map +1 -0
  138. package/dist/schema-permissions.test.d.ts +2 -0
  139. package/dist/schema-permissions.test.d.ts.map +1 -0
  140. package/dist/schema-permissions.test.js +43 -0
  141. package/dist/schema-permissions.test.js.map +1 -0
  142. package/dist/schema.d.ts +11 -9
  143. package/dist/schema.d.ts.map +1 -1
  144. package/dist/svelte/context.svelte.test.js +50 -0
  145. package/dist/svelte/rune-patterns.svelte.test.js +301 -0
  146. package/dist/svelte/test-helpers.svelte.js +14 -0
  147. package/dist/svelte/use-all.svelte.d.ts.map +1 -1
  148. package/dist/svelte/use-all.svelte.js +7 -1
  149. package/dist/testing/fixtures/basic/schema.d.ts +11 -0
  150. package/dist/testing/fixtures/basic/schema.d.ts.map +1 -0
  151. package/dist/testing/fixtures/basic/schema.js +10 -0
  152. package/dist/testing/fixtures/basic/schema.js.map +1 -0
  153. package/dist/testing/index.d.ts +2 -1
  154. package/dist/testing/index.d.ts.map +1 -1
  155. package/dist/testing/index.js +2 -1
  156. package/dist/testing/index.js.map +1 -1
  157. package/dist/testing/index.test.js +109 -9
  158. package/dist/testing/index.test.js.map +1 -1
  159. package/dist/testing/local-jazz-server.d.ts +2 -0
  160. package/dist/testing/local-jazz-server.d.ts.map +1 -1
  161. package/dist/testing/local-jazz-server.js +21 -51
  162. package/dist/testing/local-jazz-server.js.map +1 -1
  163. package/dist/testing/policy-test-app.d.ts.map +1 -1
  164. package/dist/testing/policy-test-app.js +71 -3
  165. package/dist/testing/policy-test-app.js.map +1 -1
  166. package/dist/typed-app.d.ts +364 -0
  167. package/dist/typed-app.d.ts.map +1 -0
  168. package/dist/{testing/fixtures/basic/app.js → typed-app.js} +118 -30
  169. package/dist/typed-app.js.map +1 -0
  170. package/dist/vue/use-all.d.ts +2 -2
  171. package/dist/vue/use-all.d.ts.map +1 -1
  172. package/dist/vue/use-all.js +9 -3
  173. package/dist/vue/use-all.js.map +1 -1
  174. package/dist/vue/use-all.test.js +137 -0
  175. package/dist/vue/use-all.test.js.map +1 -1
  176. package/package.json +15 -13
  177. package/dist/codegen/codegen.test.d.ts +0 -2
  178. package/dist/codegen/codegen.test.d.ts.map +0 -1
  179. package/dist/codegen/codegen.test.js +0 -1134
  180. package/dist/codegen/codegen.test.js.map +0 -1
  181. package/dist/codegen/index.d.ts +0 -18
  182. package/dist/codegen/index.d.ts.map +0 -1
  183. package/dist/codegen/index.js +0 -22
  184. package/dist/codegen/index.js.map +0 -1
  185. package/dist/codegen/query-builder-generator.d.ts +0 -26
  186. package/dist/codegen/query-builder-generator.d.ts.map +0 -1
  187. package/dist/codegen/query-builder-generator.js +0 -377
  188. package/dist/codegen/query-builder-generator.js.map +0 -1
  189. package/dist/codegen/type-generator.d.ts +0 -30
  190. package/dist/codegen/type-generator.d.ts.map +0 -1
  191. package/dist/codegen/type-generator.js +0 -368
  192. package/dist/codegen/type-generator.js.map +0 -1
  193. package/dist/runtime/napi.fjall.db.all.integration.test.d.ts +0 -2
  194. package/dist/runtime/napi.fjall.db.all.integration.test.d.ts.map +0 -1
  195. package/dist/runtime/napi.fjall.db.all.integration.test.js +0 -76
  196. package/dist/runtime/napi.fjall.db.all.integration.test.js.map +0 -1
  197. package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.d.ts +0 -2
  198. package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.d.ts.map +0 -1
  199. package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.js +0 -47
  200. package/dist/runtime/napi.fjall.db.subscribeAll.integration.test.js.map +0 -1
  201. package/dist/runtime/napi.fjall.test-helpers.d.ts +0 -34
  202. package/dist/runtime/napi.fjall.test-helpers.d.ts.map +0 -1
  203. package/dist/runtime/napi.fjall.test-helpers.js +0 -172
  204. package/dist/runtime/napi.fjall.test-helpers.js.map +0 -1
  205. package/dist/sql-gen.d.ts +0 -5
  206. package/dist/sql-gen.d.ts.map +0 -1
  207. package/dist/sql-gen.js +0 -234
  208. package/dist/sql-gen.js.map +0 -1
  209. package/dist/sql-gen.test.d.ts +0 -2
  210. package/dist/sql-gen.test.d.ts.map +0 -1
  211. package/dist/sql-gen.test.js +0 -481
  212. package/dist/sql-gen.test.js.map +0 -1
  213. package/dist/svelte/context.test.d.ts +0 -2
  214. package/dist/svelte/context.test.d.ts.map +0 -1
  215. package/dist/svelte/context.test.js +0 -55
  216. package/dist/svelte/use-all.test.d.ts +0 -2
  217. package/dist/svelte/use-all.test.d.ts.map +0 -1
  218. package/dist/svelte/use-all.test.js +0 -147
  219. package/dist/testing/fixtures/basic/app.d.ts +0 -59
  220. package/dist/testing/fixtures/basic/app.d.ts.map +0 -1
  221. package/dist/testing/fixtures/basic/app.js.map +0 -1
  222. package/dist/testing/fixtures/basic/current.d.ts +0 -2
  223. package/dist/testing/fixtures/basic/current.d.ts.map +0 -1
  224. package/dist/testing/fixtures/basic/current.js +0 -6
  225. package/dist/testing/fixtures/basic/current.js.map +0 -1
@@ -14,11 +14,6 @@ Browser clients default to anonymous local auth when no JWT/backend auth is conf
14
14
  On first load, Jazz auto-generates a device token and persists it in `localStorage`.
15
15
 
16
16
  ```tsx
17
- const anonymousAuthClient = createJazzClient({
18
- appId: "my-app",
19
- env: "dev",
20
- userBranch: "main",
21
- });
22
17
 
23
18
  return (
24
19
 
@@ -73,11 +68,6 @@ const client = createJazzClient({
73
68
  If you want to force a specific device token, pass `localAuthToken` as an explicit override:
74
69
 
75
70
  ```tsx
76
- const anonymousAuthWithTokenClient = createJazzClient({
77
- appId: "my-app",
78
- localAuthMode: "anonymous",
79
- localAuthToken: "device-token-123",
80
- });
81
71
 
82
72
  return (
83
73
 
@@ -138,12 +128,6 @@ When you want to test multi-user flows locally, switch to `demo` profiles and ex
138
128
  ```tsx
139
129
  const demoAuthAppId = "my-app";
140
130
  const demoAuthActive = getActiveSyntheticAuth(demoAuthAppId, { defaultMode: "demo" });
141
- const demoAuthClient = createJazzClient({
142
- appId: demoAuthAppId,
143
- serverUrl: "http://127.0.0.1:4200",
144
- localAuthMode: demoAuthActive.localAuthMode,
145
- localAuthToken: demoAuthActive.localAuthToken,
146
- });
147
131
 
148
132
  return (
149
133
  <>
@@ -242,15 +226,6 @@ Use `useLinkExternalIdentity` right after sign-in to preserve anon/demo-created
242
226
  const externalAuthAppId = "my-app";
243
227
  const externalAuthServerUrl = "http://127.0.0.1:4200";
244
228
  const externalAuthProviderJwt = "<provider-jwt>";
245
- const externalAuthLocalClient = createJazzClient({
246
- appId: externalAuthAppId,
247
- serverUrl: externalAuthServerUrl,
248
- });
249
- const externalAuthJwtClient = createJazzClient({
250
- appId: externalAuthAppId,
251
- serverUrl: externalAuthServerUrl,
252
- jwtToken: externalAuthProviderJwt,
253
- });
254
229
 
255
230
  const [hasJwt, setHasJwt] = useState(false);
256
231
  const linkExternalIdentity = useLinkExternalIdentity({
@@ -372,6 +347,122 @@ Use `JazzClient.linkExternalIdentity(...)` directly:
372
347
 
373
348
  After linking, external JWT sessions resolve to the same principal.
374
349
 
350
+ ## Use the Current Session in Client Code
351
+
352
+ When you want to attach rows to the current user, or fetch only the rows relevant to that
353
+ user, read the resolved session in your client and derive a user id from it.
354
+
355
+ ### 1. Get the Session and User ID
356
+
357
+ ```tsx
358
+ const session = useSession();
359
+ ```
360
+ ```tsx
361
+ const sessionUserId = session?.user_id ?? null;
362
+ ```
363
+
364
+ ```vue
365
+ const session = useSession();
366
+ ```
367
+ ```vue
368
+ const sessionUserId = session?.user_id ?? null;
369
+ ```
370
+
371
+ ```svelte
372
+ const session = getSession();
373
+ ```
374
+ ```svelte
375
+ const sessionUserId = $derived(session?.user_id ?? null);
376
+ ```
377
+
378
+ ```ts
379
+ const session = await resolveClientSession(config);
380
+ ```
381
+ ```ts
382
+ const sessionUserId = session?.user_id ?? null;
383
+ ```
384
+
385
+ ### 2. Read Rows Relevant to the Current User
386
+
387
+ Use your framework's normal reactive read primitive with the same user-scoped filter.
388
+
389
+ ```tsx
390
+ const ownedTodos =
391
+ useAll(sessionUserId ? app.todos.where({ ownerId: sessionUserId }) : undefined) ?? [];
392
+ ```
393
+
394
+ ```vue
395
+ const ownedTodos = useAll(app.todos.where({ ownerId: sessionUserId ?? "__no-session__" }));
396
+ ```
397
+
398
+ ```svelte
399
+ const ownedTodos = new QuerySubscription(
400
+ app.todos.where({ ownerId: sessionUserId ?? '__no-session__' }),
401
+ );
402
+ ```
403
+
404
+ ```ts
405
+ const ownedTodos = sessionUserId ? await db.all(app.todos.where({ ownerId: sessionUserId })) : [];
406
+ ```
407
+
408
+ ### 3. Insert a User-Owned Row
409
+
410
+ ```tsx
411
+ function addOwnedTodo(title: string) {
412
+ if (!sessionUserId) return;
413
+
414
+ db.insert(app.todos, {
415
+ title,
416
+ done: false,
417
+ ownerId: sessionUserId,
418
+ });
419
+ }
420
+ ```
421
+
422
+ ```vue
423
+ function addOwnedTodo(title: string) {
424
+ if (!sessionUserId) return;
425
+
426
+ db.insert(app.todos, {
427
+ title,
428
+ done: false,
429
+ ownerId: sessionUserId,
430
+ });
431
+ }
432
+ ```
433
+
434
+ ```svelte
435
+ function addOwnedTodo(title: string) {
436
+ if (!sessionUserId) return;
437
+
438
+ db.insert(app.todos, {
439
+ title,
440
+ done: false,
441
+ ownerId: sessionUserId,
442
+ });
443
+ }
444
+ ```
445
+
446
+ ```ts
447
+ function addOwnedTodo(title: string) {
448
+ if (!sessionUserId) return;
449
+
450
+ db.insert(app.todos, {
451
+ title,
452
+ done: false,
453
+ ownerId: sessionUserId,
454
+ });
455
+ }
456
+ ```
457
+
458
+ Use whatever column matches your schema, such as `owner_id`, `author_id`, `assignee_id`, or
459
+ `user_id`. The same pattern works whether a row is strictly owned by one user or simply scoped
460
+ to a user.
461
+
462
+ These client-side filters are useful for UX, but they do not make data private on their own.
463
+ Use [Permissions](/docs/permissions) to define row-level policies that actually enforce which
464
+ sessions can read or mutate a row.
465
+
375
466
  ## Session Resolution Order
376
467
 
377
468
  For `jazz-tools server`:
@@ -408,7 +499,6 @@ jazz-tools server "$JAZZ_APP_ID" \
408
499
  ### Offline-only mode
409
500
 
410
501
  ```tsx
411
- const offlineOnlyAuthClient = createJazzClient({ appId: "my-app" });
412
502
 
413
503
  return (
414
504
 
@@ -450,6 +540,410 @@ const client = createJazzClient({ appId: "my-app" });
450
540
  }
451
541
  ```
452
542
 
543
+ ===PAGE:files-and-blobs===
544
+ TITLE:Files & Blobs
545
+ DESCRIPTION:Chunked browser Blob and stream storage using conventional files and file_parts tables.
546
+
547
+ This page explains how to store images and other binary blobs in Jazz using browser `Blob`,
548
+ `File`, and `ReadableStream` values. These files benefit from the same sync, local-first storage,
549
+ and permission model as the rest of your structured data.
550
+
551
+ Use plain `s.bytes()` when the binary value is small, naturally belongs on the row itself, and
552
+ should always load together with the rest of that row.
553
+
554
+ ## Add the Conventional Tables
555
+
556
+ `db.createFileFromBlob`, `db.createFileFromStream`, `db.loadFileAsBlob`, and
557
+ `db.loadFileAsStream` currently expect these exact table and column names on `app`:
558
+
559
+ ```ts
560
+ file_parts: s.table({
561
+ data: s.bytes(),
562
+ }),
563
+ files: s.table({
564
+ name: s.string().optional(),
565
+ mimeType: s.string(),
566
+ partIds: s.array(s.ref("file_parts")),
567
+ partSizes: s.array(s.int()),
568
+ }),
569
+ uploads: s.table({
570
+ ownerId: s.string(),
571
+ label: s.string(),
572
+ fileId: s.ref("files"),
573
+ }),
574
+ ```
575
+
576
+ `file_parts.data` stores the raw chunk bytes. `files.partIds` keeps the ordered chunk ids, and
577
+ `files.partSizes` stores each chunk's byte length in the same order.
578
+
579
+ In the example above, `uploads.file` is your app-owned reference to a stored file. `files.name`
580
+ is optional metadata; `createFileFromBlob(...)` fills it from `File.name` when available.
581
+
582
+ ## Add Permissions
583
+
584
+ ```ts
585
+
586
+ policy.uploads.allowRead.where({ ownerId: session.user_id });
587
+ policy.uploads.allowInsert.where({ ownerId: session.user_id });
588
+ policy.uploads.allowUpdate.where({ ownerId: session.user_id });
589
+ policy.uploads.allowDelete.where({ ownerId: session.user_id });
590
+
591
+ // Files are created before the parent upload row exists, so inserts are direct for now.
592
+ policy.files.allowInsert.where({});
593
+ policy.file_parts.allowInsert.where({});
594
+
595
+ policy.files.allowRead.where(allowedTo.readReferencing(policy.uploads, "fileId"));
596
+ policy.file_parts.allowRead.where(allowedTo.readReferencing(policy.files, "partIds"));
597
+
598
+ policy.files.allowDelete.where(allowedTo.deleteReferencing(policy.uploads, "fileId"));
599
+ policy.file_parts.allowDelete.where(allowedTo.deleteReferencing(policy.files, "partIds"));
600
+ });
601
+ ```
602
+
603
+ `uploads` owns access. `files` inherit read and delete access from `uploads.file`, and
604
+ `file_parts` inherit from `files.partIds`.
605
+
606
+ `createFileFromBlob(...)` and `createFileFromStream(...)` create `file_parts` and `files` before
607
+ your parent row exists, so insert inheritance does not naturally apply yet. In this MVP, grant
608
+ direct insert access to the clients that may upload, or perform uploads in a trusted backend
609
+ context.
610
+
611
+ Files are write-once in this MVP, so `files` and `file_parts` normally do not need update
612
+ policies.
613
+
614
+ ## Create from a Blob
615
+
616
+ ```ts
617
+
618
+ const file = await db.createFileFromBlob(app, blob, { tier: "edge" });
619
+
620
+ return db.insertDurable(
621
+ app.uploads,
622
+ {
623
+ ownerId: EXAMPLE_OWNER_ID,
624
+ label: "Profile photo",
625
+ fileId: file.id,
626
+ },
627
+ { tier: "edge" },
628
+ );
629
+ }
630
+ ```
631
+
632
+ The helper chunks the blob, creates the `file_parts` rows first, then creates the `files` row,
633
+ and returns that file row so you can store its id on your own table.
634
+
635
+ ## Create from a ReadableStream
636
+
637
+ ```ts
638
+
639
+ const file = await db.createFileFromStream(app, stream, {
640
+ tier: "edge",
641
+ name: "camera.raw",
642
+ mimeType: "application/octet-stream",
643
+ });
644
+
645
+ return db.insertDurable(
646
+ app.uploads,
647
+ {
648
+ ownerId: EXAMPLE_OWNER_ID,
649
+ label: "Camera import",
650
+ fileId: file.id,
651
+ },
652
+ { tier: "edge" },
653
+ );
654
+ }
655
+ ```
656
+
657
+ ## Load as a Blob
658
+
659
+ ```ts
660
+
661
+ const upload = await db.one(app.uploads.where({ id: uploadId }), { tier: "edge" });
662
+ if (!upload) {
663
+ return null;
664
+ }
665
+
666
+ const blob = await db.loadFileAsBlob(app, upload.fileId, { tier: "edge" });
667
+ return blob;
668
+ }
669
+ ```
670
+
671
+ ## Load as a ReadableStream
672
+
673
+ ```ts
674
+
675
+ const upload = await db.one(app.uploads.where({ id: uploadId }), { tier: "edge" });
676
+ if (!upload) {
677
+ return null;
678
+ }
679
+
680
+ const stream = await db.loadFileAsStream(app, upload.fileId, { tier: "edge" });
681
+ return stream;
682
+ }
683
+ ```
684
+
685
+ `loadFileAsBlob(...)` and `loadFileAsStream(...)` first fetch the `files` row without includes,
686
+ then query each referenced `file_parts` row one at a time. That keeps loading naive and
687
+ sequential instead of eager-loading every chunk into one query.
688
+
689
+ ## Durability and Incomplete Local Data
690
+
691
+ The file helpers forward normal Jazz durability options:
692
+
693
+ - Pass `tier` to `createFileFromBlob(...)` or `createFileFromStream(...)`.
694
+ - Pass query options like `{ tier: "edge" }` to `loadFileAsBlob(...)` or `loadFileAsStream(...)`.
695
+
696
+ If the requested tier does not have the full file yet, `loadFileAsBlob(...)` fails the whole
697
+ read and `loadFileAsStream(...)` errors when it reaches the missing part. Use `edge` or `global`
698
+ when you need the read to wait for a more complete remote snapshot than local OPFS currently has.
699
+
700
+ ## No Automatic Cascade Yet
701
+
702
+ Until file cascade delete lands, delete the chunk rows and the `files` row before deleting your
703
+ parent app row so the inherited delete policies still match:
704
+
705
+ ```ts
706
+
707
+ const upload = await db.one(app.uploads.where({ id: uploadId }), { tier: "edge" });
708
+ if (!upload) {
709
+ return;
710
+ }
711
+
712
+ const file = await db.one(app.files.where({ id: upload.fileId }), { tier: "edge" });
713
+
714
+ if (file) {
715
+ // Delete chunks and the file while the parent upload row still exists.
716
+ for (const partId of file.partIds) {
717
+ db.delete(app.file_parts, partId);
718
+ }
719
+ db.delete(app.files, file.id);
720
+ }
721
+
722
+ db.delete(app.uploads, uploadId);
723
+ }
724
+ ```
725
+
726
+ ===PAGE:guides/framework-patterns===
727
+ TITLE:Framework Patterns
728
+ DESCRIPTION:Side-by-side reference for React, Vue, and Svelte Jazz APIs — subscriptions, database access, and dependency injection.
729
+
730
+ ## API equivalents
731
+
732
+ | Concept | React / Expo | Vue | Svelte |
733
+ | --------------- | -------------------------------- | --------------------------------- | ------------------------------- |
734
+ | Provider | `` | `` | `` |
735
+ | Live query | `useAll(query)` | `useAll(query)` | `new QuerySubscription(query)` |
736
+ | DB access | `useDb()` | `useDb()` | `getDb()` |
737
+ | Session | `useSession()` | `useSession()` | `getSession()` |
738
+ | Client creation | `createJazzClient(config)` | `createJazzClient(config)` | `createJazzClient(config)` |
739
+
740
+ ## Provider setup
741
+
742
+ All frameworks wrap the app in a provider that makes the database available to every
743
+ component in the tree — no prop drilling required.
744
+
745
+ ```tsx
746
+
747
+ return (
748
+ Loading...</p>}>
749
+
750
+ );
751
+ }
752
+ ````
753
+
754
+ ```vue
755
+ <script setup lang="ts">
756
+
757
+ const client = createJazzClient(config);
758
+
759
+ </script>
760
+
761
+ <template>
762
+
763
+ <template #fallback>
764
+ <p>Loading...</p>
765
+ </template>
766
+
767
+ </template>
768
+ ````
769
+
770
+ ```svelte
771
+ <script lang="ts">
772
+ import { createJazzClient, JazzSvelteProvider } from "jazz-tools/svelte";
773
+ const client = createJazzClient(config);
774
+ </script>
775
+
776
+ ```
777
+
778
+ ## Live queries
779
+
780
+ React and Vue use composition hooks; Svelte uses a class with a reactive `.current` property.
781
+
782
+ ```tsx
783
+ const tasks = useAll(app.tasks.where({ done: false }));
784
+
785
+ // undefined = not yet connected; [] = connected, no rows; [...] = rows present
786
+ if (tasks === undefined) return ;
787
+
788
+ ````
789
+
790
+ ```vue
791
+ <script setup lang="ts">
792
+
793
+ const tasks = useAll(app.tasks.where({ done: false }));
794
+ // undefined = not yet connected; [] = connected, no rows; [...] = rows present
795
+ </script>
796
+
797
+ <template>
798
+
799
+ </template>
800
+ ````
801
+
802
+ ```svelte
803
+ <script lang="ts">
804
+ import { QuerySubscription } from "jazz-tools/svelte";
805
+ const tasks = new QuerySubscription(
806
+ app.tasks.where({ done: false }),
807
+ );
808
+ </script>
809
+
810
+ {#each tasks.current ?? [] as task}
811
+
812
+ {/each}
813
+ ```
814
+
815
+ The `?? []` guard handles the `undefined` (not yet connected) case. See
816
+ [Reading Data: First delivery and the undefined signal](/docs/reading-data#first-delivery-and-the-undefined-signal) for patterns that depend on this signal.
817
+
818
+ In Vue, `useAll` returns a `ShallowRef` — it unwraps automatically in templates, but in
819
+ `<script setup>` you would access it as `tasks.value`.
820
+
821
+ ## Accessing the database for writes
822
+
823
+ ```tsx
824
+ // Must be called at component top level (rules of hooks)
825
+
826
+ const db = useDb();
827
+
828
+ await db.insert(app.messages, { text, createdAt: Date.now() });
829
+
830
+ ````
831
+
832
+ ```ts
833
+ // Must be called inside setup() or <script setup>
834
+
835
+ const db = useDb();
836
+
837
+ await db.insert(app.messages, { text, createdAt: Date.now() });
838
+ ````
839
+
840
+ ```ts
841
+ // Callable anywhere — component, store, or utility module
842
+
843
+ const db = getDb();
844
+
845
+ await db.insert(app.messages, { text, createdAt: Date.now() });
846
+
847
+ ````
848
+
849
+ `getDb()` in Svelte can be called outside of component lifecycle — useful in event handlers,
850
+ class methods, and utility modules. `useDb()` in React and Vue must follow the rules of hooks.
851
+
852
+ ## Session / user identity
853
+
854
+ ```tsx
855
+
856
+ const session = useSession(); // { user_id: string } | null
857
+ ````
858
+
859
+ ```ts
860
+
861
+ const session = useSession(); // { user_id: string } | null
862
+ ```
863
+
864
+ ```ts
865
+
866
+ const session = getSession(); // { user_id: string } | null
867
+ ```
868
+
869
+ ===PAGE:guides/where-operators===
870
+ TITLE:WHERE Operators
871
+ DESCRIPTION:Full reference for filter operators available in Jazz query builders, with examples for each column kind.
872
+
873
+ ## Summary
874
+
875
+ ## Operator reference by column type
876
+
877
+ ## Examples
878
+
879
+ ### Equality and inequality
880
+
881
+ ```ts
882
+ // Exact match (shorthand — no operator object needed)
883
+ db.all(app.orders.where({ status: "pending" }));
884
+
885
+ // Not equal
886
+ db.all(app.users.where({ id: { ne: currentUserId } }));
887
+
888
+ // One of a set
889
+ db.all(app.orders.where({ status: { in: ["pending", "processing"] } }));
890
+ ```
891
+
892
+ ### Numeric comparisons
893
+
894
+ ```ts
895
+ // Greater than / less than
896
+ db.all(app.sessions.where({ lastSeen: { gt: staleCutoff } }));
897
+ db.all(app.orders.where({ total: { gte: 1000 } }));
898
+ db.all(app.products.where({ stock: { lt: 10 } }));
899
+ ```
900
+
901
+ ### String contains
902
+
903
+ ```ts
904
+ // Substring match (case-sensitive)
905
+ db.all(app.messages.where({ text: { contains: searchTerm } }));
906
+ ```
907
+
908
+ ### Null checks on optional references
909
+
910
+ ```ts
911
+ // Rows where the optional ref is not set
912
+ db.all(app.tasks.where({ assignedTo: { isNull: true } }));
913
+
914
+ // Rows where it is set
915
+ db.all(app.tasks.where({ assignedTo: { isNull: false } }));
916
+ ```
917
+
918
+ ### Multiple conditions (AND)
919
+
920
+ All predicates passed to `where(...)` are AND-combined:
921
+
922
+ ```ts
923
+ // done AND assigned to this user
924
+ db.all(
925
+ app.tasks.where({
926
+ done: true,
927
+ assignedTo: userId,
928
+ }),
929
+ );
930
+ ```
931
+
932
+ ### Combining with ordering and limits
933
+
934
+ ```ts
935
+ db.all(app.messages.where({ threadId }).orderBy("createdAt", "asc").limit(50));
936
+ ```
937
+
938
+ ## Live subscriptions with WHERE
939
+
940
+ `useAll` and `QuerySubscription` accept the same query builders as `db.all`. The subscription
941
+ stays active and re-delivers results whenever any row enters or exits the filter:
942
+
943
+ ```ts
944
+ const pending = useAll(app.tasks.where({ done: false }));
945
+ ```
946
+
453
947
  ===PAGE:index===
454
948
  TITLE:Overview
455
949
  DESCRIPTION:The database that syncs.
@@ -550,79 +1044,166 @@ Slugs match the URL path under `/docs/` — for example `schemas`, `reading-data
550
1044
 
551
1045
  ===PAGE:migrations===
552
1046
  TITLE:Migrations
553
- DESCRIPTION:Schema evolution workflow and generated migration stubs.
1047
+ DESCRIPTION:Review and publish typed compatibility edges between schema hashes.
554
1048
 
555
1049
  If you are trying to get your first app running, you can skip this page and return later.
556
1050
 
557
- ## Traditional Migrations vs Jazz Lenses
1051
+ ## Why Jazz Migrations Are Different
1052
+
1053
+ - Traditional migration systems run a linear sequence and require peers to converge before older versions stop writing.
1054
+ - Jazz migration lenses are applied at read/write time, so mixed-version clients can keep operating while data is translated between schema hashes.
1055
+ - Schemas still auto-publish on connect. Migrations do not: they are always reviewed and pushed explicitly.
558
1056
 
559
- - Traditional migration systems run a linear sequence and require all peers to converge before old versions stop writing.
560
- - Jazz migration lenses are evaluated at read/write time, so mixed-version clients can continue operating while data is translated across schema versions.
561
- - This enables incremental rollouts where older and newer app versions overlap safely during migration windows.
1057
+ ## Workflow
562
1058
 
563
- ## CLI Workflow
1059
+ All Jazz apps use the same migration workflow, including Rust backends.
564
1060
 
565
1061
  ```bash
566
1062
  #!/usr/bin/env bash
567
1063
 
568
- # 1) Edit schema/current.ts.
569
- # 2) Generate migration stubs from the updated schema.
570
- npx jazz-tools@alpha build
1064
+ # 1) Edit schema.ts during development and watch for Jazz migration warnings.
1065
+
1066
+ # 2) Generate a typed migration stub after Jazz reports the old/new hashes.
1067
+ npx jazz-tools@alpha migrations create <fromHash> <toHash>
1068
+
1069
+ # 3) Fill in migrate, rename the file, then publish the reviewed edge.
1070
+ npx jazz-tools@alpha migrations push <fromHash> <toHash>
571
1071
 
572
1072
  ```
573
1073
 
574
1074
  ```bash
575
1075
  #!/usr/bin/env bash
576
1076
 
577
- # 1) Edit schema/current.sql.
578
- # 2) Generate migration stubs from the updated schema.
579
- npx jazz-tools@alpha build
1077
+ # 1) Edit schema.ts during development and watch for Jazz migration warnings.
1078
+
1079
+ # 2) Generate a typed migration stub after Jazz reports the old/new hashes.
1080
+ npx jazz-tools@alpha migrations create <fromHash> <toHash>
1081
+
1082
+ # 3) Fill in migrate, rename the file, then publish the reviewed edge.
1083
+ npx jazz-tools@alpha migrations push <fromHash> <toHash>
580
1084
 
581
1085
  ```
582
1086
 
583
- For TypeScript DSL migrations, one generated file can define both forward and backward behavior in the same stub.
584
- For SQL migrations, keep explicit forward/backward files and edit each direction intentionally.
1087
+ When Jazz encounters rows whose schema hash is no longer reachable from the current app schema, it logs a warning with the exact short-hash `migrations create <fromHash> <toHash>` command to run next.
585
1088
 
586
- ## Generated Stubs
1089
+ ## Generated Stub
587
1090
 
588
- ```ts
1091
+ `migrations create` writes a copy-pastable file with short `fromHash`, `toHash`, and minimal typed `from` / `to` witnesses. You only need to fill in `migrate`, then rename the file.
1092
+
1093
+ ```ts
589
1094
 
590
- migrate("todos", {
591
- description: col.add().string({ default: "" }),
1095
+ migrate: {
1096
+ todos: {
1097
+ description: s.add.string({ default: null }),
1098
+ },
1099
+ },
1100
+ fromHash: "a01f5c72ec47",
1101
+ toHash: "311995e9a178",
1102
+ from: {
1103
+ todos: s.table({
1104
+ title: s.string(),
1105
+ done: s.boolean(),
1106
+ parentId: s.ref("todos").optional(),
1107
+ projectId: s.ref("projects").optional(),
1108
+ owner_id: s.string(),
1109
+ }),
1110
+ },
1111
+ to: {
1112
+ todos: s.table({
1113
+ title: s.string(),
1114
+ done: s.boolean(),
1115
+ description: s.string().optional(),
1116
+ parentId: s.ref("todos").optional(),
1117
+ projectId: s.ref("projects").optional(),
1118
+ owner_id: s.string(),
1119
+ }),
1120
+ },
592
1121
  });
593
1122
 
594
1123
  ```
595
1124
 
596
- Forward:
597
- ```sql
598
- ALTER TABLE todos ADD COLUMN description TEXT DEFAULT '';
599
- ```
600
- Backward:
601
- ```sql
602
- ALTER TABLE todos DROP COLUMN description;
603
- ```
1125
+ ## Edited Stub
604
1126
 
605
- ## Edited Stubs
1127
+ After review, keep the same hashes and adjust the `migrate` object as needed:
606
1128
 
607
- ```ts
1129
+ ```ts
608
1130
 
609
1131
  // Example of editing a generated migration stub.
610
- migrate("todos", {
611
- description: col.add().string({ default: "No description" }),
1132
+
1133
+ migrate: {
1134
+ todos: {
1135
+ description: s.add.string({ default: "No description" }),
1136
+ },
1137
+ },
1138
+ fromHash: "a01f5c72ec47",
1139
+ toHash: "311995e9a178",
1140
+ from: {
1141
+ todos: s.table({
1142
+ title: s.string(),
1143
+ done: s.boolean(),
1144
+ parentId: s.ref("todos").optional(),
1145
+ projectId: s.ref("projects").optional(),
1146
+ owner_id: s.string(),
1147
+ }),
1148
+ },
1149
+ to: {
1150
+ todos: s.table({
1151
+ title: s.string(),
1152
+ done: s.boolean(),
1153
+ description: s.string().optional(),
1154
+ parentId: s.ref("todos").optional(),
1155
+ projectId: s.ref("projects").optional(),
1156
+ owner_id: s.string(),
1157
+ }),
1158
+ },
612
1159
  });
613
1160
 
614
1161
  ```
615
1162
 
616
- Forward:
617
- ```sql
618
- -- Example of editing a generated migration stub.
619
- ALTER TABLE todos ADD COLUMN description TEXT DEFAULT 'No description';
1163
+ ## Backwards Defaults
1164
+
1165
+ When a newer schema drops a column that older clients still expect, define a backwards default in the migration edge:
1166
+
1167
+ ```ts
1168
+
1169
+ // Example: dropping a column with a backwards default.
1170
+ // Clients still on the older schema continue seeing legacy_priority.
1171
+
1172
+ migrate: {
1173
+ todos: {
1174
+ legacy_priority: s.drop.int({ backwardsDefault: 0 }),
1175
+ },
1176
+ },
1177
+ fromHash: "311995e9a178",
1178
+ toHash: "73b65d082ab8",
1179
+ from: {
1180
+ todos: s.table({
1181
+ title: s.string(),
1182
+ done: s.boolean(),
1183
+ description: s.string().optional(),
1184
+ parentId: s.ref("todos").optional(),
1185
+ projectId: s.ref("projects").optional(),
1186
+ owner_id: s.string(),
1187
+ legacy_priority: s.int(),
1188
+ }),
1189
+ },
1190
+ to: {
1191
+ todos: s.table({
1192
+ title: s.string(),
1193
+ done: s.boolean(),
1194
+ description: s.string().optional(),
1195
+ parentId: s.ref("todos").optional(),
1196
+ projectId: s.ref("projects").optional(),
1197
+ owner_id: s.string(),
1198
+ }),
1199
+ },
1200
+ });
620
1201
 
621
1202
  ```
622
- Backward:
623
- ```sql
624
- ALTER TABLE todos DROP COLUMN description;
625
- ```
1203
+
1204
+ ## Rust Apps Use the Same Files
1205
+
1206
+ Rust services still author and ship TypeScript migrations. The Rust runtime consumes the compiled internal schema representation through the CLI and low-level APIs; it does not maintain a separate SQL migration format.
626
1207
 
627
1208
  ===PAGE:permissions===
628
1209
  TITLE:Permissions
@@ -630,33 +1211,33 @@ DESCRIPTION:Fast RLS (ReBAC), simple policies, combinators, and inherited access
630
1211
 
631
1212
  ## Schema Policy Syntax
632
1213
 
633
- Define policies in `schema/permissions.ts` for TypeScript DSL apps, or directly in SQL.
634
-
635
- Assuming the following schema:
636
-
637
- ```ts
638
- table("projects", {
639
- name: col.string(),
640
- ownerId: col.string(),
641
- });
642
-
643
- table("todos", {
644
- title: col.string(),
645
- done: col.boolean(),
646
- project: col.ref("projects").optional(),
647
- ownerId: col.string(),
648
- });
1214
+ Define policies in a root `permissions.ts` next to `schema.ts`. This applies to every Jazz app, including Rust backends that consume the compiled schema via the CLI.
649
1215
 
650
- table("todoShares", {
651
- todo: col.ref("todos"),
652
- user_id: col.string(),
653
- can_read: col.boolean(),
654
- });
655
- ```
1216
+ Assuming the following schema:
656
1217
 
657
- Define permissions in `schema/permissions.ts`:
1218
+ ```ts
1219
+ const schema = {
1220
+ projects: s.table({
1221
+ name: s.string(),
1222
+ ownerId: s.string(),
1223
+ }),
1224
+ todos: s.table({
1225
+ title: s.string(),
1226
+ done: s.boolean(),
1227
+ projectId: s.ref("projects").optional(),
1228
+ owner_id: s.string(),
1229
+ }),
1230
+ todoShares: s.table({
1231
+ todoId: s.ref("todos"),
1232
+ userId: s.string(),
1233
+ can_read: s.boolean(),
1234
+ }),
1235
+ };
1236
+ ```
1237
+
1238
+ Define permissions in `permissions.ts`:
658
1239
 
659
- ```ts
1240
+ ```ts
660
1241
 
661
1242
  policy.todos.allowRead.where({ owner_id: session.user_id });
662
1243
  policy.todos.allowInsert.where({ owner_id: session.user_id });
@@ -666,32 +1247,12 @@ table("todoShares", {
666
1247
 
667
1248
  ```
668
1249
 
669
- ```sql
670
- CREATE TABLE projects (
671
- name TEXT NOT NULL
672
- );
673
-
674
- CREATE TABLE todos (
675
- title TEXT NOT NULL,
676
- done BOOLEAN NOT NULL,
677
- description TEXT,
678
- parent UUID REFERENCES todos,
679
- project UUID REFERENCES projects,
680
- owner_id TEXT NOT NULL
681
- );
682
- CREATE POLICY todos_select_policy ON todos FOR SELECT USING (owner_id = @session.user_id);
683
- CREATE POLICY todos_insert_policy ON todos FOR INSERT WITH CHECK (owner_id = @session.user_id);
684
- CREATE POLICY todos_update_policy ON todos FOR UPDATE USING (owner_id = @session.user_id) WITH CHECK (owner_id = @session.user_id);
685
- CREATE POLICY todos_delete_policy ON todos FOR DELETE USING (owner_id = @session.user_id);
686
-
687
- ```
688
-
689
- Apps that do not need user-scoped filtering can still define explicit allow-all policies (`policy.todos.allowRead.where({})` / `USING (TRUE)`).
1250
+ Apps that do not need user-scoped filtering can still define explicit allow-all policies (`policy.todos.allowRead.always()` or `policy.todos.allowRead.where({})`).
690
1251
 
691
1252
  ## Simple Policies
692
1253
 
693
- ```ts
694
- definePermissions(app, ({ policy, allOf, session }) => {
1254
+ ```ts
1255
+ s.definePermissions(app, ({ policy, allOf, session }) => {
695
1256
  policy.todos.allowRead.where({ owner_id: session.user_id });
696
1257
  policy.todos.allowInsert.where({ owner_id: session.user_id });
697
1258
  policy.todos.allowUpdate
@@ -701,25 +1262,36 @@ definePermissions(app, ({ policy, allOf, session }) => {
701
1262
  });
702
1263
  ```
703
1264
 
704
- ```sql
705
- -- #region permissions-simple-sql
706
- CREATE POLICY todos_select_policy ON todos FOR SELECT USING (owner_id = @session.user_id);
707
- CREATE POLICY todos_insert_policy ON todos FOR INSERT WITH CHECK (owner_id = @session.user_id);
708
- CREATE POLICY todos_update_policy ON todos FOR UPDATE USING ((owner_id = @session.user_id) AND (done = FALSE)) WITH CHECK (owner_id = @session.user_id);
709
- CREATE POLICY todos_delete_policy ON todos FOR DELETE USING (owner_id = @session.user_id);
710
- -- #endregion permissions-simple-sql
1265
+ ## Explicitly Allow an Operation (`.always()`)
711
1266
 
712
- -- #region permissions-inherits-sql
713
- CREATE POLICY todos_select_policy ON todos FOR SELECT USING ((done = FALSE) OR (INHERITS SELECT VIA project));
714
- CREATE POLICY todos_update_policy ON todos FOR UPDATE USING ((INHERITS UPDATE VIA project) AND (done = FALSE)) WITH CHECK (INHERITS UPDATE VIA project);
715
- -- #endregion permissions-inherits-sql
1267
+ Use `.always()` when an operation should always be permitted. It is equivalent to `.where({})`.
716
1268
 
1269
+ ```ts
1270
+ s.definePermissions(app, ({ policy }) => {
1271
+ policy.todos.allowRead.always();
1272
+ policy.todos.allowInsert.always();
1273
+ policy.todos.allowUpdate.always();
1274
+ policy.todos.allowDelete.always();
1275
+ });
1276
+ ```
1277
+
1278
+ ## Explicitly Deny an Operation (`.never()`)
1279
+
1280
+ Use `.never()` when an operation should be impossible. It is equivalent to `.where(anyOf([]))`.
1281
+
1282
+ ```ts
1283
+ s.definePermissions(app, ({ policy }) => {
1284
+ policy.todos.allowRead.never();
1285
+ policy.todos.allowInsert.never();
1286
+ policy.todos.allowUpdate.never();
1287
+ policy.todos.allowDelete.never();
1288
+ });
717
1289
  ```
718
1290
 
719
1291
  ## Combining Conditions (`allOf` / `anyOf`)
720
1292
 
721
1293
  ```ts
722
- definePermissions(app, ({ policy, allOf, anyOf, allowedTo, session }) => {
1294
+ s.definePermissions(app, ({ policy, allOf, anyOf, allowedTo, session }) => {
723
1295
  policy.todos.allowRead.where(
724
1296
  anyOf([{ owner_id: session.user_id }, allOf([{ done: false }, allowedTo.read("project")])]),
725
1297
  );
@@ -740,32 +1312,29 @@ pub fn combinator_policy() -> TablePolicies {
740
1312
 
741
1313
  `allOf([...])` compiles to logical `AND`, and `anyOf([...])` compiles to logical `OR`.
742
1314
 
1315
+ ## JWT Session Claims
1316
+
1317
+ When external auth JWTs carry claims, `session.where(...)` lets you check them directly in permissions without mapping them onto row columns first.
1318
+
1319
+ ```ts
1320
+ s.definePermissions(app, ({ policy, anyOf, session }) => {
1321
+ policy.todos.allowRead.where(
1322
+ anyOf([{ owner_id: session.user_id }, session.where({ "claims.role": "manager" })]),
1323
+ );
1324
+ });
1325
+ ```
1326
+
743
1327
  ## Inherited Access Policies (`allowedTo.*`)
744
1328
 
745
- TypeScript DSL exposes inherited access via `allowedTo.read/insert/update/delete(...)`, which compiles to `INHERITS` policy expressions.
1329
+ `allowedTo.read/insert/update/delete(...)` lets one table inherit access from another through relationships.
746
1330
 
747
- ```ts
748
- definePermissions(app, ({ policy, anyOf, allOf, allowedTo }) => {
1331
+ ```ts
1332
+ s.definePermissions(app, ({ policy, anyOf, allOf, allowedTo }) => {
749
1333
  policy.todos.allowRead.where(anyOf([{ done: false }, allowedTo.read("project")]));
750
1334
  policy.todos.allowUpdate
751
1335
  .whereOld(allOf([allowedTo.update("project"), { done: false }]))
752
1336
  .whereNew(allowedTo.update("project"));
753
1337
  });
754
- ```
755
-
756
- ```sql
757
- -- #region permissions-simple-sql
758
- CREATE POLICY todos_select_policy ON todos FOR SELECT USING (owner_id = @session.user_id);
759
- CREATE POLICY todos_insert_policy ON todos FOR INSERT WITH CHECK (owner_id = @session.user_id);
760
- CREATE POLICY todos_update_policy ON todos FOR UPDATE USING ((owner_id = @session.user_id) AND (done = FALSE)) WITH CHECK (owner_id = @session.user_id);
761
- CREATE POLICY todos_delete_policy ON todos FOR DELETE USING (owner_id = @session.user_id);
762
- -- #endregion permissions-simple-sql
763
-
764
- -- #region permissions-inherits-sql
765
- CREATE POLICY todos_select_policy ON todos FOR SELECT USING ((done = FALSE) OR (INHERITS SELECT VIA project));
766
- CREATE POLICY todos_update_policy ON todos FOR UPDATE USING ((INHERITS UPDATE VIA project) AND (done = FALSE)) WITH CHECK (INHERITS UPDATE VIA project);
767
- -- #endregion permissions-inherits-sql
768
-
769
1338
  ```
770
1339
 
771
1340
  ## Advanced: Recursive INHERITS
@@ -773,7 +1342,7 @@ CREATE POLICY todos_update_policy ON todos FOR UPDATE USING ((INHERITS UPDATE VI
773
1342
  Use depth-bounded inherited access when foreign keys can form recursive chains.
774
1343
 
775
1344
  ```ts
776
- definePermissions(app, ({ policy, allowedTo }) => {
1345
+ s.definePermissions(app, ({ policy, allowedTo }) => {
777
1346
  policy.todos.allowRead.where(allowedTo.read("parent"));
778
1347
  policy.todos.allowUpdate
779
1348
  .whereOld(allowedTo.update("parent", { maxDepth: 5 }))
@@ -794,16 +1363,14 @@ pub fn recursive_inherits_policy() -> TablePolicies {
794
1363
 
795
1364
  ## Policy Enforcement and Request Context
796
1365
 
797
- Jazz permissions use relationship-based policy evaluation with requester session context.
798
- Backend endpoints should scope a client/session per request before reads and writes.
799
- Frontend clients are evaluated with the auth/session identity used to initialize context.
1366
+ Jazz permissions use relationship-based policy evaluation with requester session context. Backend endpoints should scope a client/session per request before reads and writes.
800
1367
 
801
1368
  ```ts
802
1369
 
803
1370
  try {
804
1371
  const rows = await context
805
1372
  .forRequest(req, schemaApp)
806
- .query(schemaApp.todos.where({ done: true }));
1373
+ .all(schemaApp.todos.where({ done: true }));
807
1374
  res.json(rows);
808
1375
  } catch {
809
1376
  sendQueryError(res);
@@ -840,27 +1407,80 @@ Install dependencies in your Expo app:
840
1407
 
841
1408
  For full environment and server setup options, see [Setup](/docs/setup).
842
1409
 
1410
+ ## Project configuration
1411
+
1412
+ ### Polyfills
1413
+
1414
+ Jazz requires a few global polyfills for networking and streams on React Native. Import them as the very first line in your entry point (`index.js`), before any other code:
1415
+
1416
+ ```js
1417
+
1418
+ registerRootComponent(App);
1419
+
1420
+ ```
1421
+
1422
+ ### Babel
1423
+
1424
+ Create a `babel.config.cjs` with the Expo preset and `import.meta` transform enabled:
1425
+
1426
+ ```js
1427
+ module.exports = function (api) {
1428
+ api.cache(true);
1429
+ return {
1430
+ presets: [["babel-preset-expo", { unstable_transformImportMeta: true }]],
1431
+ };
1432
+ };
1433
+
1434
+ ```
1435
+
1436
+ ### Metro
1437
+
1438
+ Create a `metro.config.js` that extends Expo's defaults. If you use **pnpm**, enable symlink support:
1439
+
1440
+ ```js
1441
+
1442
+ const require = createRequire(import.meta.url);
1443
+ const { getDefaultConfig } = require("expo/metro-config");
1444
+
1445
+ const __filename = fileURLToPath(import.meta.url);
1446
+ const projectRoot = path.dirname(__filename);
1447
+
1448
+ const config = getDefaultConfig(projectRoot);
1449
+
1450
+ // So Metro uses our Babel config (babel.config.cjs); it doesn't auto-detect .cjs
1451
+ config.transformer = config.transformer || {};
1452
+ config.transformer.extendsBabelConfigPath = path.resolve(projectRoot, "babel.config.cjs");
1453
+
1454
+ // pnpm uses symlinks for hoisted packages
1455
+ config.resolver.unstable_enableSymlinks = true;
1456
+
1457
+ ```
1458
+
843
1459
  ## Define your schema
844
1460
 
845
- Create `schema/current.ts` at the root of your project. This is the source of truth for your data model.
1461
+ Create `schema.ts` at the root of your project. This is the source of truth for your data model.
846
1462
 
847
1463
  ```ts
848
1464
 
849
- table("projects", {
850
- name: col.string(),
851
- });
1465
+ const schema = {
1466
+ projects: s.table({
1467
+ name: s.string(),
1468
+ }),
1469
+ todos: s.table({
1470
+ title: s.string(),
1471
+ done: s.boolean(),
1472
+ description: s.string().optional(),
1473
+ ownerId: s.string(),
1474
+ parentId: s.ref("todos").optional(),
1475
+ projectId: s.ref("projects").optional(),
1476
+ }),
1477
+ };
1478
+
1479
+ type AppSchema = s.Schema<typeof schema>;
852
1480
 
853
- table("todos", {
854
- title: col.string(),
855
- done: col.boolean(),
856
- description: col.string().optional(),
857
- ownerId: col.string(),
858
- parent: col.ref("todos").optional(),
859
- project: col.ref("projects").optional(),
860
- });
861
1481
  ```
862
1482
 
863
- ### Run codegen
1483
+ ### Validate schema
864
1484
 
865
1485
  Deep dive: [Schemas](/docs/schemas).
866
1486
 
@@ -869,12 +1489,6 @@ Deep dive: [Schemas](/docs/schemas).
869
1489
  `JazzProvider` initializes the Jazz runtime and makes the database available to descendant components via React context.
870
1490
 
871
1491
  ```tsx
872
- const client = createJazzClient({
873
- appId: "00000000-0000-0000-0000-000000000002",
874
- serverUrl: "http://10.0.2.2:1625",
875
- localAuthMode: "demo",
876
- // jwtToken: authToken, // Use this (instead of localAuthMode) for external auth.
877
- });
878
1492
 
879
1493
  return (
880
1494
 
@@ -917,7 +1531,7 @@ Query builders are fluent and immutable. Chain `.where()`, `.orderBy()`, `.limit
917
1531
  }
918
1532
  ```
919
1533
 
920
- By default `useAll` returns results immediately from the local replica. Use durability options when you need explicit confirmation boundaries.
1534
+ By default `useAll` returns results immediately from the local replica. Use query options when you need explicit confirmation boundaries.
921
1535
 
922
1536
  Deep dive: [Reading Data](/docs/reading-data).
923
1537
 
@@ -926,19 +1540,19 @@ Deep dive: [Reading Data](/docs/reading-data).
926
1540
  `useDb` returns the `Db` handle for mutations. Mutations apply locally first and return promises that resolve at the selected durability tier.
927
1541
 
928
1542
  ```tsx
929
- const addTodo = async () => {
1543
+ const addTodo = () => {
930
1544
  const trimmed = title.trim();
931
1545
  if (!trimmed || !sessionUserId) return;
932
- await db.insert(app.todos, { title: trimmed, done: false, owner_id: sessionUserId });
1546
+ db.insert(app.todos, { title: trimmed, done: false, ownerId: sessionUserId });
933
1547
  setTitle("");
934
1548
  };
935
1549
 
936
- const toggleTodo = async (todo: Todo) => {
937
- await db.update(app.todos, todo.id, { done: !todo.done });
1550
+ const toggleTodo = (todo: Todo) => {
1551
+ db.update(app.todos, todo.id, { done: !todo.done });
938
1552
  };
939
1553
 
940
- const removeTodo = async (id: string) => {
941
- await db.delete(app.todos, id);
1554
+ const removeTodo = (id: string) => {
1555
+ db.delete(app.todos, id);
942
1556
  };
943
1557
  ```
944
1558
 
@@ -953,7 +1567,7 @@ Mutation API details: [Writing Data](/docs/writing-data).
953
1567
 
954
1568
  ### Reading with a durability tier
955
1569
 
956
- Use `db.subscribeAll` (or `db.all`) with a durability tier when you need tier-gated delivery.
1570
+ Inside components, use `useAll(query, options)` with the same `QueryOptions` as `db.subscribeAll`. Outside components, use `db.subscribeAll` (or `db.all`) directly when you need tier-gated delivery or deltas.
957
1571
 
958
1572
  ```ts
959
1573
 
@@ -975,8 +1589,8 @@ Use durable mutation APIs with `{ tier }` when you need explicit write durabilit
975
1589
  {
976
1590
  title: todoTitle,
977
1591
  done: false,
978
- owner_id: EXAMPLE_OWNER_ID,
979
- project: EXAMPLE_PROJECT_ID,
1592
+ ownerId: EXAMPLE_OWNER_ID,
1593
+ projectId: EXAMPLE_PROJECT_ID,
980
1594
  },
981
1595
  { tier: "worker" },
982
1596
  );
@@ -1016,7 +1630,7 @@ Outside components (or when you need deltas), use `db.subscribeAll` directly:
1016
1630
 
1017
1631
  ### Relations with include
1018
1632
 
1019
- If your schema has `col.ref()` columns, codegen produces typed include interfaces. Use `.include()` to load related rows in one query.
1633
+ If your schema has `s.ref()` columns, Jazz infers typed include interfaces directly from `schema.ts`. Use `.include()` to load related rows in one query.
1020
1634
 
1021
1635
  ```ts
1022
1636
 
@@ -1087,10 +1701,6 @@ More query patterns: [Reading Data](/docs/reading-data).
1087
1701
  If you provide no `jwtToken` or explicit local auth fields, Jazz uses local synthetic auth:
1088
1702
 
1089
1703
  ```tsx
1090
- const anonymousAuthExpoClient = createJazzClient({
1091
- appId: "my-app",
1092
- serverUrl: "http://127.0.0.1:4200",
1093
- });
1094
1704
 
1095
1705
  return (
1096
1706
 
@@ -1101,11 +1711,6 @@ const anonymousAuthExpoClient = createJazzClient({
1101
1711
  If needed, you can explicitly override the local token:
1102
1712
 
1103
1713
  ```tsx
1104
- const anonymousAuthWithTokenExpoClient = createJazzClient({
1105
- appId: "my-app",
1106
- localAuthMode: "anonymous",
1107
- localAuthToken: "device-token-123",
1108
- });
1109
1714
 
1110
1715
  return (
1111
1716
 
@@ -1120,12 +1725,6 @@ Use demo-mode synthetic profiles for local multi-user testing:
1120
1725
  ```tsx
1121
1726
  const demoAuthExpoAppId = "my-app";
1122
1727
  const demoAuthExpoActive = getActiveSyntheticAuth(demoAuthExpoAppId, { defaultMode: "demo" });
1123
- const demoAuthExpoClient = createJazzClient({
1124
- appId: demoAuthExpoAppId,
1125
- serverUrl: "http://127.0.0.1:4200",
1126
- localAuthMode: demoAuthExpoActive.localAuthMode,
1127
- localAuthToken: demoAuthExpoActive.localAuthToken,
1128
- });
1129
1728
 
1130
1729
  return (
1131
1730
 
@@ -1141,15 +1740,6 @@ After provider sign-in, link the current local identity so existing local data s
1141
1740
  const externalAuthExpoAppId = "my-app";
1142
1741
  const externalAuthExpoServerUrl = "http://127.0.0.1:4200";
1143
1742
  const externalAuthExpoProviderJwt = "<provider-jwt>";
1144
- const externalAuthExpoLocalClient = createJazzClient({
1145
- appId: externalAuthExpoAppId,
1146
- serverUrl: externalAuthExpoServerUrl,
1147
- });
1148
- const externalAuthExpoJwtClient = createJazzClient({
1149
- appId: externalAuthExpoAppId,
1150
- serverUrl: externalAuthExpoServerUrl,
1151
- jwtToken: externalAuthExpoProviderJwt,
1152
- });
1153
1743
 
1154
1744
  const [hasJwt, setHasJwt] = useState(false);
1155
1745
  const linkExternalIdentity = useLinkExternalIdentity({
@@ -1193,7 +1783,6 @@ jazz-tools server "$JAZZ_APP_ID" \
1193
1783
  Omit `serverUrl` to keep all data local:
1194
1784
 
1195
1785
  ```tsx
1196
- const offlineOnlyAuthExpoClient = createJazzClient({ appId: "my-app" });
1197
1786
 
1198
1787
  return (
1199
1788
 
@@ -1205,19 +1794,19 @@ Full auth flows and server behavior: [Authentication](/docs/authentication).
1205
1794
 
1206
1795
  ## Permissions
1207
1796
 
1208
- Define row-level policies in `schema/permissions.ts` with `definePermissions(...)`:
1797
+ Define row-level policies in `permissions.ts` with `s.definePermissions(...)`:
1209
1798
 
1210
1799
  ```ts
1211
1800
 
1212
1801
  // Each user only sees their own rows.
1213
- policy.todos.allowRead.where({ owner_id: session.user_id }),
1802
+ policy.todos.allowRead.where({ ownerId: session.user_id }),
1214
1803
  // New rows must belong to the current user.
1215
- policy.todos.allowInsert.where({ owner_id: session.user_id }),
1804
+ policy.todos.allowInsert.where({ ownerId: session.user_id }),
1216
1805
  // Users can only mutate their own incomplete todos.
1217
1806
  policy.todos.allowUpdate
1218
- .whereOld(allOf([{ owner_id: session.user_id }, { done: false }]))
1219
- .whereNew({ owner_id: session.user_id }),
1220
- policy.todos.allowDelete.where(allOf([{ owner_id: session.user_id }, { done: false }])),
1807
+ .whereOld(allOf([{ ownerId: session.user_id }, { done: false }]))
1808
+ .whereNew({ ownerId: session.user_id }),
1809
+ policy.todos.allowDelete.where(allOf([{ ownerId: session.user_id }, { done: false }])),
1221
1810
  ]);
1222
1811
  ```
1223
1812
 
@@ -1244,24 +1833,28 @@ For full environment and server setup options, see [Setup](/docs/setup).
1244
1833
 
1245
1834
  ## Define your schema
1246
1835
 
1247
- Create `schema/current.ts` at the root of your project. This is the source of truth for your data model.
1836
+ Create `schema.ts` at the root of your project. This is the source of truth for your data model.
1248
1837
 
1249
1838
  ```ts
1250
1839
 
1251
- table("projects", {
1252
- name: col.string(),
1253
- });
1840
+ const schema = {
1841
+ projects: s.table({
1842
+ name: s.string(),
1843
+ }),
1844
+ todos: s.table({
1845
+ title: s.string(),
1846
+ done: s.boolean(),
1847
+ description: s.string().optional(),
1848
+ parentId: s.ref("todos").optional(),
1849
+ projectId: s.ref("projects").optional(),
1850
+ }),
1851
+ };
1852
+
1853
+ type AppSchema = s.Schema<typeof schema>;
1254
1854
 
1255
- table("todos", {
1256
- title: col.string(),
1257
- done: col.boolean(),
1258
- description: col.string().optional(),
1259
- parent: col.ref("todos").optional(),
1260
- project: col.ref("projects").optional(),
1261
- });
1262
1855
  ```
1263
1856
 
1264
- ### Run codegen
1857
+ ### Validate schema
1265
1858
 
1266
1859
  Deep dive: [Schemas](/docs/schemas).
1267
1860
 
@@ -1270,12 +1863,6 @@ Deep dive: [Schemas](/docs/schemas).
1270
1863
  `JazzProvider` initializes the Jazz runtime and makes the database available to descendant components via React context.
1271
1864
 
1272
1865
  ```tsx
1273
- const client = createJazzClient({
1274
- appId: "todo-react-example",
1275
- serverUrl: "http://127.0.0.1:1625",
1276
- localAuthMode: "anonymous",
1277
- // jwtToken: authToken, // Use this (instead of localAuthMode) for external auth.
1278
- });
1279
1866
 
1280
1867
  return (
1281
1868
 
@@ -1313,7 +1900,7 @@ Query builders are fluent and immutable. Chain `.where()`, `.orderBy()`, `.limit
1313
1900
  );
1314
1901
  ```
1315
1902
 
1316
- By default `useAll` returns results immediately from the local replica. Use durability options when you need explicit confirmation boundaries.
1903
+ By default `useAll` returns results immediately from the local replica. Use query options when you need explicit confirmation boundaries.
1317
1904
 
1318
1905
  Deep dive: [Reading Data](/docs/reading-data).
1319
1906
 
@@ -1322,16 +1909,16 @@ Deep dive: [Reading Data](/docs/reading-data).
1322
1909
  `useDb` returns the `Db` handle for mutations. Mutations apply locally first and return promises that resolve at the selected durability tier.
1323
1910
 
1324
1911
  ```tsx
1325
- async function addTodo(todoTitle: string) {
1326
- await db.insert(app.todos, { title: todoTitle, done: false });
1912
+ function addTodo(todoTitle: string) {
1913
+ db.insert(app.todos, { title: todoTitle, done: false });
1327
1914
  }
1328
1915
 
1329
- async function toggleTodo(todo: { id: string; done: boolean }) {
1330
- await db.update(app.todos, todo.id, { done: !todo.done });
1916
+ function toggleTodo(todo: { id: string; done: boolean }) {
1917
+ db.update(app.todos, todo.id, { done: !todo.done });
1331
1918
  }
1332
1919
 
1333
- async function removeTodo(id: string) {
1334
- await db.delete(app.todos, id);
1920
+ function removeTodo(id: string) {
1921
+ db.delete(app.todos, id);
1335
1922
  }
1336
1923
  ```
1337
1924
 
@@ -1347,7 +1934,7 @@ Mutation API details: [Writing Data](/docs/writing-data).
1347
1934
 
1348
1935
  ### Reading with a durability tier
1349
1936
 
1350
- Use `db.subscribeAll` (or `db.all`) with a durability tier when you need tier-gated delivery.
1937
+ Inside components, use `useAll(query, options)` with the same `QueryOptions` as `db.subscribeAll`. Outside components, use `db.subscribeAll` (or `db.all`) directly when you need tier-gated delivery or deltas.
1351
1938
 
1352
1939
  ```ts
1353
1940
 
@@ -1364,7 +1951,11 @@ Use durable mutation APIs with `{ tier }` to wait for specific durability tiers:
1364
1951
 
1365
1952
  ```ts
1366
1953
 
1367
- const { id } = await db.insertDurable(app.todos, { title: todoTitle, done: false }, { tier: "edge" });
1954
+ const { id } = await db.insertDurable(
1955
+ app.todos,
1956
+ { title: todoTitle, done: false },
1957
+ { tier: "edge" },
1958
+ );
1368
1959
  await db.updateDurable(app.todos, id, { done: true }, { tier: "edge" });
1369
1960
  await db.deleteDurable(app.todos, id, { tier: "global" });
1370
1961
  }
@@ -1400,7 +1991,7 @@ Outside components (or when you need deltas), use `db.subscribeAll` directly:
1400
1991
 
1401
1992
  ### Relations with include
1402
1993
 
1403
- If your schema has `col.ref()` columns, codegen produces typed include interfaces. Use `.include()` to load related rows in one query.
1994
+ If your schema has `s.ref()` columns, Jazz infers typed include interfaces directly from `schema.ts`. Use `.include()` to load related rows in one query.
1404
1995
 
1405
1996
  ```ts
1406
1997
 
@@ -1473,11 +2064,6 @@ For browser clients, `anonymous` is the default when no external JWT/local auth
1473
2064
  If you provide no `jwtToken` or explicit local auth fields, Jazz auto-generates an anonymous token on first load and persists it in local storage.
1474
2065
 
1475
2066
  ```tsx
1476
- const anonymousAuthClient = createJazzClient({
1477
- appId: "my-app",
1478
- env: "dev",
1479
- userBranch: "main",
1480
- });
1481
2067
 
1482
2068
  return (
1483
2069
 
@@ -1488,11 +2074,6 @@ const anonymousAuthClient = createJazzClient({
1488
2074
  If needed, you can explicitly override the local token:
1489
2075
 
1490
2076
  ```tsx
1491
- const anonymousAuthWithTokenClient = createJazzClient({
1492
- appId: "my-app",
1493
- localAuthMode: "anonymous",
1494
- localAuthToken: "device-token-123",
1495
- });
1496
2077
 
1497
2078
  return (
1498
2079
 
@@ -1507,12 +2088,6 @@ Use synthetic user profiles for local multi-user testing:
1507
2088
  ```tsx
1508
2089
  const demoAuthAppId = "my-app";
1509
2090
  const demoAuthActive = getActiveSyntheticAuth(demoAuthAppId, { defaultMode: "demo" });
1510
- const demoAuthClient = createJazzClient({
1511
- appId: demoAuthAppId,
1512
- serverUrl: "http://127.0.0.1:4200",
1513
- localAuthMode: demoAuthActive.localAuthMode,
1514
- localAuthToken: demoAuthActive.localAuthToken,
1515
- });
1516
2091
 
1517
2092
  return (
1518
2093
  <>
@@ -1530,15 +2105,6 @@ After provider sign-in, link the current local identity so existing local data s
1530
2105
  const externalAuthAppId = "my-app";
1531
2106
  const externalAuthServerUrl = "http://127.0.0.1:4200";
1532
2107
  const externalAuthProviderJwt = "<provider-jwt>";
1533
- const externalAuthLocalClient = createJazzClient({
1534
- appId: externalAuthAppId,
1535
- serverUrl: externalAuthServerUrl,
1536
- });
1537
- const externalAuthJwtClient = createJazzClient({
1538
- appId: externalAuthAppId,
1539
- serverUrl: externalAuthServerUrl,
1540
- jwtToken: externalAuthProviderJwt,
1541
- });
1542
2108
 
1543
2109
  const [hasJwt, setHasJwt] = useState(false);
1544
2110
  const linkExternalIdentity = useLinkExternalIdentity({
@@ -1581,7 +2147,6 @@ jazz-tools server "$JAZZ_APP_ID" \
1581
2147
  Omit `serverUrl` to keep all data local:
1582
2148
 
1583
2149
  ```tsx
1584
- const offlineOnlyAuthClient = createJazzClient({ appId: "my-app" });
1585
2150
 
1586
2151
  return (
1587
2152
 
@@ -1593,7 +2158,7 @@ Full auth flows and server behavior: [Authentication](/docs/authentication).
1593
2158
 
1594
2159
  ## Permissions
1595
2160
 
1596
- Define row-level policies in `schema/permissions.ts` with `definePermissions(...)`:
2161
+ Define row-level policies in `permissions.ts` with `s.definePermissions(...)`:
1597
2162
 
1598
2163
  ```ts
1599
2164
 
@@ -1631,24 +2196,28 @@ For full environment and server setup options, see [Setup](/docs/setup).
1631
2196
 
1632
2197
  ## Define your schema
1633
2198
 
1634
- Create `schema/current.ts` at the root of your project. This is the source of truth for your data model.
2199
+ Create `schema.ts` at the root of your project. This is the source of truth for your data model.
1635
2200
 
1636
2201
  ```ts
1637
2202
 
1638
- table("projects", {
1639
- name: col.string(),
1640
- });
2203
+ const schema = {
2204
+ projects: s.table({
2205
+ name: s.string(),
2206
+ }),
2207
+ todos: s.table({
2208
+ title: s.string(),
2209
+ done: s.boolean(),
2210
+ description: s.string().optional(),
2211
+ parentId: s.ref("todos").optional(),
2212
+ projectId: s.ref("projects").optional(),
2213
+ }),
2214
+ };
2215
+
2216
+ type AppSchema = s.Schema<typeof schema>;
1641
2217
 
1642
- table("todos", {
1643
- title: col.string(),
1644
- done: col.boolean(),
1645
- description: col.string().optional(),
1646
- parent: col.ref("todos").optional(),
1647
- project: col.ref("projects").optional(),
1648
- });
1649
2218
  ```
1650
2219
 
1651
- ### Run codegen
2220
+ ### Validate schema
1652
2221
 
1653
2222
  Deep dive: [Schemas](/docs/schemas).
1654
2223
 
@@ -1660,8 +2229,12 @@ Call `createJazzClient` with your config outside the component tree, then pass t
1660
2229
 
1661
2230
  <script lang="ts">
1662
2231
  import { createJazzClient, JazzSvelteProvider } from 'jazz-tools/svelte';
2232
+ import AuthSessionExamples from './AuthSessionExamples.svelte';
1663
2233
  import TodoList from './TodoList.svelte';
1664
2234
 
2235
+ // Keep docs-only auth snippets in the compiled example app.
2236
+ void AuthSessionExamples;
2237
+
1665
2238
  const client = createJazzClient({
1666
2239
  appId: 'todo-svelte-example',
1667
2240
  serverUrl: 'http://127.0.0.1:1625',
@@ -1720,7 +2293,7 @@ Deep dive: [Reading Data](/docs/reading-data).
1720
2293
 
1721
2294
  <script lang="ts">
1722
2295
  import { getDb, QuerySubscription } from 'jazz-tools/svelte';
1723
- import { app } from '../schema/app.js';
2296
+ import { app } from '../schema.js';
1724
2297
 
1725
2298
  const db = getDb();
1726
2299
  const todos = new QuerySubscription(app.todos);
@@ -1729,25 +2302,25 @@ Deep dive: [Reading Data](/docs/reading-data).
1729
2302
  app.todos.where({ done: false }).orderBy('title', 'asc').limit(50),
1730
2303
  );
1731
2304
 
1732
- const confirmedTodos = new QuerySubscription(app.todos, 'edge');
2305
+ const confirmedTodos = new QuerySubscription(app.todos, { tier: 'edge' });
1733
2306
 
1734
2307
  let title = $state('');
1735
2308
 
1736
- async function handleSubmit(e: SubmitEvent) {
2309
+ function handleSubmit(e: SubmitEvent) {
1737
2310
  e.preventDefault();
1738
2311
  if (!title.trim()) return;
1739
2312
 
1740
- await db.insert(app.todos, { title: title.trim(), done: false });
2313
+ db.insert(app.todos, { title: title.trim(), done: false });
1741
2314
 
1742
2315
  title = '';
1743
2316
  }
1744
2317
 
1745
- async function toggleTodo(todo: { id: string; done: boolean }) {
1746
- await db.update(app.todos, todo.id, { done: !todo.done });
2318
+ function toggleTodo(todo: { id: string; done: boolean }) {
2319
+ db.update(app.todos, todo.id, { done: !todo.done });
1747
2320
  }
1748
2321
 
1749
- async function removeTodo(id: string) {
1750
- await db.delete(app.todos, id);
2322
+ function removeTodo(id: string) {
2323
+ db.delete(app.todos, id);
1751
2324
  }
1752
2325
 
1753
2326
  async function addImportantTodo(todoTitle: string) {
@@ -1795,12 +2368,12 @@ Mutation API details: [Writing Data](/docs/writing-data).
1795
2368
  has synced to the nearest server. Use `global` when you need the broadest server-side durability
1796
2369
  acknowledgement.
1797
2370
 
1798
- ### Reading with a tier
2371
+ ### Reading with query options
1799
2372
 
1800
- Pass a tier as the second argument to `QuerySubscription` to gate initial delivery.
2373
+ Pass `QueryOptions` as the second argument to `QuerySubscription` to control subscription delivery. Use `tier` when you need to gate initial delivery.
1801
2374
 
1802
2375
  ```ts
1803
- const confirmedTodos = new QuerySubscription(app.todos, 'edge');
2376
+ const confirmedTodos = new QuerySubscription(app.todos, { tier: 'edge' });
1804
2377
  ```
1805
2378
 
1806
2379
  ### Writing with a durability tier
@@ -1847,7 +2420,7 @@ Outside components (or when you need deltas), use `db.subscribeAll` directly:
1847
2420
 
1848
2421
  ### Relations with include
1849
2422
 
1850
- If your schema has `col.ref()` columns, codegen produces typed include interfaces. Use `.include()` to load related rows in one query.
2423
+ If your schema has `s.ref()` columns, Jazz infers typed include interfaces directly from `schema.ts`. Use `.include()` to load related rows in one query.
1851
2424
 
1852
2425
  ```ts
1853
2426
 
@@ -2048,51 +2621,143 @@ jazz-tools server "$JAZZ_APP_ID" \
2048
2621
 
2049
2622
  ```
2050
2623
 
2051
- ### Offline-only mode
2052
-
2053
- Omit `serverUrl` to keep all data local:
2624
+ ### Offline-only mode
2625
+
2626
+ Omit `serverUrl` to keep all data local:
2627
+
2628
+ ```svelte
2629
+
2630
+ <script lang="ts">
2631
+ import { createJazzClient, JazzSvelteProvider } from 'jazz-tools/svelte';
2632
+
2633
+ const client = createJazzClient({ appId: 'my-app' });
2634
+ </script>
2635
+
2636
+ {#snippet children({ db })}
2637
+ <slot />
2638
+ {/snippet}
2639
+
2640
+ ```
2641
+
2642
+ Full auth flows and server behavior: [Authentication](/docs/authentication).
2643
+
2644
+ ## Permissions
2645
+
2646
+ Define row-level policies in `permissions.ts` with `s.definePermissions(...)`:
2647
+
2648
+ ```ts
2649
+
2650
+ // Everyone can read todos.
2651
+ policy.todos.allowRead.where({}),
2652
+ // New todos start as incomplete.
2653
+ policy.todos.allowInsert.where({ done: false }),
2654
+ // Completed todos are immutable.
2655
+ policy.todos.allowUpdate.whereOld({ done: false }).whereNew({}),
2656
+ // Only open todos can be deleted.
2657
+ policy.todos.allowDelete.where({ done: false }),
2658
+ ]);
2659
+ ```
2660
+
2661
+ Think about permissions in four checks:
2662
+
2663
+ - Who can read a row?
2664
+ - Who can insert a row?
2665
+ - Which old/new row states are allowed on update?
2666
+ - Who can delete a row?
2667
+
2668
+ For advanced policy patterns and Rust parity, see [Permissions](/docs/permissions).
2669
+
2670
+ ===PAGE:quickstarts/typescript-browser===
2671
+ TITLE:TypeScript (Browser)
2672
+ DESCRIPTION:Build a local-first browser app with Jazz using plain TypeScript. Schema, subscriptions, and mutations without a framework.
2673
+
2674
+ ## Install
2675
+
2676
+ Install the runtime package:
2677
+
2678
+ `pnpm add jazz-tools`
2679
+
2680
+ For full environment and server setup options, see [Setup](/docs/setup).
2681
+
2682
+ ## Define your schema
2683
+
2684
+ Create `schema.ts` at the root of your project. This is the source of truth for your data model.
2685
+
2686
+ ```ts
2687
+ projects: s.table({
2688
+ name: s.string(),
2689
+ }),
2690
+ todos: s.table({
2691
+ title: s.string(),
2692
+ done: s.boolean(),
2693
+ description: s.string().optional(),
2694
+ ownerId: s.string().optional(),
2695
+ parentId: s.ref("todos").optional(),
2696
+ projectId: s.ref("projects").optional(),
2697
+ }),
2698
+ ```
2699
+
2700
+ ### Validate schema
2701
+
2702
+ Deep dive: [Schemas](/docs/schemas).
2703
+ For chunked browser file storage, see [Files & Blobs](/docs/files-and-blobs).
2704
+
2705
+ ## Create a client
2706
+
2707
+ `createDb` initialises the Jazz runtime and returns a `Db` instance. Call it once at app startup.
2708
+
2709
+ ```ts
2710
+ const db = await createDb({
2711
+ appId: readEnvAppId() ?? "todo-client-example",
2712
+ env: "dev",
2713
+ userBranch: "main",
2714
+ ...config,
2715
+ });
2716
+ ```
2717
+
2718
+ - `appId` identifies the app namespace for storage and sync.
2719
+ - `serverUrl` enables sync; omit it for offline-only mode.
2054
2720
 
2055
- ```svelte
2721
+ ## Read data
2056
2722
 
2057
- <script lang="ts">
2058
- import { createJazzClient, JazzSvelteProvider } from 'jazz-tools/svelte';
2723
+ ### Subscriptions
2059
2724
 
2060
- const client = createJazzClient({ appId: 'my-app' });
2061
- </script>
2725
+ `subscribeAll` streams live results into a callback whenever the data changes.
2062
2726
 
2063
- {#snippet children({ db })}
2064
- <slot />
2065
- {/snippet}
2727
+ ```ts
2066
2728
 
2729
+ return db.subscribeAll(app.todos.where({ done: false }), ({ all }) => onCount(all.length));
2730
+ }
2067
2731
  ```
2068
2732
 
2069
- Full auth flows and server behavior: [Authentication](/docs/authentication).
2070
-
2071
- ## Permissions
2733
+ The callback receives `{ all }` — an array of the current matching rows. Call the returned function to unsubscribe.
2072
2734
 
2073
- Define row-level policies in `schema/permissions.ts` with `definePermissions(...)`:
2735
+ ### One-shot reads
2074
2736
 
2075
2737
  ```ts
2076
2738
 
2077
- // Everyone can read todos.
2078
- policy.todos.allowRead.where({}),
2079
- // New todos start as incomplete.
2080
- policy.todos.allowInsert.where({ done: false }),
2081
- // Completed todos are immutable.
2082
- policy.todos.allowUpdate.whereOld({ done: false }).whereNew({}),
2083
- // Only open todos can be deleted.
2084
- policy.todos.allowDelete.where({ done: false }),
2085
- ]);
2739
+ return db.all(app.todos.where({ done: false }));
2740
+ }
2086
2741
  ```
2087
2742
 
2088
- Think about permissions in four checks:
2743
+ Deep dive: [Reading Data](/docs/reading-data).
2089
2744
 
2090
- - Who can read a row?
2091
- - Who can insert a row?
2092
- - Which old/new row states are allowed on update?
2093
- - Who can delete a row?
2745
+ ## Write data
2094
2746
 
2095
- For advanced policy patterns and Rust parity, see [Permissions](/docs/permissions).
2747
+ ```ts
2748
+
2749
+ db.insert(app.todos, {
2750
+ title: "Write docs",
2751
+ done: false,
2752
+ ownerId: EXAMPLE_OWNER_ID,
2753
+ projectId: EXAMPLE_PROJECT_ID,
2754
+ });
2755
+ db.update(app.todos, todoId, { done: true });
2756
+ db.delete(app.todos, todoId);
2757
+ }
2758
+ ```
2759
+
2760
+ Mutation API details: [Writing Data](/docs/writing-data).
2096
2761
 
2097
2762
  ===PAGE:quickstarts/typescript-server===
2098
2763
  TITLE:TypeScript Server
@@ -2108,61 +2773,67 @@ For full environment and server setup options, see [Setup](/docs/setup).
2108
2773
 
2109
2774
  ## Define your schema
2110
2775
 
2111
- Create `schema/current.ts` at the root of your project. This is the source of truth for your data model.
2776
+ Create `schema.ts` at the root of your project. This is the source of truth for your data model.
2112
2777
 
2113
2778
  ```ts
2114
2779
 
2115
- table("projects", {
2116
- name: col.string(),
2117
- });
2780
+ const schema = {
2781
+ projects: s.table({
2782
+ name: s.string(),
2783
+ }),
2784
+ todos: s.table({
2785
+ title: s.string(),
2786
+ done: s.boolean(),
2787
+ description: s.string().optional(),
2788
+ parentId: s.ref("todos").optional(),
2789
+ projectId: s.ref("projects").optional(),
2790
+ owner_id: s.string(),
2791
+ }),
2792
+ };
2118
2793
 
2119
- table("todos", {
2120
- title: col.string(),
2121
- done: col.boolean(),
2122
- description: col.string().optional(),
2123
- parent: col.ref("todos").optional(),
2124
- project: col.ref("projects").optional(),
2125
- ownerId: col.string(),
2126
- });
2794
+ type AppSchema = s.Schema<typeof schema>;
2127
2795
 
2128
2796
  ```
2129
2797
 
2130
- ### Run codegen
2798
+ ### Validate schema
2131
2799
 
2132
2800
  Deep dive: [Schemas](/docs/schemas).
2133
2801
 
2134
2802
  ## Create a server context
2135
2803
 
2136
- Initialize a Jazz context once at process startup and lazily create a shared server client:
2804
+ Initialize a Jazz context once at process startup and lazily create a shared `Db`:
2137
2805
 
2138
2806
  ```ts
2139
2807
  const context = createJazzContext({
2140
2808
  appId: "todo-server-ts",
2141
2809
  app: schemaApp,
2142
- dataPath: "./data/jazz.db",
2810
+ permissions,
2811
+ driver: { type: "persistent", dataPath: "./data/jazz.db" },
2143
2812
  serverUrl: process.env.JAZZ_SERVER_URL,
2144
2813
  backendSecret: process.env.JAZZ_BACKEND_SECRET,
2145
2814
  });
2146
- const client = context.client();
2815
+ const db = context.db();
2816
+ void db;
2147
2817
  ```
2148
2818
 
2149
2819
  - `appId` identifies the app namespace for storage and sync.
2150
- - `app` is your generated schema DSL export (`app.wasmSchema` is loaded for runtime setup).
2151
- - `serverUrl` + `backendSecret` enable request-scoped API operations via `context.forRequest(req)`.
2820
+ - `app` is your typed schema export (`app.wasmSchema` is loaded for runtime setup).
2821
+ - `context.db()` gives you a high-level `Db` using the context's configured runtime/auth.
2822
+ - `serverUrl` + `backendSecret` enable backend-authenticated handles via `context.asBackend()` and `context.forRequest(req)`.
2152
2823
  - Runtime storage path controls where local server state persists.
2153
2824
 
2154
2825
  For more server context options, see [Setup](/docs/setup#backend-context-setup).
2155
2826
 
2156
2827
  ## Read data in API handlers
2157
2828
 
2158
- Use `context.forRequest(req)` so each request is scoped to the requester session.
2829
+ Use `context.forRequest(req)` so each request gets a requester-scoped `Db`.
2159
2830
 
2160
2831
  ### One-shot reads
2161
2832
 
2162
2833
  ```ts
2163
2834
 
2164
2835
  const requester = context.forRequest(req);
2165
- const rows = await requester.query(
2836
+ const rows = await requester.all(
2166
2837
  schemaApp.todos.where({ done: false }).orderBy("title", "asc").limit(100),
2167
2838
  );
2168
2839
  res.json(rows);
@@ -2181,15 +2852,15 @@ Use `context.forRequest(req)` so each request is scoped to the requester session
2181
2852
  res.setHeader("Connection", "keep-alive");
2182
2853
  res.flushHeaders();
2183
2854
 
2184
- const snapshot = await requester.query(query);
2855
+ const snapshot = await requester.all(query);
2185
2856
  res.write(`data: ${JSON.stringify({ type: "snapshot", rows: snapshot })}\n\n`);
2186
2857
 
2187
- const subscriptionId = requester.subscribe(query, (delta) => {
2858
+ const unsubscribe = requester.subscribeAll(query, (delta) => {
2188
2859
  res.write(`data: ${JSON.stringify({ type: "delta", delta })}\n\n`);
2189
2860
  });
2190
2861
 
2191
2862
  req.on("close", () => {
2192
- client.unsubscribe(subscriptionId);
2863
+ unsubscribe();
2193
2864
  });
2194
2865
  }
2195
2866
  ```
@@ -2198,7 +2869,7 @@ Deep dive: [Reading Data](/docs/reading-data).
2198
2869
 
2199
2870
  ## Write data behind API routes
2200
2871
 
2201
- Mutations should also use `context.forRequest(req)` so policies evaluate against the requester context.
2872
+ Mutations should also use `context.forRequest(req)` so policies evaluate against the requester context while keeping the same high-level `Db` API.
2202
2873
 
2203
2874
  ```ts
2204
2875
 
@@ -2214,24 +2885,15 @@ Mutations should also use `context.forRequest(req)` so policies evaluate against
2214
2885
  return;
2215
2886
  }
2216
2887
 
2217
- const values: Value[] = [
2218
- { type: "Text", value: title },
2219
- { type: "Boolean", value: false },
2220
- { type: "Text", value: req.body.description?.trim() ?? "" },
2221
- { type: "Null" },
2222
- { type: "Null" },
2223
- { type: "Text", value: userId },
2224
- ];
2225
-
2226
2888
  const requester = context.forRequest(req);
2227
- const id = await requester.create("todos", values);
2228
-
2229
- res.status(201).json({
2230
- id,
2889
+ const todo = requester.insert(schemaApp.todos, {
2231
2890
  title,
2232
2891
  done: false,
2892
+ description: req.body.description?.trim() || undefined,
2233
2893
  owner_id: userId,
2234
2894
  });
2895
+
2896
+ res.status(201).json(todo);
2235
2897
  }
2236
2898
  ```
2237
2899
 
@@ -2275,7 +2937,7 @@ Full auth flows and session resolution: [Authentication](/docs/authentication).
2275
2937
 
2276
2938
  ## Permissions
2277
2939
 
2278
- Define row-level policies in `schema/permissions.ts`:
2940
+ Define row-level policies in `permissions.ts`:
2279
2941
 
2280
2942
  ```ts
2281
2943
 
@@ -2303,24 +2965,28 @@ For full environment and server setup options, see [Setup](/docs/setup).
2303
2965
 
2304
2966
  ## Define your schema
2305
2967
 
2306
- Create `schema/current.ts` at the root of your project. This is the source of truth for your data model.
2968
+ Create `schema.ts` at the root of your project. This is the source of truth for your data model.
2307
2969
 
2308
2970
  ```ts
2309
2971
 
2310
- table("projects", {
2311
- name: col.string(),
2312
- });
2972
+ const schema = {
2973
+ projects: s.table({
2974
+ name: s.string(),
2975
+ }),
2976
+ todos: s.table({
2977
+ title: s.string(),
2978
+ done: s.boolean(),
2979
+ description: s.string().optional(),
2980
+ parentId: s.ref("todos").optional(),
2981
+ projectId: s.ref("projects").optional(),
2982
+ }),
2983
+ };
2984
+
2985
+ type AppSchema = s.Schema<typeof schema>;
2313
2986
 
2314
- table("todos", {
2315
- title: col.string(),
2316
- done: col.boolean(),
2317
- description: col.string().optional(),
2318
- parent: col.ref("todos").optional(),
2319
- project: col.ref("projects").optional(),
2320
- });
2321
2987
  ```
2322
2988
 
2323
- ### Run codegen
2989
+ ### Validate schema
2324
2990
 
2325
2991
  Deep dive: [Schemas](/docs/schemas).
2326
2992
 
@@ -2331,6 +2997,9 @@ Deep dive: [Schemas](/docs/schemas).
2331
2997
  ```vue
2332
2998
  <script setup lang="ts">
2333
2999
 
3000
+ // Keep docs-only auth snippets in the compiled example app.
3001
+ void AuthSessionExamples;
3002
+
2334
3003
  const client = createJazzClient({
2335
3004
  appId: "todo-vue-example",
2336
3005
  serverUrl: "http://127.0.0.1:1625",
@@ -2386,15 +3055,15 @@ Deep dive: [Reading Data](/docs/reading-data).
2386
3055
  `useDb` returns the shared `Db` handle for mutations. Mutations apply locally first and return promises that resolve at the selected durability tier.
2387
3056
 
2388
3057
  ```ts
2389
- async function addTodo(todoTitle: string) {
2390
- await db.insert(app.todos, { title: todoTitle, done: false });
3058
+ function addTodo(todoTitle: string) {
3059
+ db.insert(app.todos, { title: todoTitle, done: false });
2391
3060
  }
2392
3061
 
2393
- async function toggleTodo(todo: { id: string; done: boolean }) {
2394
- await db.update(app.todos, todo.id, { done: !todo.done });
3062
+ function toggleTodo(todo: { id: string; done: boolean }) {
3063
+ db.update(app.todos, todo.id, { done: !todo.done });
2395
3064
  }
2396
3065
 
2397
- async function removeTodo(id: string) {
3066
+ function removeTodo(id: string) {
2398
3067
  db.delete(app.todos, id);
2399
3068
  }
2400
3069
  ```
@@ -2428,7 +3097,11 @@ Use durable mutation APIs with `{ tier }` to wait for specific durability tiers:
2428
3097
 
2429
3098
  ```ts
2430
3099
 
2431
- const { id } = await db.insertDurable(app.todos, { title: todoTitle, done: false }, { tier: "edge" });
3100
+ const { id } = await db.insertDurable(
3101
+ app.todos,
3102
+ { title: todoTitle, done: false },
3103
+ { tier: "edge" },
3104
+ );
2432
3105
  await db.updateDurable(app.todos, id, { done: true }, { tier: "edge" });
2433
3106
  await db.deleteDurable(app.todos, id, { tier: "global" });
2434
3107
  }
@@ -2464,7 +3137,7 @@ Outside components (or when you need deltas), use `db.subscribeAll` directly:
2464
3137
 
2465
3138
  ### Relations with include
2466
3139
 
2467
- If your schema has `col.ref()` columns, codegen produces typed include interfaces. Use `.include()` to load related rows in one query.
3140
+ If your schema has `s.ref()` columns, Jazz infers typed include interfaces directly from `schema.ts`. Use `.include()` to load related rows in one query.
2468
3141
 
2469
3142
  ```ts
2470
3143
 
@@ -2673,7 +3346,7 @@ Full auth flows and server behavior: [Authentication](/docs/authentication).
2673
3346
 
2674
3347
  ## Permissions
2675
3348
 
2676
- Define row-level policies in `schema/permissions.ts` with `definePermissions(...)`:
3349
+ Define row-level policies in `permissions.ts` with `s.definePermissions(...)`:
2677
3350
 
2678
3351
  ```ts
2679
3352
 
@@ -2766,6 +3439,135 @@ const todos = useAll(app.todos);
2766
3439
  const todos = new QuerySubscription(app.todos);
2767
3440
  ```
2768
3441
 
3442
+ ### First delivery and the undefined signal
3443
+
3444
+ `useAll` and `QuerySubscription` return `undefined` until the subscription has received its
3445
+ first response from the server. Once the server has delivered an initial snapshot, the value
3446
+ becomes an array — empty (`[]`) if no rows match, or populated.
3447
+
3448
+ ```ts
3449
+ // undefined = still connecting to the sync server
3450
+ // [] = connected, no matching rows yet
3451
+ // [...] = connected, rows present
3452
+ ```
3453
+
3454
+ Any initialisation that depends on a consistent remote view can check `undefined` directly:
3455
+
3456
+ ```ts
3457
+ useEffect(() => {
3458
+ if (rows === undefined) return; // wait for server snapshot before acting
3459
+ reconcile();
3460
+ }, [rows]);
3461
+ ```
3462
+
3463
+ ### Running queries conditionally
3464
+
3465
+ There are some cases where you want to defer running a query. This could happen if the query depends on an input the user hasn't provided yet,
3466
+ or on the output of a previous query. In these cases, you can pass `undefined` to `useAll` to defer the query until the condition is met.
3467
+
3468
+ ```ts
3469
+ const [filter, setFilter] = useState<string | null>(null);
3470
+ const rows = useAll(filter ? app.todos.where({ title: { contains: filter } }) : undefined);
3471
+ ```
3472
+
3473
+ When an undefined query is passed to `useAll`, it will return `undefined` until the query is provided.
3474
+
3475
+ ### React Suspense and Transitions
3476
+
3477
+ The docs React example app includes a Suspense-based list that keeps the previous result visible
3478
+ while a filter or page change resolves by moving the query behind a `Suspense` boundary and
3479
+ reading it with `useAllSuspense(query)`.
3480
+
3481
+ ```tsx
3482
+
3483
+ const db = useDb();
3484
+ const [title, setTitle] = useState("");
3485
+ const [filterTitle, setFilterTitle] = useState("");
3486
+ const [showDoneOnly, setShowDoneOnly] = useState(false);
3487
+ const [page, setPage] = useState(0);
3488
+ const [isPending, startTransition] = useTransition();
3489
+ const deferredFilterTitle = useDeferredValue(filterTitle);
3490
+
3491
+ let query = app.todos
3492
+ .orderBy("id", "desc")
3493
+ .limit(25)
3494
+ .offset(page * 25);
3495
+
3496
+ if (deferredFilterTitle.trim()) {
3497
+ query = query.where({ title: { contains: deferredFilterTitle.trim() } });
3498
+ }
3499
+ if (showDoneOnly) {
3500
+ query = query.where({ done: true });
3501
+ }
3502
+
3503
+ const isLoading = isPending || deferredFilterTitle !== filterTitle;
3504
+
3505
+ function updatePage(nextPage: number) {
3506
+ startTransition(() => {
3507
+ setPage(nextPage);
3508
+ });
3509
+ }
3510
+
3511
+ function handleFilterChange(e: React.ChangeEvent) {
3512
+ setFilterTitle(e.target.value);
3513
+ startTransition(() => {
3514
+ setPage(0);
3515
+ });
3516
+ }
3517
+
3518
+ async function handleSubmit(e: React.FormEvent) {
3519
+ e.preventDefault();
3520
+ const trimmedTitle = title.trim();
3521
+
3522
+ if (!trimmedTitle) {
3523
+ return;
3524
+ }
3525
+
3526
+ await db.insert(app.todos, { title: trimmedTitle, done: false });
3527
+ setTitle("");
3528
+ }
3529
+
3530
+ return (
3531
+ <>
3532
+ <form onSubmit={(e) => void handleSubmit(e)}>
3533
+ <input
3534
+ type="text"
3535
+ value={title}
3536
+ onChange={(e) => setTitle(e.target.value)}
3537
+ placeholder="What needs to be done?"
3538
+ required
3539
+ />
3540
+ <button type="submit">Add</button>
3541
+ </form>
3542
+
3543
+ <div>
3544
+ <input
3545
+ type="text"
3546
+ value={filterTitle}
3547
+ onChange={handleFilterChange}
3548
+ placeholder="Filter by title (contains)"
3549
+ aria-label="Filter by title"
3550
+ />
3551
+ <label>
3552
+ <input
3553
+ type="checkbox"
3554
+ checked={showDoneOnly}
3555
+ onChange={(e) => setShowDoneOnly(e.target.checked)}
3556
+ />
3557
+ Done only
3558
+ </label>
3559
+ </div>
3560
+
3561
+ Loading todos...</p>}>
3562
+ <div style={{ opacity: isLoading ? 0.5 : 1, transition: "opacity 0.2s" }}>
3563
+
3564
+ </div>
3565
+
3566
+ </>
3567
+ );
3568
+ }
3569
+ ```
3570
+
2769
3571
  ## Filters
2770
3572
 
2771
3573
  ### API Reference
@@ -2785,7 +3587,7 @@ const todos = useAll(app.todos);
2785
3587
 
2786
3588
  await db.all(app.todos.where({ done: false }));
2787
3589
  await db.all(app.todos.where({ title: { contains: "milk" } }));
2788
- await db.all(app.todos.where({ project: { ne: EXAMPLE_PROJECT_ID } }));
3590
+ await db.all(app.todos.where({ projectId: { ne: EXAMPLE_PROJECT_ID } }));
2789
3591
  }
2790
3592
  ```
2791
3593
 
@@ -2869,41 +3671,48 @@ pub async fn read_todo_page(
2869
3671
  - **Behavior:** `include(...)` loads related rows and returns nested objects in one query result.
2870
3672
  - **Parameters (TypeScript):** `include({ relationName: true | nestedInclude | queryBuilder })`.
2871
3673
  - **Nested includes:** pass nested objects to load multi-hop relations.
3674
+ - **Missing forward references:** included forward relations may be `undefined` at runtime, even when the FK column is non-nullable. Jazz cannot guarantee referential integrity at read time: a referenced row may be deleted, not synced locally yet, or hidden by permissions.
3675
+ - **Required forward includes (TypeScript):** call `requireIncludes()` after `include(...)` to drop rows whose included references cannot be resolved.
3676
+ - **Nullable scalar FKs:** `requireIncludes()` does not drop a row just because a nullable FK is `null`; in that case the included relation remains `undefined`.
3677
+ - **Reverse relations:** `requireIncludes()` does not force reverse includes to be non-empty and does not filter on them.
3678
+ - **Forward array refs:** without `requireIncludes()`, missing referenced members are skipped from the included array. With `requireIncludes()`, the row is dropped if any referenced member is unavailable.
3679
+
3680
+ For chunked files stored in the conventional `files` / `file_parts` tables, prefer
3681
+ `db.loadFileAsBlob(...)` or `db.loadFileAsStream(...)` over eager `include(...)` loading of
3682
+ parts. See [Files & Blobs](/docs/files-and-blobs).
2872
3683
 
2873
3684
  Schema references used by the include examples:
2874
3685
 
2875
3686
  ```ts
2876
-
2877
- table("projects", {
2878
- name: col.string(),
2879
- });
2880
-
2881
- table("todos", {
2882
- title: col.string(),
2883
- done: col.boolean(),
2884
- description: col.string().optional(),
2885
- parent: col.ref("todos").optional(),
2886
- project: col.ref("projects").optional(),
2887
- });
2888
-
3687
+ projects: s.table({
3688
+ name: s.string(),
3689
+ }),
3690
+ todos: s.table({
3691
+ title: s.string(),
3692
+ done: s.boolean(),
3693
+ description: s.string().optional(),
3694
+ ownerId: s.string().optional(),
3695
+ parentId: s.ref("todos").optional(),
3696
+ projectId: s.ref("projects").optional(),
3697
+ }),
2889
3698
  ```
2890
3699
 
2891
- ```sql
2892
- CREATE TABLE projects (
2893
- name TEXT NOT NULL
2894
- );
3700
+ ```ts
2895
3701
 
2896
- CREATE TABLE todos (
2897
- title TEXT NOT NULL,
2898
- done BOOLEAN NOT NULL,
2899
- description TEXT,
2900
- parent UUID REFERENCES todos,
2901
- project UUID REFERENCES projects
2902
- );
2903
- CREATE POLICY todos_select_policy ON todos FOR SELECT USING (TRUE);
2904
- CREATE POLICY todos_insert_policy ON todos FOR INSERT WITH CHECK (TRUE);
2905
- CREATE POLICY todos_update_policy ON todos FOR UPDATE USING (TRUE) WITH CHECK (TRUE);
2906
- CREATE POLICY todos_delete_policy ON todos FOR DELETE USING (TRUE);
3702
+ const schema = {
3703
+ projects: s.table({
3704
+ name: s.string(),
3705
+ }),
3706
+ todos: s.table({
3707
+ title: s.string(),
3708
+ done: s.boolean(),
3709
+ description: s.string().optional(),
3710
+ parent: s.ref("todos").optional(),
3711
+ project: s.ref("projects").optional(),
3712
+ }),
3713
+ };
3714
+
3715
+ type AppSchema = s.Schema<typeof schema>;
2907
3716
 
2908
3717
  ```
2909
3718
 
@@ -2926,6 +3735,58 @@ pub async fn read_todos_with_related_rows(client: &JazzClient) -> jazz_tools::Re
2926
3735
  }
2927
3736
  ```
2928
3737
 
3738
+ ## Picking Columns via select
3739
+
3740
+ ### API Reference
3741
+
3742
+ - **Availability:** generated TypeScript query builders.
3743
+ - **Behavior:** `select(...)` narrows the root row to `id` plus the columns you pick.
3744
+ - **With includes:** `include(...)` still works after `select(...)`.
3745
+ - **Selecting in included rows:** pass a query builder inside `include(...)`, for example `include({ project: app.projects.select("name") })`.
3746
+
3747
+ ```ts
3748
+
3749
+ return db.all(
3750
+ app.todos
3751
+ .select("title")
3752
+ .where({ done: false })
3753
+ .include({ project: app.projects.select("name") }),
3754
+ );
3755
+ }
3756
+ ```
3757
+
3758
+ ## Magic Columns
3759
+
3760
+ ### Permission Introspection
3761
+
3762
+ Permission introspection columns are computed for the current session and are omitted from
3763
+ `select("*")`, so you opt into them explicitly.
3764
+
3765
+ For example, `select("*", "$canDelete")` returns the normal row columns plus `$canDelete`.
3766
+
3767
+ - `$canRead` reflects whether the current session can read the row. Since read policies already
3768
+ gate which rows appear, returned rows will usually show `$canRead: true`.
3769
+ - `$canEdit` reflects whether the current session passes the row's `UPDATE USING` policy.
3770
+ - `$canDelete` reflects whether the current session passes the row's `DELETE USING` policy.
3771
+ - Without a session, permission introspection columns return `null`.
3772
+
3773
+ ```ts
3774
+
3775
+ return db.all(
3776
+ app.todos.select("title", "$canRead", "$canEdit", "$canDelete").orderBy("title", "asc"),
3777
+ );
3778
+ }
3779
+
3780
+ return db.all(app.todos.select("*", "$canDelete").orderBy("title", "asc"));
3781
+ }
3782
+
3783
+ return db.all(app.todos.where({ $canEdit: true }).select("title", "$canEdit"));
3784
+ }
3785
+
3786
+ return db.all(app.todos.where({ $canDelete: true }).select("title", "$canDelete"));
3787
+ }
3788
+ ```
3789
+
2929
3790
  ## Advanced: Recursive Queries
2930
3791
 
2931
3792
  Recursive queries compute bounded transitive closures from a seed row set.
@@ -2934,7 +3795,7 @@ Recursive queries compute bounded transitive closures from a seed row set.
2934
3795
 
2935
3796
  return app.todos.gather({
2936
3797
  start: { done: false },
2937
- step: ({ current }) => app.todos.where({ parent: current }).hopTo("parent"),
3798
+ step: ({ current }) => app.todos.where({ parentId: current }).hopTo("parent"),
2938
3799
  maxDepth: 10,
2939
3800
  });
2940
3801
  }
@@ -2947,8 +3808,12 @@ Recursive queries compute bounded transitive closures from a seed row set.
2947
3808
  - `db.all(query, options?)`
2948
3809
  - `db.one(query, options?)`
2949
3810
  - `db.subscribeAll(query, callback, options?)`
3811
+ - `useAll(query, options?)`
3812
+ - `useAllSuspense(query, options?)`
3813
+ - `new QuerySubscription(query, options?)`
2950
3814
  - `options.tier`: `"worker" | "edge" | "global"`
2951
3815
  - `options.localUpdates`: `"immediate" | "deferred"` (default: `"immediate"`)
3816
+ - `options.propagation`: `"full" | "local-only"` (default: `"full"`)
2952
3817
 
2953
3818
  Durability tiers control when initial query delivery is considered confirmed:
2954
3819
 
@@ -2961,6 +3826,7 @@ Defaults:
2961
3826
  - Browser/client default tier is `worker`.
2962
3827
  - Backend/server-connected default tier is `edge`.
2963
3828
  - `localUpdates: "immediate"` keeps local subscription updates synchronous after the first tier-confirmed snapshot. The initial delivery remains tier-gated.
3829
+ - `propagation: "full"` sends query updates through the normal local+remote path. `propagation: "local-only"` limits delivery to local changes.
2964
3830
 
2965
3831
  Tradeoff summary:
2966
3832
 
@@ -2987,140 +3853,272 @@ Read durability options pair naturally with write durability tiers:
2987
3853
 
2988
3854
  - Use write durability tiers to gate mutation durability checkpoints.
2989
3855
  - Use read durability options to gate query/subscription delivery checkpoints.
3856
+ - React hooks forward these options to the underlying `db.subscribeAll(...)` subscription.
3857
+ - Svelte `QuerySubscription` forwards these options to the underlying `db.subscribeAll(...)` subscription.
2990
3858
 
2991
3859
  See [Write Durability Tiers](/docs/writing-data#write-durability-tiers) for write-side semantics.
2992
3860
 
3861
+ ### Seeding and one-time setup
3862
+
3863
+ For one-off setup (seeding default data, initialising a user record on first sign-in), you
3864
+ often need to ensure no other client has already done the same work before writing. Use
3865
+ `db.all` with `{ tier: "global" }` to wait until the global core has been consulted before
3866
+ reading — this prevents duplicate seeding from concurrent fresh clients.
3867
+
3868
+ ```ts
3869
+ // Reads nothing until globally consistent — prevents duplicate seeding
3870
+ // from concurrent fresh clients on first visit.
3871
+ const existing = await db.all(app.settings, { tier: "global" });
3872
+
3873
+ if (existing.length === 0) {
3874
+ for (const seed of DEFAULT_SETTINGS) {
3875
+ await db.insert(app.settings, seed);
3876
+ }
3877
+ }
3878
+ ```
3879
+
3880
+ | Call | Waits for | Use when |
3881
+ | ----------------------------------- | ------------------------- | ------------------------------------------------- |
3882
+ | `db.all(query)` | Local worker (OPFS) | Reading cached local state |
3883
+ | `db.all(query, { tier: "global" })` | Global core snapshot | Seeding or one-time setup that must not duplicate |
3884
+ | `useAll(query)` | Local snapshot, then live | Live UI that re-renders on every change |
3885
+
3886
+ For most subscriptions, omitting a tier is the right choice: Jazz delivers results from local
3887
+ storage immediately and streams in remote updates as they arrive, giving users the fastest
3888
+ possible experience. Reserve explicit tiers for the cases above where eventual consistency is
3889
+ not acceptable.
3890
+
2993
3891
  ===PAGE:schemas===
2994
3892
  TITLE:Schemas
2995
- DESCRIPTION:Define tables and relationships in TS DSL or SQL, then use the CLI for generated artifacts.
3893
+ DESCRIPTION:Define tables and relationships in root schema.ts, then review migration edges as your app evolves.
2996
3894
 
2997
3895
  ## Table Definitions
2998
3896
 
3897
+ Every Jazz app defines its schema in a root `schema.ts`, including Rust backends.
3898
+
2999
3899
  ```ts
3000
3900
 
3001
- table("projects", {
3002
- name: col.string(),
3003
- });
3901
+ const schema = {
3902
+ projects: s.table({
3903
+ name: s.string(),
3904
+ }),
3905
+ todos: s.table({
3906
+ title: s.string(),
3907
+ done: s.boolean(),
3908
+ description: s.string().optional(),
3909
+ parent: s.ref("todos").optional(),
3910
+ project: s.ref("projects").optional(),
3911
+ }),
3912
+ };
3004
3913
 
3005
- table("todos", {
3006
- title: col.string(),
3007
- done: col.boolean(),
3008
- description: col.string().optional(),
3009
- parent: col.ref("todos").optional(),
3010
- project: col.ref("projects").optional(),
3011
- });
3914
+ type AppSchema = s.Schema<typeof schema>;
3012
3915
 
3013
3916
  ```
3014
3917
 
3015
- ```sql
3016
- CREATE TABLE projects (
3017
- name TEXT NOT NULL
3018
- );
3918
+ Rust apps use the same `schema.ts` file. The schema definition language is still TypeScript, and Rust consumes the compiled internal schema representation through the CLI and low-level APIs.
3019
3919
 
3020
- CREATE TABLE todos (
3021
- title TEXT NOT NULL,
3022
- done BOOLEAN NOT NULL,
3023
- description TEXT,
3024
- parent UUID REFERENCES todos,
3025
- project UUID REFERENCES projects
3026
- );
3027
- CREATE POLICY todos_select_policy ON todos FOR SELECT USING (TRUE);
3028
- CREATE POLICY todos_insert_policy ON todos FOR INSERT WITH CHECK (TRUE);
3029
- CREATE POLICY todos_update_policy ON todos FOR UPDATE USING (TRUE) WITH CHECK (TRUE);
3030
- CREATE POLICY todos_delete_policy ON todos FOR DELETE USING (TRUE);
3920
+ ```ts
3921
+
3922
+ const schema = {
3923
+ projects: s.table({
3924
+ name: s.string(),
3925
+ }),
3926
+ todos: s.table({
3927
+ title: s.string(),
3928
+ done: s.boolean(),
3929
+ description: s.string().optional(),
3930
+ parent: s.ref("todos").optional(),
3931
+ project: s.ref("projects").optional(),
3932
+ }),
3933
+ };
3934
+
3935
+ type AppSchema = s.Schema<typeof schema>;
3031
3936
 
3032
3937
  ```
3033
3938
 
3939
+ In practice, a Rust app runs the schema export command and deserializes the JSON into Jazz's Rust `Schema` type:
3940
+
3941
+ ```bash
3942
+ npx jazz-tools schema export --format json
3943
+ ```
3944
+
3945
+ The Rust examples shell out to this command at startup, so the schema definition stays unified even when the app itself is written in Rust. See [Setup](/docs/setup) for the end-to-end backend context pattern.
3946
+
3947
+ Each top-level property of the `schema` object is a table. In the example above, `projects` and `todos` become tables, and each nested property becomes a column on that table.
3948
+
3949
+ The `type AppSchema = s.Schema<typeof schema>; export const app: s.App = s.defineApp(schema);` pattern is there to keep TypeScript hovers short and readable. Without that alias boundary, TypeScript tends to expand the entire schema literal when you hover `app`, `app.todos`, or derived row/query types.
3950
+
3034
3951
  ## Datatypes and References
3035
3952
 
3036
- - TypeScript DSL defaults to `col.string`, `col.boolean`, and `col.ref` for relationships.
3037
- - SQL keeps the same model via `TEXT`, `BOOLEAN`, and `UUID REFERENCES ...`.
3038
- - Use SQL as the alternate path when your project is Rust-first.
3953
+ - Use `s.string()`, `s.boolean()`, `s.int()`, `s.timestamp()`, and `s.bytes()` for scalar fields.
3954
+ - Use `s.ref("tableName")` for foreign keys. Jazz infers typed `.include(...)` relations directly from `schema.ts`.
3955
+ - For browser files and large uploaded blobs, define the conventional `files` / `file_parts` tables and store a normal reference to `files` from your app rows. See [Files & Blobs](/docs/files-and-blobs).
3039
3956
 
3040
3957
  ### Available column types
3041
3958
 
3042
- ## Jazz CLI and Generated Files
3959
+ ## Workflow and Schema Evolution
3043
3960
 
3044
- ### TS Client Codegen
3961
+ ### Edit `schema.ts` during development
3045
3962
 
3046
- ```bash
3047
- #!/usr/bin/env bash
3963
+ Most of the time, schema evolution starts with a normal edit to `schema.ts`. New writes use the new schema immediately.
3048
3964
 
3049
- # Regenerate SQL and typed TS bindings from schema/current.ts.
3050
- npx jazz-tools@alpha build
3965
+ When older rows are no longer reachable from the current schema graph, Jazz logs a warning with the exact hashes you need next. These warnings can appear in browser logs, backend logs, or be forwarded from upstream servers. A typical warning looks like this, wrapped for readability:
3051
3966
 
3967
+ ```text
3968
+ [client] Detected 3 rows of todos with differing schema versions.
3969
+ [client] To ensure data visibility and forward/backward compatibility please create
3970
+ a new migration with:
3971
+ npx jazz-tools migrations create \
3972
+ a01f5c72ec47 \
3973
+ 311995e9a178
3052
3974
  ```
3053
3975
 
3054
- Run this from a normal app root. It regenerates `schema/current.sql` and typed client bindings (for example `schema/app.ts`) from `schema/current.ts`.
3976
+ That warning is the handoff from day-to-day schema editing to reviewed compatibility work.
3055
3977
 
3056
- ### Generated migration stubs
3978
+ ### Generate a migration edge with `migrations create`
3057
3979
 
3058
- TypeScript migration stubs encode both forward and backward behavior in one file.
3059
- SQL migrations are split into explicit forward and backward files.
3980
+ `migrations create` connects to your Jazz server, loads both schema hashes, and writes a prefilled file into `migrations/{dateCreated}-unnamed-{fromHash}-{toHash}.ts`.
3060
3981
 
3061
- ```ts
3982
+ ```bash
3983
+ npx jazz-tools migrations create <fromHash> <toHash>
3984
+ ```
3062
3985
 
3063
- migrate("todos", {
3064
- description: col.add().string({ default: "" }),
3065
- });
3986
+ The generated file already contains `fromHash`, `toHash`, and minimal typed `from` / `to` witnesses. In most cases, the only manual work is to fill in `migrate` and replace `unnamed` in the filename with a real migration name.
3066
3987
 
3067
- ```
3988
+ ### Publish a reviewed edge with `migrations push`
3068
3989
 
3069
- Forward:
3070
- ```sql
3071
- ALTER TABLE todos ADD COLUMN description TEXT DEFAULT '';
3072
- ```
3073
- Backward:
3074
- ```sql
3075
- ALTER TABLE todos DROP COLUMN description;
3990
+ Publishing is the explicit gate. New schema hashes are still learned automatically when clients connect, but Jazz never auto-pushes migrations.
3991
+
3992
+ ```bash
3993
+ npx jazz-tools migrations push <fromHash> <toHash>
3076
3994
  ```
3077
3995
 
3078
- ### Edited stubs (example customization)
3996
+ Run this after reviewing the migration file. Once pushed, that edge becomes available to make data visible across those schema hashes.
3079
3997
 
3080
- ```ts
3998
+ ## Example Migration Files
3081
3999
 
3082
- // Example of editing a generated migration stub.
3083
- migrate("todos", {
3084
- description: col.add().string({ default: "No description" }),
4000
+ Migration files live in `migrations/` and are always TypeScript, even for Rust apps.
4001
+
4002
+ ### Generated stub
4003
+
4004
+ ```ts
4005
+
4006
+ migrate: {
4007
+ todos: {
4008
+ description: s.add.string({ default: null }),
4009
+ },
4010
+ },
4011
+ fromHash: "a01f5c72ec47",
4012
+ toHash: "311995e9a178",
4013
+ from: {
4014
+ todos: s.table({
4015
+ title: s.string(),
4016
+ done: s.boolean(),
4017
+ parentId: s.ref("todos").optional(),
4018
+ projectId: s.ref("projects").optional(),
4019
+ owner_id: s.string(),
4020
+ }),
4021
+ },
4022
+ to: {
4023
+ todos: s.table({
4024
+ title: s.string(),
4025
+ done: s.boolean(),
4026
+ description: s.string().optional(),
4027
+ parentId: s.ref("todos").optional(),
4028
+ projectId: s.ref("projects").optional(),
4029
+ owner_id: s.string(),
4030
+ }),
4031
+ },
3085
4032
  });
3086
4033
 
3087
4034
  ```
3088
4035
 
3089
- ```sql
3090
- -- Example of editing a generated migration stub.
3091
- ALTER TABLE todos ADD COLUMN description TEXT DEFAULT 'No description';
4036
+ ### Edited stub
3092
4037
 
3093
- ```
4038
+ ```ts
3094
4039
 
3095
- ### Backwards default examples
4040
+ // Example of editing a generated migration stub.
3096
4041
 
3097
- Backwards defaults define what older-schema clients see for fields dropped in newer schemas.
3098
- When new clients write rows without that field, the backward lens supplies the configured default to old clients.
3099
- In TypeScript, the same migration stub carries both forward and backward translation behavior.
3100
- In SQL, the backward lens file explicitly reintroduces dropped columns with a default.
4042
+ migrate: {
4043
+ todos: {
4044
+ description: s.add.string({ default: "No description" }),
4045
+ },
4046
+ },
4047
+ fromHash: "a01f5c72ec47",
4048
+ toHash: "311995e9a178",
4049
+ from: {
4050
+ todos: s.table({
4051
+ title: s.string(),
4052
+ done: s.boolean(),
4053
+ parentId: s.ref("todos").optional(),
4054
+ projectId: s.ref("projects").optional(),
4055
+ owner_id: s.string(),
4056
+ }),
4057
+ },
4058
+ to: {
4059
+ todos: s.table({
4060
+ title: s.string(),
4061
+ done: s.boolean(),
4062
+ description: s.string().optional(),
4063
+ parentId: s.ref("todos").optional(),
4064
+ projectId: s.ref("projects").optional(),
4065
+ owner_id: s.string(),
4066
+ }),
4067
+ },
4068
+ });
3101
4069
 
3102
- ```ts
4070
+ ```
4071
+
4072
+ ### Backwards default example
4073
+
4074
+ ```ts
3103
4075
 
3104
4076
  // Example: dropping a column with a backwards default.
3105
- // Clients still on v1 continue seeing legacy_priority.
3106
- // For rows written by v2 clients, the lens supplies this default value.
3107
- migrate("todos", {
3108
- legacy_priority: col.drop().int({ backwardsDefault: 0 }),
4077
+ // Clients still on the older schema continue seeing legacy_priority.
4078
+
4079
+ migrate: {
4080
+ todos: {
4081
+ legacy_priority: s.drop.int({ backwardsDefault: 0 }),
4082
+ },
4083
+ },
4084
+ fromHash: "311995e9a178",
4085
+ toHash: "73b65d082ab8",
4086
+ from: {
4087
+ todos: s.table({
4088
+ title: s.string(),
4089
+ done: s.boolean(),
4090
+ description: s.string().optional(),
4091
+ parentId: s.ref("todos").optional(),
4092
+ projectId: s.ref("projects").optional(),
4093
+ owner_id: s.string(),
4094
+ legacy_priority: s.int(),
4095
+ }),
4096
+ },
4097
+ to: {
4098
+ todos: s.table({
4099
+ title: s.string(),
4100
+ done: s.boolean(),
4101
+ description: s.string().optional(),
4102
+ parentId: s.ref("todos").optional(),
4103
+ projectId: s.ref("projects").optional(),
4104
+ owner_id: s.string(),
4105
+ }),
4106
+ },
3109
4107
  });
3110
4108
 
3111
4109
  ```
3112
4110
 
3113
- Forward:
3114
- ```sql
3115
- -- Example: drop a column in the forward direction.
3116
- ALTER TABLE todos DROP COLUMN legacy_priority;
4111
+ ## Permissions
3117
4112
 
3118
- ```
3119
- Backward:
3120
- ```sql
3121
- -- Example: old-schema clients still read legacy_priority via this backward lens.
3122
- -- Rows written by new clients are translated to legacy_priority = 0.
3123
- ALTER TABLE todos ADD COLUMN legacy_priority INTEGER DEFAULT 0;
4113
+ To define row-level policies, add a sibling `permissions.ts` next to `schema.ts`.
4114
+
4115
+ ```ts
4116
+
4117
+ policy.todos.allowRead.where({ owner_id: session.user_id });
4118
+ policy.todos.allowInsert.where({ owner_id: session.user_id });
4119
+ policy.todos.allowUpdate.where({ owner_id: session.user_id });
4120
+ policy.todos.allowDelete.where({ owner_id: session.user_id });
4121
+ });
3124
4122
 
3125
4123
  ```
3126
4124
 
@@ -3175,12 +4173,6 @@ Use `JAZZ_APP_ID` as the app ID configuration key across environments.
3175
4173
  For auth modes and upgrade/link flow, see [Authentication](/docs/authentication).
3176
4174
 
3177
4175
  ```tsx
3178
- const client = createJazzClient({
3179
- appId: "todo-react-example",
3180
- serverUrl: "http://127.0.0.1:1625",
3181
- localAuthMode: "anonymous",
3182
- // jwtToken: authToken, // Use this (instead of localAuthMode) for external auth.
3183
- });
3184
4176
 
3185
4177
  return (
3186
4178
 
@@ -3193,6 +4185,9 @@ const client = createJazzClient({
3193
4185
  ```vue
3194
4186
  <script setup lang="ts">
3195
4187
 
4188
+ // Keep docs-only auth snippets in the compiled example app.
4189
+ void AuthSessionExamples;
4190
+
3196
4191
  const client = createJazzClient({
3197
4192
  appId: "todo-vue-example",
3198
4193
  serverUrl: "http://127.0.0.1:1625",
@@ -3217,8 +4212,12 @@ const client = createJazzClient({
3217
4212
 
3218
4213
  <script lang="ts">
3219
4214
  import { createJazzClient, JazzSvelteProvider } from 'jazz-tools/svelte';
4215
+ import AuthSessionExamples from './AuthSessionExamples.svelte';
3220
4216
  import TodoList from './TodoList.svelte';
3221
4217
 
4218
+ // Keep docs-only auth snippets in the compiled example app.
4219
+ void AuthSessionExamples;
4220
+
3222
4221
  const client = createJazzClient({
3223
4222
  appId: 'todo-svelte-example',
3224
4223
  serverUrl: 'http://127.0.0.1:1625',
@@ -3257,11 +4256,12 @@ For React Native/Expo clients, make sure `serverUrl` points to a host your runti
3257
4256
  const context = createJazzContext({
3258
4257
  appId,
3259
4258
  app: schemaApp,
3260
- dataPath: dbPath,
4259
+ permissions,
4260
+ driver: { type: "persistent", dataPath: dbPath },
3261
4261
  env: "dev",
3262
4262
  userBranch: "main",
3263
4263
  });
3264
- const client = context.client();
4264
+ const db = context.db();
3265
4265
  ```
3266
4266
 
3267
4267
  ```rs
@@ -3280,8 +4280,8 @@ For React Native/Expo clients, make sure `serverUrl` points to a host your runti
3280
4280
  ### Backend Identity Pattern
3281
4281
 
3282
4282
  Client-side usage enforces permissions for the local user context.
3283
- Backend usage should derive a requester-scoped client directly from the request.
3284
- In TypeScript backends, use `createJazzContext(...)` once at startup, then `context.forRequest(req)` to extract the bearer JWT payload and scope the client.
4283
+ Backend usage can either use `context.db()` for the configured runtime identity or derive a requester-scoped `Db` directly from the request.
4284
+ In TypeScript backends, create the context once at startup with both `app` and `permissions`, then call `context.forRequest(req)` when you need a bearer-JWT-scoped high-level `Db`.
3285
4285
 
3286
4286
  #### Per-request user-scoped client
3287
4287
 
@@ -3290,7 +4290,7 @@ In TypeScript backends, use `createJazzContext(...)` once at startup, then `cont
3290
4290
  try {
3291
4291
  const rows = await context
3292
4292
  .forRequest(req, schemaApp)
3293
- .query(schemaApp.todos.where({ done: true }));
4293
+ .all(schemaApp.todos.where({ done: true }));
3294
4294
  res.json(rows);
3295
4295
  } catch {
3296
4296
  sendQueryError(res);
@@ -3334,54 +4334,59 @@ DESCRIPTION:Insert, update, and delete APIs with local-first execution, framewor
3334
4334
  `insert`, `update`, and `delete` apply immediately in the local runtime.
3335
4335
  That keeps UI latency low because local state updates do not wait for any network hop.
3336
4336
 
3337
- Mutation methods return promises that resolve when the requested durability tier is reached.
4337
+ `insertDurable`, `updateDurable`, and `deleteDurable` return promises that
4338
+ resolve when the requested durability tier is reached.
4339
+
4340
+ For browser `Blob`, `File`, or `ReadableStream` uploads, use `db.createFileFromBlob(...)`
4341
+ or `db.createFileFromStream(...)` and then store the returned file id on your own row. See
4342
+ [Files & Blobs](/docs/files-and-blobs).
3338
4343
 
3339
4344
  ## Mutating from Framework Context
3340
4345
 
3341
4346
  Use the framework context binding to access the shared `Db` instance and execute mutations.
3342
4347
 
3343
4348
  ```tsx
3344
- async function addTodo(todoTitle: string) {
3345
- await db.insert(app.todos, { title: todoTitle, done: false });
4349
+ function addTodo(todoTitle: string) {
4350
+ db.insert(app.todos, { title: todoTitle, done: false });
3346
4351
  }
3347
4352
 
3348
- async function toggleTodo(todo: { id: string; done: boolean }) {
3349
- await db.update(app.todos, todo.id, { done: !todo.done });
4353
+ function toggleTodo(todo: { id: string; done: boolean }) {
4354
+ db.update(app.todos, todo.id, { done: !todo.done });
3350
4355
  }
3351
4356
 
3352
- async function removeTodo(id: string) {
3353
- await db.delete(app.todos, id);
4357
+ function removeTodo(id: string) {
4358
+ db.delete(app.todos, id);
3354
4359
  }
3355
4360
  ```
3356
4361
 
3357
4362
  ```ts
3358
- async function addTodo(todoTitle: string) {
3359
- await db.insert(app.todos, { title: todoTitle, done: false });
4363
+ function addTodo(todoTitle: string) {
4364
+ db.insert(app.todos, { title: todoTitle, done: false });
3360
4365
  }
3361
4366
 
3362
- async function toggleTodo(todo: { id: string; done: boolean }) {
3363
- await db.update(app.todos, todo.id, { done: !todo.done });
4367
+ function toggleTodo(todo: { id: string; done: boolean }) {
4368
+ db.update(app.todos, todo.id, { done: !todo.done });
3364
4369
  }
3365
4370
 
3366
- async function removeTodo(id: string) {
4371
+ function removeTodo(id: string) {
3367
4372
  db.delete(app.todos, id);
3368
4373
  }
3369
4374
  ```
3370
4375
 
3371
4376
  ```tsx
3372
- const addTodo = async () => {
4377
+ const addTodo = () => {
3373
4378
  const trimmed = title.trim();
3374
4379
  if (!trimmed || !sessionUserId) return;
3375
- await db.insert(app.todos, { title: trimmed, done: false, owner_id: sessionUserId });
4380
+ db.insert(app.todos, { title: trimmed, done: false, ownerId: sessionUserId });
3376
4381
  setTitle("");
3377
4382
  };
3378
4383
 
3379
- const toggleTodo = async (todo: Todo) => {
3380
- await db.update(app.todos, todo.id, { done: !todo.done });
4384
+ const toggleTodo = (todo: Todo) => {
4385
+ db.update(app.todos, todo.id, { done: !todo.done });
3381
4386
  };
3382
4387
 
3383
- const removeTodo = async (id: string) => {
3384
- await db.delete(app.todos, id);
4388
+ const removeTodo = (id: string) => {
4389
+ db.delete(app.todos, id);
3385
4390
  };
3386
4391
  ```
3387
4392
 
@@ -3393,12 +4398,12 @@ async function removeTodo(id: string) {
3393
4398
  ```
3394
4399
 
3395
4400
  ```svelte
3396
- async function toggleTodo(todo: { id: string; done: boolean }) {
3397
- await db.update(app.todos, todo.id, { done: !todo.done });
4401
+ function toggleTodo(todo: { id: string; done: boolean }) {
4402
+ db.update(app.todos, todo.id, { done: !todo.done });
3398
4403
  }
3399
4404
 
3400
- async function removeTodo(id: string) {
3401
- await db.delete(app.todos, id);
4405
+ function removeTodo(id: string) {
4406
+ db.delete(app.todos, id);
3402
4407
  }
3403
4408
  ```
3404
4409
 
@@ -3406,14 +4411,14 @@ async function removeTodo(id: string) {
3406
4411
 
3407
4412
  ```ts
3408
4413
 
3409
- await db.insert(app.todos, {
4414
+ db.insert(app.todos, {
3410
4415
  title: "Write docs",
3411
4416
  done: false,
3412
- owner_id: EXAMPLE_OWNER_ID,
3413
- project: EXAMPLE_PROJECT_ID,
4417
+ ownerId: EXAMPLE_OWNER_ID,
4418
+ projectId: EXAMPLE_PROJECT_ID,
3414
4419
  });
3415
- await db.update(app.todos, todoId, { done: true });
3416
- await db.delete(app.todos, todoId);
4420
+ db.update(app.todos, todoId, { done: true });
4421
+ db.delete(app.todos, todoId);
3417
4422
  }
3418
4423
  ```
3419
4424
 
@@ -3427,7 +4432,7 @@ pub async fn write_todo_crud(client: &JazzClient, existing_id: ObjectId) -> jazz
3427
4432
  Value::Null,
3428
4433
  ];
3429
4434
 
3430
- let _new_id = client.create("todos", values).await?;
4435
+ let _new_row = client.create("todos", values).await?;
3431
4436
  client
3432
4437
  .update(
3433
4438
  existing_id,
@@ -3439,6 +4444,20 @@ pub async fn write_todo_crud(client: &JazzClient, existing_id: ObjectId) -> jazz
3439
4444
  }
3440
4445
  ```
3441
4446
 
4447
+ ### Partial Updates and Nullable Fields
4448
+
4449
+ `update(...)` and `updateDurable(...)` only modify the keys you pass.
4450
+ Omitted fields, and fields explicitly set to `undefined`, are left unchanged.
4451
+ To clear a nullable column in TypeScript, pass `null`.
4452
+ Required fields cannot be set to `null`.
4453
+
4454
+ ```ts
4455
+
4456
+ db.update(app.todos, todoId, { ownerId: null }); // clears the nullable FK
4457
+ db.update(app.todos, todoId, { description: undefined }); // leaves the field unchanged
4458
+ }
4459
+ ```
4460
+
3442
4461
  ## Reset Browser Storage
3443
4462
 
3444
4463
  For browser clients, you can delete the local OPFS database for the current namespace:
@@ -3457,33 +4476,70 @@ Notes:
3457
4476
 
3458
4477
  ## Write Durability Tiers
3459
4478
 
3460
- ### API Reference
4479
+ The tier on a write controls _where the promise resolves_. Writes flow upward — from the
4480
+ local OPFS worker, to the nearest edge server, to the global core — and the tier determines
4481
+ how far the write must travel before the call returns.
3461
4482
 
3462
- - `insert(table, data, options?)`
3463
- - `update(table, id, data, options?)`
3464
- - `delete(table, id, options?)`
3465
- - `options.tier`: `"worker" | "edge" | "global"`
4483
+ Data flows back down on demand. An edge server only fetches data from the global core when a
4484
+ client connected to it subscribes to that data. Edge nodes are demand-driven caches; the
4485
+ global core is the most durable tier and the point all others reconcile against. Two clients
4486
+ on different edge nodes may never share data unless one of them subscribes to data the other
4487
+ has written.
4488
+
4489
+ ### Data consistency
3466
4490
 
3467
- Durability tiers control when the returned promise resolves:
4491
+ Jazz uses last-write-wins CRDT merge strategies, so any tier may hold more recent
4492
+ unreconciled writes than the global core at a given moment. The global core represents the
4493
+ most durable snapshot, not necessarily the most current one. You can get closer to strongly
4494
+ consistent reads by using `global`-tier writes and reads exclusively, but under normal
4495
+ operation the system converges quickly.
3468
4496
 
3469
- - `worker`: waits until the local worker/storage tier acknowledges the write.
3470
- - `edge`: waits until the edge server acknowledges the write.
3471
- - `global`: waits until the global server acknowledges the write.
4497
+ ### API Reference
4498
+
4499
+ - `insert(table, data)`
4500
+ - `update(table, id, data)`
4501
+ - `delete(table, id)`
4502
+ - `insertDurable(table, data, { tier })`
4503
+ - `updateDurable(table, id, data, options?)`
4504
+ - `deleteDurable(table, id, options?)`
4505
+ - `options.tier`: `"worker" | "edge" | "global"`
3472
4506
 
3473
4507
  Defaults:
3474
4508
 
3475
4509
  - Browser/client default tier is `worker`.
3476
4510
  - Backend/server-connected default tier is `edge`.
3477
4511
 
3478
- Tradeoff summary:
4512
+ ### Which tier to use
3479
4513
 
3480
- - Lower tier: lower latency, lower durability scope.
3481
- - Higher tier: higher latency, wider durability scope.
4514
+ **`"worker"` (default)** the write is persisted to the local OPFS database. The promise
4515
+ resolves as soon as the WASM worker confirms it. Other tabs on the same device see the change
4516
+ immediately; remote clients do not yet.
3482
4517
 
3483
- In practical terms:
4518
+ Use `worker` for:
3484
4519
 
3485
- - Local-first updates remain immediate for UI/subscription state.
3486
- - Mutation promises resolve at the selected durability checkpoint.
4520
+ - Local-only state (draft text, UI prefs)
4521
+ - Any write where you do not need remote confirmation
4522
+
4523
+ **`"edge"`** — the write has reached the nearest sync server and been acknowledged. Clients
4524
+ connected to the same edge server will see the write immediately via their subscriptions.
4525
+ Clients on other edge nodes will see it once their edge fetches the update from the global
4526
+ core, which happens when they subscribe to the relevant data.
4527
+
4528
+ Use `edge` for:
4529
+
4530
+ - Any write that other clients must receive (messages, shared state, user actions)
4531
+ - Writes that depend on remote acknowledgement before continuing
4532
+
4533
+ **`"global"`** — the write has propagated to the global core and is globally durable. Any
4534
+ client that subscribes to the relevant data will receive it, regardless of which edge node
4535
+ they are connected to. Highest latency; use only when edge-level acknowledgement is not
4536
+ sufficient.
4537
+
4538
+ Use `global` for:
4539
+
4540
+ - Seeding default data or one-time setup that must not duplicate across concurrent clients
4541
+ - Financial or audit records
4542
+ - Actions that must survive edge server failure
3487
4543
 
3488
4544
  ```ts
3489
4545
 
@@ -3492,8 +4548,8 @@ In practical terms:
3492
4548
  {
3493
4549
  title: "Write docs with durability tier",
3494
4550
  done: false,
3495
- owner_id: EXAMPLE_OWNER_ID,
3496
- project: EXAMPLE_PROJECT_ID,
4551
+ ownerId: EXAMPLE_OWNER_ID,
4552
+ projectId: EXAMPLE_PROJECT_ID,
3497
4553
  },
3498
4554
  { tier: "edge" },
3499
4555
  );
@@ -3507,7 +4563,7 @@ In practical terms:
3507
4563
  pub async fn write_todo_with_default_durability(
3508
4564
  client: &JazzClient,
3509
4565
  ) -> jazz_tools::Result {
3510
- let id = client
4566
+ let (id, _row_values) = client
3511
4567
  .create(
3512
4568
  "todos",
3513
4569
  vec![
@@ -3526,6 +4582,32 @@ pub async fn write_todo_with_default_durability(
3526
4582
  }
3527
4583
  ```
3528
4584
 
4585
+ ### When remote clients see your writes
4586
+
4587
+ All writes propagate upward through the infrastructure tiers and will eventually reach remote
4588
+ clients that subscribe to the relevant data. The tier only controls when the promise resolves
4589
+ and what timing guarantee you get.
4590
+
4591
+ A `worker`-tier write resolves as soon as the local OPFS worker acknowledges it. The write
4592
+ will propagate to the edge and onward, but with no timing guarantee — it sits in the local
4593
+ database until the sync layer flushes it. Remote clients will see it eventually, but not
4594
+ necessarily by the time your promise resolves.
4595
+
4596
+ An `edge`-tier write resolves only after the nearest sync server has acknowledged it. Clients
4597
+ connected to the same edge node will see the write as soon as the promise resolves. Clients on
4598
+ other edge nodes will see it once their edge fetches the update from the global core.
4599
+
4600
+ ```ts
4601
+ // Remote clients will see this eventually — no timing guarantee on delivery
4602
+ db.update(app.tasks, id, { done: true, completedBy: userId });
4603
+
4604
+ // Remote clients on this edge see this as soon as the promise resolves
4605
+ await db.updateDurable(app.tasks, id, { done: true, completedBy: userId }, { tier: "edge" });
4606
+ ```
4607
+
4608
+ If remote clients are not reacting to a write promptly, check whether the tier is `"edge"` or
4609
+ higher.
4610
+
3529
4611
  ## Combining Write Durability with Read Durability
3530
4612
 
3531
4613
  Write durability tiers and read durability options are complementary knobs:
@@ -3538,4 +4620,4 @@ See [Read Durability Options](/docs/reading-data#read-durability-options) for re
3538
4620
  ## Backend Request Context Reminder
3539
4621
 
3540
4622
  Backend writes should always run under a requester-scoped session.
3541
- See [Setup](/docs/setup) for context setup details.
4623
+ See [Setup](/docs/setup) for context setup details.