@voltro/cli 0.52.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/THIRD-PARTY-NOTICES.md +8311 -3318
  3. package/dist/{apiBuild-CSFI8QGq.js → apiBuild-CaPfoWku.js} +11 -5
  4. package/dist/apiBuild-DHtLXYx9.js +2 -0
  5. package/dist/bin.js +1 -1
  6. package/dist/build-D-OnvNMf.js +843 -0
  7. package/dist/{checkCommand-COmqc2cB.js → checkCommand-C5elt0tW.js} +6 -6
  8. package/dist/{checkCommand-2SbqzukH.js → checkCommand-D2ZduVlh.js} +1 -1
  9. package/dist/codegen-BWpt3VgF.js +2 -0
  10. package/dist/{codegen-VF479Cnb.js → codegen-FEk8AZHb.js} +2 -2
  11. package/dist/{codegenCommand-DCdG2JN-.js → codegenCommand-BOiWQ5hz.js} +4 -4
  12. package/dist/{codemodRunner-r7J9lIa7.js → codemodRunner-BjtB2lq6.js} +109 -11
  13. package/dist/{commands-Cc_nV8WI.js → commands-DyxAmhP0.js} +30 -30
  14. package/dist/{dashboardCommand-C-gKvwqh.js → dashboardCommand-BdKTyT13.js} +5 -5
  15. package/dist/{dataCommand-BgpBHnlB.js → dataCommand-Bab9X7s8.js} +2 -2
  16. package/dist/{dbCommand-sHedr-NJ.js → dbCommand-06O2finM.js} +277 -236
  17. package/dist/dbCommand-B1EXBC6f.js +2 -0
  18. package/dist/{dev-CRHoCEiy.js → dev-C6LGF4iY.js} +3006 -2426
  19. package/dist/{dev--A3nsxA3.js → dev-GjJWAYo2.js} +1 -1
  20. package/dist/doctorCommand-B0hX0tdz.js +2 -0
  21. package/dist/{doctorCommand-DtfJ3FA6.js → doctorCommand-etMkflRc.js} +101 -69
  22. package/dist/{dormancyCommand-Drn7o0No.js → dormancyCommand-UwZ1AZzB.js} +1 -1
  23. package/dist/{embeddingsCommand-Z-jO1fWN.js → embeddingsCommand-C70zWHwo.js} +1 -1
  24. package/dist/{envCommand-D4gCrrTZ.js → envCommand-dSyKvRkM.js} +18 -18
  25. package/dist/{evolveCommand-CMROeKeA.js → evolveCommand-CG0_ebO5.js} +2 -2
  26. package/dist/fileConventions-DASGEmj-.js +35 -0
  27. package/dist/{fileTaxonomy-DvDUV9wq.js → fileTaxonomy-B7uxipWS.js} +55 -55
  28. package/dist/fontPipeline-LxIHa1vo.js +2 -0
  29. package/dist/fontPipeline-Tsh8kZfA.js +152 -0
  30. package/dist/frameworkTableAssembly-C_7Z-rMs.js +2 -0
  31. package/dist/{frameworkTableAssembly-w-XnLa3q.js → frameworkTableAssembly-DKx3ba3S.js} +5 -5
  32. package/dist/imagePipeline-B_GVJgm6.js +2 -0
  33. package/dist/imagePipeline-CBZmjT4i.js +127 -0
  34. package/dist/index.js +1 -1
  35. package/dist/{infoCommand-DXM868o_.js → infoCommand-_53iOc_j.js} +1 -1
  36. package/dist/{inspect-CtL_xTbu.js → inspect-Bd8-9wsi.js} +1 -1
  37. package/dist/inspect-CuoDInfZ.js +2 -0
  38. package/dist/{inspectGateHint-BjnFubmH.js → inspectGateHint-4LxkNtrz.js} +1 -1
  39. package/dist/manifestBuild-C4-J1-m_.js +2 -0
  40. package/dist/{manifestBuild-CuU1VrSm.js → manifestBuild-Cqgsx2bM.js} +1 -1
  41. package/dist/{metaCommands-C6RFmF1r.js → metaCommands-Cn2oboG4.js} +8 -2
  42. package/dist/{migrate-D0F-eTlK.js → migrate-Cko9rswM.js} +2 -2
  43. package/dist/{pageConvention-CzUiSbtU.js → pageConvention-C938S8oC.js} +1 -1
  44. package/dist/{privacyCommand-DGdopOI6.js → privacyCommand-DWTQMC6R.js} +2 -2
  45. package/dist/{probeCommand-C9gazU0H.js → probeCommand-DkGGLknv.js} +83 -24
  46. package/dist/{renderModeScan-CcH2X1_D.js → renderModeScan-CUbOeOAg.js} +3 -1
  47. package/dist/{renderProfile-Ck32Fzxr.js → renderProfile-CskIgAfn.js} +2 -2
  48. package/dist/{runtimeTrace-BPQyCmC5.js → runtimeTrace-c0APJz7E.js} +1 -1
  49. package/dist/{sdkgen-Se88ifTd.js → sdkgen-BiQCgIEr.js} +1 -1
  50. package/dist/{serveCommand-DkP3OT0W.js → serveCommand-CueKQgzl.js} +869 -805
  51. package/dist/serveCommand-DsnrVN3U.js +2 -0
  52. package/dist/serveEntry.js +1 -1
  53. package/dist/start-BJzZLbt8.js +3 -0
  54. package/dist/start-ekPan8BT.js +1510 -0
  55. package/dist/startEntry.js +1 -1
  56. package/dist/{staticCommand-BwNEDlSU.js → staticCommand-xlSL-IWk.js} +1 -1
  57. package/dist/{test-f3amja6a.js → test-BWPQcRoB.js} +1 -1
  58. package/dist/updateCommand-Bqql_rsQ.js +2 -0
  59. package/dist/{updateCommand-BMk2e4ky.js → updateCommand-C_8I8Rzo.js} +139 -115
  60. package/dist/webDev-C7jWJ5dX.js +2 -0
  61. package/dist/{webDev-BgWL9gKV.js → webDev-oczpugbx.js} +1455 -922
  62. package/dist/{webhooksCommand-CoIO3jbj.js → webhooksCommand-4SVPDjKg.js} +1 -1
  63. package/package.json +60 -19
  64. package/templates/AGENTS.md +5 -3
  65. package/templates/agent-docs/_index.md +5 -3
  66. package/templates/agent-docs/_manifest.json +20 -4
  67. package/templates/agent-docs/ai.md +2 -2
  68. package/templates/agent-docs/authentication.md +1 -1
  69. package/templates/agent-docs/cli.md +2 -2
  70. package/templates/agent-docs/configuration.md +17 -0
  71. package/templates/agent-docs/data.md +470 -26
  72. package/templates/agent-docs/database/advancedqueries.md +7 -7
  73. package/templates/agent-docs/database/columntypes.md +2 -2
  74. package/templates/agent-docs/database/querying.md +1 -1
  75. package/templates/agent-docs/database/schema.md +1 -1
  76. package/templates/agent-docs/database/seedsdialects.md +1 -1
  77. package/templates/agent-docs/database/transactions.md +3 -3
  78. package/templates/agent-docs/deployment.md +8 -0
  79. package/templates/agent-docs/internationalization.md +2 -2
  80. package/templates/agent-docs/introduction.md +7 -1
  81. package/templates/agent-docs/local-first-mobile.md +102 -4
  82. package/templates/agent-docs/observability.md +2 -0
  83. package/templates/agent-docs/plugins/auth.md +1 -1
  84. package/templates/agent-docs/plugins/comments.md +142 -0
  85. package/templates/agent-docs/plugins/notifications.md +47 -4
  86. package/templates/agent-docs/plugins/presence.md +16 -3
  87. package/templates/agent-docs/plugins/prometheus.md +1 -1
  88. package/templates/agent-docs/plugins/queue.md +129 -0
  89. package/templates/agent-docs/plugins.md +11 -7
  90. package/templates/agent-docs/reference.md +16 -4
  91. package/templates/agent-docs/routing.md +527 -3
  92. package/templates/agent-docs/schema-driven-ui.md +214 -3
  93. package/templates/agent-docs/security.md +3 -3
  94. package/templates/agent-docs/whats-new.md +160 -66
  95. package/templates/apps/api-ai/package.json +6 -6
  96. package/templates/apps/api-auth/package.json +8 -8
  97. package/templates/apps/api-backend/package.json +7 -7
  98. package/templates/apps/api-backend-deactivation/package.json +7 -7
  99. package/templates/apps/api-backend-mail/package.json +8 -8
  100. package/templates/apps/api-backend-mariadb/package.json +9 -9
  101. package/templates/apps/api-backend-sqlite/package.json +8 -8
  102. package/templates/apps/api-backend-storage/package.json +8 -8
  103. package/templates/apps/api-cms/package.json +9 -9
  104. package/templates/apps/api-collab/package.json +8 -8
  105. package/templates/apps/api-data-advanced/package.json +8 -8
  106. package/templates/apps/api-durable/package.json +8 -8
  107. package/templates/apps/api-feature-flags/package.json +9 -9
  108. package/templates/apps/api-governance/package.json +8 -8
  109. package/templates/apps/api-kv/package.json +8 -8
  110. package/templates/apps/api-moderation/package.json +8 -8
  111. package/templates/apps/api-observability/package.json +8 -8
  112. package/templates/apps/api-ratelimit/package.json +8 -8
  113. package/templates/apps/api-rbac/package.json +8 -8
  114. package/templates/apps/api-rest/package.json +7 -7
  115. package/templates/apps/api-row-history/package.json +8 -8
  116. package/templates/apps/api-saas/package.json +10 -10
  117. package/templates/apps/api-saas-starter/package.json +10 -10
  118. package/templates/apps/api-search/package.json +8 -8
  119. package/templates/apps/api-status/package.json +8 -8
  120. package/templates/apps/api-webhooks/package.json +9 -9
  121. package/templates/apps/changelog/app.config.ts +26 -2
  122. package/templates/apps/changelog/content/releases/{0.1.0.mdx → v0-1-0.mdx} +0 -1
  123. package/templates/apps/changelog/content/releases/{0.2.0.mdx → v0-2-0.mdx} +0 -1
  124. package/templates/apps/changelog/package.json +8 -8
  125. package/templates/apps/changelog/src/collections/releases.collection.ts +48 -0
  126. package/templates/apps/changelog/src/globals.d.ts +1 -1
  127. package/templates/apps/changelog/src/locales/de.ts +1 -1
  128. package/templates/apps/changelog/src/locales/en.ts +1 -1
  129. package/templates/apps/changelog/src/pages/[locale]/[slug]/page.tsx +7 -4
  130. package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +13 -41
  131. package/templates/apps/changelog/src/pages/[slug]/page.test.tsx +26 -51
  132. package/templates/apps/changelog/src/pages/[slug]/page.tsx +21 -19
  133. package/templates/apps/changelog/src/pages/page.test.tsx +14 -37
  134. package/templates/apps/changelog/src/pages/page.tsx +18 -12
  135. package/templates/apps/edge-functions/package.json +2 -2
  136. package/templates/apps/frontend-admin/package.json +8 -8
  137. package/templates/apps/frontend-app/package.json +9 -9
  138. package/templates/apps/frontend-auth/package.json +8 -8
  139. package/templates/apps/frontend-blank/package.json +7 -7
  140. package/templates/apps/frontend-cms/package.json +9 -9
  141. package/templates/apps/frontend-collab/package.json +10 -10
  142. package/templates/apps/frontend-contact/package.json +7 -7
  143. package/templates/apps/frontend-dashboard/package.json +7 -7
  144. package/templates/apps/frontend-docs/content/docs/de/guides/first-page.md +4 -0
  145. package/templates/apps/frontend-docs/content/docs/de/intro/getting-started.md +4 -0
  146. package/templates/apps/frontend-docs/content/docs/en/guides/first-page.md +4 -0
  147. package/templates/apps/frontend-docs/content/docs/en/intro/getting-started.md +4 -0
  148. package/templates/apps/frontend-docs/package.json +8 -6
  149. package/templates/apps/frontend-docs/src/collections/docs.collection.ts +21 -0
  150. package/templates/apps/frontend-docs/src/locales/de.ts +0 -5
  151. package/templates/apps/frontend-docs/src/locales/en.ts +0 -5
  152. package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug]/page.tsx +17 -9
  153. package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +10 -5
  154. package/templates/apps/frontend-docs/src/pages/[locale]/page.tsx +8 -0
  155. package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.test.tsx +53 -15
  156. package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.tsx +27 -33
  157. package/templates/apps/frontend-docs/src/pages/page.test.tsx +17 -3
  158. package/templates/apps/frontend-docs/src/pages/page.tsx +16 -12
  159. package/templates/apps/frontend-i18n/package.json +6 -6
  160. package/templates/apps/frontend-landing/package.json +6 -6
  161. package/templates/apps/frontend-portal/package.json +8 -8
  162. package/templates/apps/frontend-saas/package.json +8 -8
  163. package/templates/apps/frontend-spa/package.json +7 -7
  164. package/templates/apps/frontend-ssr/package.json +7 -7
  165. package/templates/apps/frontend-ssr-api/package.json +8 -8
  166. package/templates/apps/frontend-static-blog/content/posts/cms-to-ssg.md +12 -0
  167. package/templates/apps/frontend-static-blog/content/posts/hello-static.md +12 -0
  168. package/templates/apps/frontend-static-blog/content/posts/islands-not-hydration.md +14 -0
  169. package/templates/apps/frontend-static-blog/package.json +8 -6
  170. package/templates/apps/frontend-static-blog/src/collections/posts.collection.ts +27 -0
  171. package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug]/page.tsx +7 -4
  172. package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +7 -1
  173. package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.test.tsx +19 -9
  174. package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.tsx +19 -21
  175. package/templates/apps/frontend-static-blog/src/pages/page.test.tsx +18 -5
  176. package/templates/apps/frontend-static-blog/src/pages/page.tsx +21 -14
  177. package/templates/apps/frontend-status/package.json +8 -8
  178. package/templates/apps/mobile-app/package.json +4 -4
  179. package/dist/apiBuild-BYBpL7Pz.js +0 -2
  180. package/dist/build-CPgcMQug.js +0 -793
  181. package/dist/codegen-CctkDO-1.js +0 -2
  182. package/dist/dbCommand-DNb6yeOG.js +0 -2
  183. package/dist/doctorCommand-CqoWA2p5.js +0 -2
  184. package/dist/fileConventions-DOqD3lPS.js +0 -34
  185. package/dist/frameworkTableAssembly-C6ETawPR.js +0 -2
  186. package/dist/inspect-CuGDYES0.js +0 -2
  187. package/dist/manifestBuild-CPjhvM62.js +0 -2
  188. package/dist/serveCommand-DLc-BznW.js +0 -2
  189. package/dist/start-DfL3fOiN.js +0 -3
  190. package/dist/start-jw89Xbqy.js +0 -1339
  191. package/dist/updateCommand-5gFVfK5q.js +0 -2
  192. package/dist/webDev-CZbTsDcH.js +0 -2
  193. package/templates/apps/changelog/scripts/generate-rss.mjs +0 -38
  194. package/templates/apps/changelog/src/lib/releases.ts +0 -21
  195. package/templates/apps/frontend-static-blog/src/content/posts.ts +0 -64
@@ -1103,6 +1103,40 @@ target: {
1103
1103
 
1104
1104
  `path`, `by`, `match`, and `shapeItem` are browser-safe descriptor data (a dot-path string + pure functions) — the same discipline as `identify`/`shape`.
1105
1105
 
1106
+ ## Declared relations — a junction saved in the same mutation
1107
+
1108
+ A form with a multi-reference field (assigned stores, tags, members) writes a
1109
+ JUNCTION table beside the row. Declare that on the write target and the
1110
+ framework reconciles the links INSIDE the mutation's transaction — no
1111
+ hand-written junction code in the executor, and a failure rolls the whole
1112
+ write back:
1113
+
1114
+ ```ts
1115
+ export const employeesUpdate = defineMutation({
1116
+ name: 'employees.update',
1117
+ input: EmployeesUpdateInput, // carries assignedStores: string[]
1118
+ output: Employee,
1119
+ target: {
1120
+ table: 'employees',
1121
+ op: 'update',
1122
+ relations: { assignedStores: 'employee_assigned_stores' },
1123
+ },
1124
+ })
1125
+ ```
1126
+
1127
+ After the executor succeeds, `input.assignedStores` is reconciled against the
1128
+ junction via the diff-based link writer (`store.relationLinks`): missing rows
1129
+ inserted, surplus rows deleted, unchanged rows untouched — so reactive
1130
+ subscriptions on the junction see one change per changed row. The anchor
1131
+ column is derived from the junction's `reference()` targets; a self-junction
1132
+ (both columns referencing one table) is refused by name, never guessed.
1133
+
1134
+ The semantics worth knowing: an ABSENT input field leaves the links
1135
+ untouched — absent is not empty; an empty array is the explicit "clear them
1136
+ all". The row id comes from the executor's `output.id`, falling back to
1137
+ `input.id`. The link writes go through `ctx.store`, so undo capture and
1138
+ cross-table rules see them like any other write.
1139
+
1106
1140
  ## Typed Errors
1107
1141
 
1108
1142
  ```ts
@@ -2026,8 +2060,14 @@ Queries/subscriptions are for live state. Streams are for one-shot element flows
2026
2060
  ## Reconnect
2027
2061
 
2028
2062
  A dropped WebSocket rebuilds the whole client stack — new socket, new RPC
2029
- client, new subscription cache — and re-subscribes every active query, each of
2030
- which answers with a fresh snapshot.
2063
+ client, new subscription cache — and re-subscribes every active query. Inside
2064
+ the resume window (`reactive.resume.windowMs`, default 60 s) the server replays
2065
+ **only the deltas the client missed** — the re-subscribe presents the last
2066
+ materialised revision and the stream continues on the same revision line, so a
2067
+ short offline gap costs a handful of patches instead of every row. Outside the
2068
+ window, for computed queries, for row-filtered apps, or whenever anything is in
2069
+ doubt, the query answers with a fresh snapshot — the delta-resume wire contract
2070
+ lives in [the wire protocol](/docs/data/wire-protocol#reconnect--delta-resume).
2031
2071
 
2032
2072
  **What is on screen while that happens is your last-known-good data, not a
2033
2073
  skeleton.** The replacement cache is seeded from the one it retires, so `data`
@@ -2054,6 +2094,15 @@ Three things are deliberately NOT carried across:
2054
2094
  - **Entries nothing re-subscribes to.** A screen that unmounted during the
2055
2095
  reconnect does not pin its rows; the seed evicts on the normal inactive TTL.
2056
2096
 
2097
+ ### The local-first mirror partitions by subject — the carve-out
2098
+
2099
+ An app using `@voltro/local-first`'s query mirror keeps rows on the DEVICE
2100
+ across reloads. The blank-on-auth rule extends there structurally: every
2101
+ mirrored key carries the subject AND tenant, so the next subject's binding
2102
+ simply never finds the predecessor's rows, and a logout or membership
2103
+ revocation calls `purge()` on the departing partition. Nothing about the
2104
+ in-memory blanking above changes.
2105
+
2057
2106
  ### An auth change still blanks — on purpose
2058
2107
 
2059
2108
  When the rebuild happens because the connection's *subject* changed — a cookie
@@ -2158,32 +2207,40 @@ once. Order BETWEEN subscribers was never guaranteed.
2158
2207
  There is a number, it is not a constant, and which number you get depends on a
2159
2208
  property of your **queries** rather than of your scale. Re-derive it on your own
2160
2209
  hardware with `node packages/runtime/scripts/fanout-ceiling.mjs`; the figures
2161
- below are the spread across three runs on a busy developer machine at 10 matched
2162
- writes per second, against a budget of 100 ms of event-loop time per second (10%
2163
- of one core).
2210
+ below are the spread across three runs on a busy developer machine (12-core,
2211
+ macOS) at 10 matched writes per second, against a budget of 100 ms of
2212
+ event-loop time per second (10% of one core).
2164
2213
 
2165
2214
  | Subscriber population | Marginal CPU per subscriber | Subscribers per node |
2166
2215
  | --- | --- | --- |
2167
- | **Shared** — N clients on the SAME query (a leaderboard, a shared board) | 0.5–0.9 µs | ≈ 11 000–20 000 |
2168
- | **Distinct** — N clients each on their OWN query (`where userId = me`) | 22–29 µs | 350–450 |
2169
-
2170
- Ranges rather than single numbers, deliberately: that is the spread three runs
2171
- produced, and a ceiling quoted to three significant figures from one run is a
2172
- number somebody will hold you to.
2173
-
2174
- The shared case is cheap because the memoisation above applies: one read, one
2175
- diff, N emits. The distinct case gets no sharing at all — the read, the diff and
2176
- the emit are all per subscriber and a per-user dashboard is exactly that shape.
2177
- **Plan against the distinct number**, and note that it scales inversely with your
2178
- write rate: at 1 matched write per second it is ten times higher.
2179
-
2180
- Two things that are easy to assume and are not true:
2181
-
2182
- - **A more selective `where` buys no headroom.** Measured: 200 of 200
2183
- subscribers whose predicate matched *nothing* were still woken by one write on
2184
- their table. Every subscription is a dependent of its own table, so a change
2185
- wakes all of them and each re-queries. The ceiling counts subscribers **on the
2186
- table**, not subscribers whose predicate matches.
2216
+ | **Shared** — N clients on the SAME query (a leaderboard, a shared board), every write matches all of them | 0.37–0.41 µs | ≈ 25 000–27 000 |
2217
+ | **Distinct** — N clients each on their OWN query (`where userId = me`), a write matches ONE | flat per-write cost does not grow with resident subscribers | not set by subscriber count |
2218
+
2219
+ The shared case fans out by design one read and one diff (the memoisation
2220
+ above), then N emits and it is the shape that sets the ceiling. The distinct
2221
+ case changed shape entirely with **matcher authority**: a subscription whose
2222
+ query is a plain predicate read is woken by the predicate index alone, so a
2223
+ write to *somebody else's* row is a non-event, not a wake. Per write it costs a
2224
+ bucket lookup plus one delivery, regardless of how many thousands of distinct
2225
+ subscribers are resident. Measured: **0 of 200** subscribers whose predicate
2226
+ matched nothing were woken by a write on their table **a selective `where`
2227
+ buys real headroom** now.
2228
+
2229
+ What still wakes conservatively (any change on the table), and deliberately
2230
+ this list is exhaustive:
2231
+
2232
+ - queries with an **eager `.with()` spec**, a setOp (`union`/…) or a CTE — the
2233
+ handler reads rows the root predicate does not describe;
2234
+ - **`dependsOn`** raw reads (the dispatcher can only re-run the descriptor,
2235
+ never the handler);
2236
+ - **computed** queries and **`reactivityChannel`** queries (their own
2237
+ recompute paths, unchanged);
2238
+ - **oversized change events** (`tombstone` / `unrecovered` — row images the
2239
+ matcher cannot see wake the whole table for that one event; `rehydrated`
2240
+ events are judged normally).
2241
+
2242
+ One thing that is easy to assume and is not true:
2243
+
2187
2244
  - **It is not 512.** That constant bounds `onChange` LISTENERS — one per declared
2188
2245
  subscription file, reaction or aggregate, bound once at boot. Every client
2189
2246
  subscription in a process shares the dispatcher's single listener, so ten
@@ -2195,6 +2252,17 @@ mysql/mariadb (binlog), so a second node needs no extra wiring — the cost bein
2195
2252
  budgeted here is the matcher and re-query CPU each node spends on ITS OWN
2196
2253
  clients.
2197
2254
 
2255
+ ### Deltas are per-query — two queries can briefly diverge
2256
+
2257
+ Every subscription has its own revision line and its own delivery moment. After
2258
+ one write that affects two queries you hold open, the deltas arrive as two
2259
+ independent pushes — usually microseconds apart, but there is no cross-query
2260
+ transaction on the wire, and a render between the two pushes can see query A
2261
+ after the write and query B before it. Within ONE query you never see a partial
2262
+ write (a delta is computed from a committed row set); across queries, design for
2263
+ eventual agreement rather than instantaneous consistency — derive values that
2264
+ must agree atomically inside one query instead of joining two on the client.
2265
+
2198
2266
  ## Raw WebSocket gateways — `defineWebSocket`
2199
2267
 
2200
2268
  Everything above rides the framework's subscription protocol, and that stays the answer for app realtime — live queries, optimistic patches, reconnect. A **gateway** exists for the other case: a FOREIGN protocol that needs a socket the framework does not speak — a Yjs provider, a legacy device fleet, an MQTT-over-WS bridge. It mounts its own upgrade path beside the rpc socket, in a `*.ws.ts` file discovered on **both** boot paths:
@@ -4854,7 +4922,7 @@ A streaming query (what `useSubscription` opens) emits a sequence of **subscript
4854
4922
  { _tag: 'error', error: { _tag?: string, message: string, ...fields }, revision?: number }
4855
4923
  ```
4856
4924
 
4857
- - **`revision`** — monotonically increasing; lets the client order events and detect gaps.
4925
+ - **`revision`** — monotonically increasing; lets the client order events. **Revisions may JUMP forward** — under socket backpressure the server coalesces updates a slow consumer hasn't read yet into one event whose patch is computed against the row set of the last event it actually handed over, so patch continuity holds across the jump. A jump is therefore normal, never a gap; only a *regressing* or repeating revision would be a protocol violation.
4858
4926
  - **`emittedAt`** — epoch milliseconds, present on `delta` only.
4859
4927
  - **`data`** (snapshot) — the full payload, typed by the query's `output` schema (a row, an array of rows, a computed value — whatever the handler returns).
4860
4928
  - **`patch`** (delta) — an id-keyed RFC-6902-style patch against the row set the client last held.
@@ -4873,6 +4941,70 @@ propagate to the shared connection and stall every *other* subscription on it
4873
4941
  client surfaces it as `useSubscription(...).error` for that one query key;
4874
4942
  siblings keep delivering their snapshots and deltas.
4875
4943
 
4944
+ ### Slow consumers — coalescing and `SubscriptionOverrun`
4945
+
4946
+ A consumer that stops reading (a backgrounded tab, a saturated link) does not
4947
+ grow the server without bound. While its socket is blocked, updates
4948
+ **coalesce**: the server keeps only the newest state per subscription and, when
4949
+ the socket accepts again, sends ONE event — a patch against the last state the
4950
+ consumer was actually handed (`revision` jumps accordingly, see above). Memory
4951
+ per blocked subscription is bounded by construction: one pending state,
4952
+ regardless of how far behind the consumer is.
4953
+
4954
+ A consumer that stays more than `reactive.socket.maxBufferedBytes` (default
4955
+ 1 MiB, env `VOLTRO_REACTIVE_MAX_BUFFERED_BYTES`) behind for
4956
+ `reactive.socket.overrunAfterMs` (default 10 s) is closed **loudly**: it
4957
+ receives an `error` event with `error._tag: 'SubscriptionOverrun'` (carrying
4958
+ `bufferedBytes` + `maxBufferedBytes`) and the stream ends — never a silent
4959
+ drop. The client re-subscribes and starts from a fresh snapshot.
4960
+
4961
+ Oversized events are telemetry, not a cap: an event over
4962
+ `reactive.socket.oversizedEventBytes` (default 256 KiB) is delivered normally
4963
+ and counted (`voltro_subscription_oversized_total`) with a WARN naming the
4964
+ query — alongside `voltro_subscription_buffered_bytes`,
4965
+ `voltro_subscription_coalesced_total` and `voltro_subscription_overrun_total`
4966
+ in the Prometheus exporter and the inspect Metrics panel.
4967
+
4968
+ ### Reconnect — delta-resume
4969
+
4970
+ A client that reconnects inside the **resume window** does not have to pay for
4971
+ a full snapshot: it sends the last `revision` it materialised in the per-call
4972
+ `voltro-resume-from` request header (the same header surface the idempotency
4973
+ key rides), and the server — which kept the subscription alive server-side for
4974
+ the window after the disconnect — **replays only the deltas that were missed**
4975
+ and re-attaches the stream on the SAME revision line.
4976
+
4977
+ The signal is the first event's tag, not a schema field:
4978
+
4979
+ - **first event `delta`** — the resume was honoured; apply the patch onto the
4980
+ rows you already hold and continue.
4981
+ - **first event `snapshot`** — the resume was declined; reset to the snapshot.
4982
+ This is the answer whenever anything is in doubt, because a wrong snapshot
4983
+ costs bytes while a wrong replay would leak rows.
4984
+
4985
+ `@voltro/client` does both automatically — the reconnect-seeded cache keeps its
4986
+ rows and revision, presents the header, and treats a snapshot-first stream as
4987
+ the reset it already knows how to do. Replayed deltas may **coalesce** exactly
4988
+ as slow-consumer updates do (revisions jump; patch continuity holds).
4989
+
4990
+ A resume is declined — always with a fresh snapshot — when:
4991
+
4992
+ - the window expired (`reactive.resume.windowMs`, default 60 s, env
4993
+ `VOLTRO_REACTIVE_RESUME_WINDOW_MS`), or more deltas were missed than the ring
4994
+ retains (`reactive.resume.maxDeltas`, default 256, env
4995
+ `VOLTRO_REACTIVE_RESUME_MAX_DELTAS`);
4996
+ - the query's `guards:` were revoked while the client was away — the
4997
+ per-delivery re-check keeps running on the detached subscription, and a
4998
+ revocation drops the retained history outright;
4999
+ - the resuming caller is a different subject or tenant (a login, logout or
5000
+ tenant switch between disconnect and resume) — the retained history is keyed
5001
+ by subject AND tenant, so a changed identity simply never finds it;
5002
+ - the app registers a row filter (`setRowFilter`), or the query is a
5003
+ **computed** query — both are excluded from resume by design: a row-filtered
5004
+ subscription's visible row set exists only per delivery, and a computed query
5005
+ re-runs a handler with no delta chain to replay. They reconnect with a fresh
5006
+ snapshot, exactly as before.
5007
+
4876
5008
  **Author a live-subscribed getter to return, not throw.** A subscription is a
4877
5009
  long-lived stream, so a getter that throws on every re-evaluation is a broken
4878
5010
  stream. For an expected-absent row, make the query `output: Schema.NullOr(...)`
@@ -5205,6 +5337,203 @@ try {
5205
5337
 
5206
5338
 
5207
5339
 
5340
+ ---
5341
+
5342
+ <!-- source: en/data/content-collections.md -->
5343
+ ## Content collections
5344
+
5345
+ _File-based, schema-typed markdown content — defineCollection over content/<name>/**/*.md, an isomorphic getCollection/getEntry, locale trees with fallback, headings for TOCs, data collections, references, and RSS feeds — without installing a markdown dependency._
5346
+
5347
+ A content collection turns a folder of markdown files into typed, rendered
5348
+ content: you declare the frontmatter schema in code, and `getCollection()` /
5349
+ `getEntry()` hand you decoded data plus server-rendered HTML with
5350
+ syntax-highlighted code fences. The markdown engine lives in the framework —
5351
+ **do not install your own `marked` / `remark` / `shiki`**; a second pipeline
5352
+ drifts from the one your artifacts, feeds and templates already use.
5353
+
5354
+ ## A blog in 20 lines
5355
+
5356
+ One collection file, one markdown file, one page:
5357
+
5358
+ ```ts
5359
+ // src/collections/posts.collection.ts
5360
+ import { Schema } from 'effect'
5361
+ import { defineCollection } from '@voltro/content'
5362
+
5363
+ export const posts = defineCollection({
5364
+ name: 'posts',
5365
+ directory: 'content/posts',
5366
+ schema: Schema.Struct({ title: Schema.String, date: Schema.String }),
5367
+ })
5368
+ export type Post = Schema.Schema.Type<typeof posts.schema>
5369
+ ```
5370
+
5371
+ ```ts
5372
+ // src/pages/blog/[slug]/page.tsx
5373
+ import { getCollection, getEntry, type ContentEntry } from '@voltro/content'
5374
+ import { useLoaderData } from '@voltro/web'
5375
+ import { posts, type Post } from '../../../collections/posts.collection'
5376
+
5377
+ export const renderMode = 'static' as const
5378
+ export const getStaticPaths = async () =>
5379
+ (await getCollection(posts.name)).map((e) => ({ params: { slug: e.slug } }))
5380
+ export const loader = async ({ params }: { params: { slug: string } }) =>
5381
+ await getEntry<Post>(posts.name, params.slug)
5382
+
5383
+ export default function Post() {
5384
+ const post = useLoaderData<ContentEntry<Post> | null>()
5385
+ if (!post) return <main>Not found</main>
5386
+ return <article dangerouslySetInnerHTML={{ __html: post.html ?? '' }} />
5387
+ }
5388
+ ```
5389
+
5390
+ Drop `content/posts/hello.md` with `title:` + `date:` frontmatter and the
5391
+ build pre-renders `/blog/hello` — highlighted code fences included.
5392
+
5393
+ ## How it stays out of your bundle
5394
+
5395
+ The loader is **isomorphic**. At build/SSR time it reads the filesystem and
5396
+ renders markdown (shiki runs on the server only). The build also emits JSON
5397
+ artifacts under `dist/assets/content/<name>[.<locale>]/…` — an index (slugs +
5398
+ frontmatter, no bodies) and one file per entry (rendered HTML + headings). On
5399
+ an SPA navigation, the CLIENT branch of `getCollection`/`getEntry` fetches
5400
+ those artifacts. The result: no markdown engine, no highlighter, and no
5401
+ content bodies in your JavaScript bundle. `voltro dev` serves the same
5402
+ artifact shapes on demand and invalidates them when a `content/**` file
5403
+ changes.
5404
+
5405
+ ## Frontmatter is a schema, and violations fail the build
5406
+
5407
+ The `schema` is an `effect/Schema` struct decoded per file. A missing or
5408
+ mistyped field is a **build error naming the file** — not a page that renders
5409
+ `undefined`. Numbers in frontmatter arrive as strings; use
5410
+ `Schema.Union(Schema.NumberFromString, Schema.Number)` for numeric fields.
5411
+ `getCollection<A>` returns entries whose `data` is the schema's inferred
5412
+ type — no casts.
5413
+
5414
+ ## Slugs come from the path
5415
+
5416
+ `content/posts/hello.md` → `hello`; nested folders stay in the slug
5417
+ (`database/joins.md` → `database/joins`). Two files resolving to one slug
5418
+ (a rename that left both) is a build error.
5419
+
5420
+ ## Locale trees + fallback
5421
+
5422
+ A collection with `i18n` treats the first path segment as the locale:
5423
+
5424
+ ```ts
5425
+ export const docs = defineCollection({
5426
+ name: 'docs',
5427
+ directory: 'content/docs',
5428
+ schema: Schema.Struct({ title: Schema.String }),
5429
+ i18n: { locales: ['en', 'de'], defaultLocale: 'en', missing: 'fallback' },
5430
+ })
5431
+ ```
5432
+
5433
+ `getCollection('docs', { locale: 'de' })` reads the `de/` tree. A slug missing
5434
+ in the requested locale is served from the default tree with
5435
+ `fallback: true` on the entry (render an "untranslated" banner off it) — or
5436
+ omitted entirely with `missing: 'missing'`. Incomplete translations are the
5437
+ normal case; decide the policy per collection instead of improvising per page.
5438
+
5439
+ ## Headings as data
5440
+
5441
+ Every rendered entry carries `headings: [{ depth, slug, text }]` — the TOC
5442
+ input. The slugs are the SAME ids stamped on the rendered `<h2 id="…">`
5443
+ elements, so sidebar anchors never drift from the body. For a TOC without a
5444
+ render pass, `extractHeadings(markdown)` (from `@voltro/content/markdown`)
5445
+ computes the same data synchronously.
5446
+
5447
+ ## Data collections
5448
+
5449
+ `kind: 'data'` reads `.json` files instead of markdown — the `authors.json`
5450
+ case. Each file decodes whole against the schema; there is no render path:
5451
+
5452
+ ```ts
5453
+ export const authors = defineCollection({
5454
+ name: 'authors',
5455
+ directory: 'content/authors',
5456
+ kind: 'data',
5457
+ schema: Schema.Struct({ name: Schema.String, url: Schema.String }),
5458
+ })
5459
+ ```
5460
+
5461
+ ## References between collections
5462
+
5463
+ `reference('<collection>')` declares a frontmatter field that names an entry
5464
+ of another collection by slug:
5465
+
5466
+ ```ts
5467
+ schema: Schema.Struct({
5468
+ title: Schema.String,
5469
+ author: reference('authors'),
5470
+ })
5471
+ ```
5472
+
5473
+ The build validates every reference — a dangling one (`author: nobody`) fails
5474
+ the build naming the collection, entry, field and target. Resolve it with
5475
+ `getEntry('authors', entry.data.author)`.
5476
+
5477
+ ## RSS feeds from a collection
5478
+
5479
+ Declare feeds in `app.config.ts`; the build writes them next to
5480
+ `sitemap.xml`, and `voltro dev` serves the same XML live:
5481
+
5482
+ ```ts
5483
+ export default {
5484
+ // …
5485
+ seo: { siteUrl: 'https://example.com' },
5486
+ feeds: [{
5487
+ path: '/rss.xml',
5488
+ collection: 'posts',
5489
+ title: 'My blog',
5490
+ item: (e) => e.data.draft === 'true' ? null : ({
5491
+ title: e.data.title, link: `/blog/${e.slug}`, date: e.data.date,
5492
+ }),
5493
+ }],
5494
+ }
5495
+ ```
5496
+
5497
+ Returning `null` from `item` excludes an entry — that is the **draft filter**:
5498
+ keep a `draft: true` field in your schema and filter it in `item` and in your
5499
+ page loaders (the changelog template's `visibleReleases` helper is the worked
5500
+ example, including future-dated staging).
5501
+
5502
+ ## No MDX — islands carry the interactivity
5503
+
5504
+ Collection bodies are **markdown, not MDX**: JSX, `import`s and
5505
+ `{expressions}` in a body are not executed. When a content page needs a live
5506
+ widget, the surrounding PAGE provides it via the islands mechanism — the
5507
+ content stays inert HTML and the widget hydrates alone:
5508
+
5509
+ ```tsx
5510
+ // src/pages/blog/[slug]/page.tsx
5511
+ export const interactive = 'islands' as const
5512
+
5513
+ export default function Post() {
5514
+ const post = useLoaderData<ContentEntry<Post>>()
5515
+ return (
5516
+ <main>
5517
+ <ReadingProgress /> {/* an island() component — the ONLY hydrated JS */}
5518
+ <article dangerouslySetInnerHTML={{ __html: post.html ?? '' }} />
5519
+ </main>
5520
+ )
5521
+ }
5522
+ ```
5523
+
5524
+ ## Limits + neighbors
5525
+
5526
+ - **Images referenced from markdown bodies** are copied as-is (no transform):
5527
+ the [image pipeline](/docs/routing/assets) covers `?image` imports from
5528
+ code. Put content images under `public/` and reference them absolutely.
5529
+ - **Files are DEVELOPER content** — versioned with the code, deployed by the
5530
+ build. Editorial content with drafts, roles and a save/publish pipeline is
5531
+ [`@voltro/cms`](/docs/data/cms). Astro's remote "Content Layer loaders"
5532
+ map to `@voltro/cms` here: remote/editorial sources go through the CMS,
5533
+ not through file collections.
5534
+
5535
+
5536
+
5208
5537
  ---
5209
5538
 
5210
5539
  <!-- source: en/data/cms.md -->
@@ -5745,3 +6074,118 @@ app's public origin comes from `VOLTRO_PUBLIC_URL`.
5745
6074
  a secret, not a connection.
5746
6075
  - **`redirectTo` is a same-origin path only.** An absolute URL is rejected —
5747
6076
  otherwise every app declaring a connection would ship an open redirector.
6077
+
6078
+
6079
+
6080
+ ---
6081
+
6082
+ <!-- source: en/data/grpc.md -->
6083
+ ## gRPC surface
6084
+
6085
+ _Serve opt-in procedures to generated gRPC clients — proto emitted from your effect/Schema with checked-in field-number stability, unary for mutations/actions, server-streaming for live queries, guards + interceptors + typed errors identical to the socket._
6086
+
6087
+ The gRPC surface serves a NAMED list of your procedures to external gRPC
6088
+ clients — the polyglot-microservice door. The `.proto` is generated from the
6089
+ same `effect/Schema` your procedures already declare, so there is no second
6090
+ contract to maintain; the wire semantics are the framework's own: guards,
6091
+ plugin interceptors and typed errors behave **identically** to the rpc
6092
+ socket, because a gRPC call runs the *same bound runner* every other surface
6093
+ uses (the e2e proves interceptor order side by side).
6094
+
6095
+ ```ts
6096
+ // app.config.ts
6097
+ export default {
6098
+ type: 'api' as const,
6099
+ name: 'api',
6100
+ grpc: {
6101
+ port: 50051,
6102
+ procedures: ['orders.get', 'orders.list', 'orders.create'],
6103
+ // tls: { certPath, keyPath, caPath? } — plaintext without it (dev / mesh).
6104
+ },
6105
+ }
6106
+ ```
6107
+
6108
+ NOTHING is exposed by default — every tag is named. Booting writes
6109
+ `.framework/grpc.proto` (hand it to any proto codegen) and mounts
6110
+ `grpc.health.v1` health checking plus server reflection (`grpcurl … list`
6111
+ works out of the box). The gRPC packages ship as script-free optional
6112
+ dependencies of `@voltro/cli`; a configured `grpc:` block with them missing
6113
+ refuses the boot by name.
6114
+
6115
+ ## Field numbers are managed — `grpc.manifest.json`
6116
+
6117
+ Field numbers are the proto wire identity, so they may never depend on
6118
+ property order. They come from a checked-in manifest in your app root:
6119
+
6120
+ - a **new** field gets the next never-used number — an **inserted** field
6121
+ never renumbers its neighbours;
6122
+ - a **deleted** field's number becomes `reserved` (emitted into the proto,
6123
+ so `protoc` refuses a colliding hand-edit too);
6124
+ - **reusing** a reserved number is a codegen error, never a warning — an old
6125
+ client would silently read the wrong field.
6126
+
6127
+ Commit the manifest with the schema change that moved it: the diff review IS
6128
+ the wire-contract review.
6129
+
6130
+ ## The mapping table
6131
+
6132
+ | Schema | proto3 |
6133
+ |---|---|
6134
+ | `Schema.String` / `Number` / `Boolean` | `string` / `double` / `bool` |
6135
+ | integer schemas | `int64` |
6136
+ | `Schema.Array(T)` | `repeated T` |
6137
+ | nested `Schema.Struct` | nested message |
6138
+ | `Schema.Record({ key: String, value: T })` | `map<string, T>` |
6139
+ | `Schema.optional(T)` **and** `Schema.NullOr(T)` | `optional T` — absent and `null` are ONE wire state (proto3 presence) |
6140
+ | string-literal unions | `string` (validated server-side on decode) |
6141
+ | unions of shapes, tuples, recursion, free-form objects | a LOUD per-procedure codegen error naming the schema path |
6142
+
6143
+ Requests are decoded against the descriptor's input schema before the
6144
+ executor runs — proto3 suppresses default values on the wire, and without
6145
+ that decode an empty string would arrive as an absent field and fail
6146
+ somewhere much later.
6147
+
6148
+ ## Status codes — complete against the wire error union
6149
+
6150
+ | outcome | gRPC status | trailers |
6151
+ |---|---|---|
6152
+ | no credential on a guarded call | `UNAUTHENTICATED` | |
6153
+ | presented-and-rejected credential | `UNAUTHENTICATED` | |
6154
+ | authenticated, missing scope (`ScopeError`) | `PERMISSION_DENIED` | `voltro-error: scope` |
6155
+ | input fails the schema | `INVALID_ARGUMENT` | `voltro-error: input` |
6156
+ | `BusinessRuleViolation` | `FAILED_PRECONDITION` | `voltro-error: rule` |
6157
+ | `requiresApproval` pending — a FLOW OUTCOME, not a failure | `FAILED_PRECONDITION` | `voltro-pending: approval` + `voltro-approval-id` |
6158
+ | your declared typed error | `FAILED_PRECONDITION` | `voltro-error: <tag>` |
6159
+ | deadline exceeded | `DEADLINE_EXCEEDED` | |
6160
+ | anything else | `INTERNAL` | |
6161
+
6162
+ **Deadlines interrupt the work.** A client deadline (`grpc-timeout`) aborts
6163
+ the executor's fiber through the request signal — the server stops doing the
6164
+ work, it does not merely suppress the response (the e2e pins this with a
6165
+ sleeping action whose post-sleep write never lands).
6166
+
6167
+ ## Streaming queries
6168
+
6169
+ A `query` becomes a **server-streaming** rpc: each frame is the CURRENT full
6170
+ snapshot, re-pushed live when the query's `source:` changes — subscribe,
6171
+ mutate from anywhere, and the open stream receives the new frame with no
6172
+ re-request. The per-delivery guard re-check applies (a revoked scope ends
6173
+ the stream with the mapped status), and slow consumers are handled through
6174
+ grpc-js write backpressure — frames coalesce to the latest snapshot rather
6175
+ than buffering unboundedly.
6176
+
6177
+ ## Declared limits (v1)
6178
+
6179
+ - **No client- or bidi-streaming**, and `*.stream.ts` procedures are NOT
6180
+ exposable — the fourth kind is a one-shot element stream with its own
6181
+ semantics; put it behind a query or keep it on the socket.
6182
+ - **No gRPC-Web** — a browser talks the framework's own subscription
6183
+ protocol (that is the better browser transport in every dimension we care
6184
+ about); gRPC is for backends.
6185
+ - **No Connect protocol** — connectrpc is NOT gRPC-Web; a connect consumer's
6186
+ alternative today is the [REST/OpenAPI projection](/docs/data/rest-routes).
6187
+ - App realtime stays on the framework's subscription protocol, and gateways
6188
+ exist for the other case: a FOREIGN protocol that needs a socket the
6189
+ framework does not speak — the same boundary
6190
+ [data/subscriptions](/docs/data/subscriptions) draws for raw WebSocket
6191
+ gateways, one sentence, two doors.
@@ -379,7 +379,7 @@ Emits `CONSTRAINT <name> UNIQUE (col1, col2, ...)` inline in CREATE
379
379
  TABLE on every dialect. Standard SQL.
380
380
 
381
381
  This is what backs `ctx.store.upsert(..., { conflictColumns: ['a', 'b'] })`
382
- — see [Bulk operations](/docs/database/bulk-operations#upsert).
382
+ — see [Bulk operations](/docs/database/bulk-operations#upsert-insert-or-update-on-conflict).
383
383
 
384
384
  ## GiST indexes (PostGIS spatial)
385
385
 
@@ -689,7 +689,7 @@ mssql, sqlite 3.8+).
689
689
  depend on?"
690
690
 
691
691
  When you don't have a recursive structure, plain
692
- [`withCte()`](/docs/database/query-builder#ctes) is enough.
692
+ [`withCte()`](/docs/database/query-builder#ctes-common-table-expressions) is enough.
693
693
 
694
694
  ## Shape
695
695
 
@@ -820,7 +820,7 @@ eager-load with `.with({...})` to get the per-field pre-filter.
820
820
 
821
821
  ## See also
822
822
 
823
- - [Plain CTEs](/docs/database/query-builder#ctes) — `withCte()` for
823
+ - [Plain CTEs](/docs/database/query-builder#ctes-common-table-expressions) — `withCte()` for
824
824
  non-recursive named sub-queries
825
825
  - [Self-joins](/docs/database/self-joins) — for single-level
826
826
  parent/child queries
@@ -830,7 +830,7 @@ eager-load with `.with({...})` to get the per-field pre-filter.
830
830
  `unionAll`, the mechanism a recursive CTE is built on
831
831
  - [Joins](/docs/database/joins) — relation-based traversal when the
832
832
  graph depth is fixed (e.g. parent + immediate children)
833
- - [Aggregations](/docs/database/query-builder#aggregations) —
833
+ - [Aggregations](/docs/database/query-builder#aggregates) —
834
834
  COUNT/SUM/AVG over a recursive CTE's result set
835
835
 
836
836
 
@@ -940,7 +940,7 @@ keep the branches' result sets bounded.
940
940
  the column-level "in A but not in B" case
941
941
  - [Aggregations](/docs/database/aggregations) — `count()` etc. on
942
942
  a set-op result is a common pattern
943
- - [CTEs](/docs/database/query-builder#ctes) — name a complex set-op
943
+ - [CTEs](/docs/database/query-builder#ctes-common-table-expressions) — name a complex set-op
944
944
  result so you can reference it in a larger query
945
945
 
946
946
 
@@ -1044,7 +1044,7 @@ Non-correlated only. The inner query can NOT reference outer-row
1044
1044
  columns like `WHERE inner.userId = users.id`. For correlated
1045
1045
  sub-queries (a common shape: "user who has at least one post created
1046
1046
  in the last hour") use a [Self-join](/docs/database/self-joins) or
1047
- an [Eager-load](/docs/database/joins#eager-loading) — both can
1047
+ an [Eager-load](/docs/database/joins#eager-loading-via-with-spec) — both can
1048
1048
  express the same query without the correlation reference.
1049
1049
 
1050
1050
  ## Reactivity
@@ -1069,7 +1069,7 @@ every dialect we ship. No per-dialect dispatch.
1069
1069
  with `count()` etc. for "count of X where Y belongs to Z"
1070
1070
  - [Self-joins](/docs/database/self-joins) — when the relationship
1071
1071
  can be expressed as a join instead
1072
- - [CTEs](/docs/database/query-builder#ctes) — for naming a
1072
+ - [CTEs](/docs/database/query-builder#ctes-common-table-expressions) — for naming a
1073
1073
  sub-query you reuse multiple times in the same outer query
1074
1074
 
1075
1075
 
@@ -350,7 +350,7 @@ on it explicitly via `.expressionIndex(name, [...], { ... })`.
350
350
 
351
351
  - [Columns](/docs/database/columns) — `.computed(row => ...)` and
352
352
  `.default(() => ...)` for the app-side variants
353
- - [Indexes](/docs/database/indexes#expression) — `.expressionIndex()`
353
+ - [Indexes](/docs/database/indexes#expression-indexes) — `.expressionIndex()`
354
354
  for indexing a generated column
355
355
  - [Full-text search](/docs/database/full-text-search) — the FTS
356
356
  pattern uses STORED tsvector generated columns
@@ -989,7 +989,7 @@ behavioural gaps are too large to paper over.
989
989
  ## See also
990
990
 
991
991
  - [Columns](/docs/database/columns) — the regular schema-DSL types
992
- - [Expression indexes](/docs/database/indexes#expression) — the
992
+ - [Expression indexes](/docs/database/indexes#expression-indexes) — the
993
993
  framework's index API (`kind: 'gist' | 'gin'` on postgres)
994
994
  - [PostGIS docs](https://postgis.net/docs/) — the official manual,
995
995
  authoritative for every spatial function the framework re-exports
@@ -309,7 +309,7 @@ The query builder has dedicated pages for the deeper topics:
309
309
  - **[Self-joins](/docs/database/self-joins)** — `.as(alias)` +
310
310
  `.innerJoin(table, alias, on)` + `.selectJoined({...})` for parent/
311
311
  child trees and CTE references.
312
- - **[CTEs](/docs/database/query-builder#ctes)** — `.withCte(name, sub)`
312
+ - **[CTEs](/docs/database/query-builder#ctes-common-table-expressions)** — `.withCte(name, sub)`
313
313
  for named sub-queries reusable inside the outer SELECT.
314
314
  - **[Recursive CTEs](/docs/database/recursive-cte)** — `.recursiveCte`
315
315
  for tree walks (org hierarchy, comment threads, file folders).
@@ -445,7 +445,7 @@ framework runs on the post-commit change channel.
445
445
  **Reach for `reference()` first.** A real foreign key across a plugin boundary
446
446
  works and survives the plugin renaming its table, because `reference()` takes
447
447
  the table as a VALUE — see
448
- [plugins/overview](/docs/plugins/overview#pointing-your-table-at-a-plugins-row).
448
+ [plugins/overview](/docs/plugins/overview).
449
449
  `pluginRef` is for the case where you have deliberately chosen NOT to have a
450
450
  key: it enforces nothing at the database level.
451
451