@voltro/cli 0.53.0 → 0.55.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 (191) hide show
  1. package/CHANGELOG.md +335 -0
  2. package/dist/{agentsMd-0l980yhL.js → agentsMd-BaLC10Na.js} +110 -82
  3. package/dist/agentsMd-DCY1RSs8.js +2 -0
  4. package/dist/{apiBuild-CaPfoWku.js → apiBuild-CMvLJM_K.js} +2 -2
  5. package/dist/apiBuild-Cl0IDx8c.js +2 -0
  6. package/dist/bin.js +1 -1
  7. package/dist/{build-D-OnvNMf.js → build-S0QOzqPT.js} +115 -115
  8. package/dist/{checkCommand-D2ZduVlh.js → checkCommand-DNkY5kwF.js} +1 -1
  9. package/dist/{checkCommand-C5elt0tW.js → checkCommand-fbj9GDjN.js} +6 -6
  10. package/dist/{cloudCmd-QUXh-b5w.js → cloudCmd-DzKcSYuy.js} +1 -1
  11. package/dist/codegen-CN6vMM4J.js +2 -0
  12. package/dist/{codegen-FEk8AZHb.js → codegen-SIepQtUl.js} +76 -65
  13. package/dist/codegenCommand-3TDJezom.js +42 -0
  14. package/dist/{codemodRunner-BjtB2lq6.js → codemodRunner-C2zxZUIw.js} +64 -9
  15. package/dist/{commands-DyxAmhP0.js → commands-BBYJ7Q3B.js} +96 -73
  16. package/dist/{dashboardCommand-BdKTyT13.js → dashboardCommand-D2kmyCLL.js} +3 -3
  17. package/dist/{dataCommand-Bab9X7s8.js → dataCommand-BEPPQiTl.js} +267 -195
  18. package/dist/dbCommand-BTyBGhIA.js +2 -0
  19. package/dist/{dbCommand-06O2finM.js → dbCommand-DZTmOFT4.js} +3 -3
  20. package/dist/{dev-C6LGF4iY.js → dev-Ca_A_S9v.js} +2439 -2397
  21. package/dist/{dev-GjJWAYo2.js → dev-DfVZaoys.js} +1 -1
  22. package/dist/{doctorCommand-etMkflRc.js → doctorCommand-CGZJK_4o.js} +21 -21
  23. package/dist/doctorCommand-djmqEcDC.js +2 -0
  24. package/dist/{dormancyCommand-UwZ1AZzB.js → dormancyCommand-DY2rYpTa.js} +1 -1
  25. package/dist/{embeddingsCommand-C70zWHwo.js → embeddingsCommand-BoCqZsgp.js} +1 -1
  26. package/dist/{envCommand-dSyKvRkM.js → envCommand-Bxy2fOjc.js} +15 -15
  27. package/dist/{evolveCommand-CG0_ebO5.js → evolveCommand-BsbZ-XDg.js} +2 -2
  28. package/dist/fileConventions-l-RIXbx8.js +36 -0
  29. package/dist/{fileTaxonomy-B7uxipWS.js → fileTaxonomy-CbyMQYx_.js} +37 -37
  30. package/dist/frameworkTableAssembly-Df2Ymp2f.js +2 -0
  31. package/dist/{frameworkTableAssembly-DKx3ba3S.js → frameworkTableAssembly-Do-cf6RJ.js} +96 -102
  32. package/dist/index.js +2 -2
  33. package/dist/{infoCommand-_53iOc_j.js → infoCommand-EmM3jPKD.js} +1 -1
  34. package/dist/{inspect-Bd8-9wsi.js → inspect-DCqILJ1G.js} +4 -0
  35. package/dist/inspect-DGJwpOAb.js +2 -0
  36. package/dist/interruptedReplace-CwnkBb2X.js +41 -0
  37. package/dist/interruptedReplace-qzmFI020.js +2 -0
  38. package/dist/manifestBuild-CJ2zvPvT.js +2 -0
  39. package/dist/{manifestBuild-Cqgsx2bM.js → manifestBuild-DjX5MoXy.js} +1 -1
  40. package/dist/{metaCommands-Cn2oboG4.js → metaCommands-x7RCi2AF.js} +2 -2
  41. package/dist/{migrate-Cko9rswM.js → migrate-CGFZS-1a.js} +2 -2
  42. package/dist/mobileCommand-D9O6iq3D.js +428 -0
  43. package/dist/mobileCommand-DAum7tsG.js +2 -0
  44. package/dist/{pageConvention-C938S8oC.js → pageConvention-CMpfDN6r.js} +1 -1
  45. package/dist/{privacyCommand-DWTQMC6R.js → privacyCommand-BCa2OoZG.js} +2 -2
  46. package/dist/{probeCommand-DkGGLknv.js → probeCommand-Bs3iVBSL.js} +1 -1
  47. package/dist/{projectScaffold-EzlErR4E.js → projectScaffold-CJfP-xbT.js} +1 -1
  48. package/dist/{projectScaffold-B4dmTlwT.js → projectScaffold-CSN0OzBV.js} +2 -2
  49. package/dist/renderModeScan-43yQ2opo.js +147 -0
  50. package/dist/{renderProfile-CskIgAfn.js → renderProfile-DvrhVJHa.js} +2 -2
  51. package/dist/{runtimeTrace-c0APJz7E.js → runtimeTrace-C1BTpHGQ.js} +1 -1
  52. package/dist/{sdkgen-BiQCgIEr.js → sdkgen-CXMwLg9n.js} +1 -1
  53. package/dist/{serveCommand-CueKQgzl.js → serveCommand-C7IrCD58.js} +899 -897
  54. package/dist/serveCommand-Cjt5S9hD.js +2 -0
  55. package/dist/serveEntry.js +1 -1
  56. package/dist/start-DH7cat4-.js +3 -0
  57. package/dist/{start-ekPan8BT.js → start-EOV7s1NZ.js} +544 -527
  58. package/dist/startEntry.js +1 -1
  59. package/dist/{staticCommand-xlSL-IWk.js → staticCommand-ey0kYmOT.js} +1 -1
  60. package/dist/{subcommandNames-DpYs3DXr.js → subcommandNames-CDzfEtKV.js} +3 -3
  61. package/dist/{templates-BR-fb4SP.js → templates-BTWZkJJT.js} +41 -9
  62. package/dist/{test-BWPQcRoB.js → test-DO27-x2P.js} +1 -1
  63. package/dist/{updateCommand-C_8I8Rzo.js → updateCommand-C_jN1w18.js} +1 -1
  64. package/dist/updateCommand-nnFjDbl4.js +2 -0
  65. package/dist/{webDev-C7jWJ5dX.js → webDev-1XpVnYkW.js} +1 -1
  66. package/dist/{webDev-oczpugbx.js → webDev-B7vNj4Bq.js} +1231 -1186
  67. package/dist/{webhooksCommand-4SVPDjKg.js → webhooksCommand-B1LVcyO3.js} +1 -1
  68. package/dist/workspaceDeps-RKEkX92S.js +45 -0
  69. package/package.json +31 -19
  70. package/templates/AGENTS.core.md +2 -0
  71. package/templates/AGENTS.md +4 -2
  72. package/templates/agent-docs/_index.md +2 -2
  73. package/templates/agent-docs/_manifest.json +1 -1
  74. package/templates/agent-docs/ai.md +4 -4
  75. package/templates/agent-docs/authentication.md +115 -0
  76. package/templates/agent-docs/cli.md +98 -12
  77. package/templates/agent-docs/data.md +121 -21
  78. package/templates/agent-docs/database/advancedqueries.md +1 -1
  79. package/templates/agent-docs/database/migrations.md +1 -1
  80. package/templates/agent-docs/database/seedsdialects.md +64 -2
  81. package/templates/agent-docs/internationalization.md +2 -0
  82. package/templates/agent-docs/introduction.md +25 -0
  83. package/templates/agent-docs/local-first-mobile.md +139 -41
  84. package/templates/agent-docs/observability.md +4 -2
  85. package/templates/agent-docs/plugins/atlassian.md +2 -2
  86. package/templates/agent-docs/plugins/audit.md +2 -2
  87. package/templates/agent-docs/plugins/billing.md +1 -1
  88. package/templates/agent-docs/plugins/cdc-out.md +8 -3
  89. package/templates/agent-docs/plugins/comments.md +22 -0
  90. package/templates/agent-docs/plugins/presence.md +32 -3
  91. package/templates/agent-docs/plugins/prometheus.md +2 -0
  92. package/templates/agent-docs/plugins/queue.md +47 -4
  93. package/templates/agent-docs/plugins.md +52 -14
  94. package/templates/agent-docs/reference.md +25 -4
  95. package/templates/agent-docs/routing.md +81 -9
  96. package/templates/agent-docs/scheduling.md +1 -1
  97. package/templates/agent-docs/schema-driven-ui.md +137 -1
  98. package/templates/agent-docs/templates/appshells.md +36 -4
  99. package/templates/agent-docs/whats-new.md +75 -158
  100. package/templates/apps/api-ai/package.json +6 -6
  101. package/templates/apps/api-auth/package.json +8 -8
  102. package/templates/apps/api-backend/package.json +7 -7
  103. package/templates/apps/api-backend-deactivation/package.json +7 -7
  104. package/templates/apps/api-backend-mail/package.json +8 -8
  105. package/templates/apps/api-backend-mariadb/package.json +9 -9
  106. package/templates/apps/api-backend-sqlite/package.json +8 -8
  107. package/templates/apps/api-backend-storage/package.json +8 -8
  108. package/templates/apps/api-cms/package.json +9 -9
  109. package/templates/apps/api-collab/README.md +3 -3
  110. package/templates/apps/api-collab/app.config.ts +1 -1
  111. package/templates/apps/api-collab/database/schema.ts +12 -8
  112. package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +1 -1
  113. package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +1 -1
  114. package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +1 -1
  115. package/templates/apps/api-collab/package.json +8 -8
  116. package/templates/apps/api-collab/template.json +1 -1
  117. package/templates/apps/api-collab/tests/documents.setBody.test.ts +10 -2
  118. package/templates/apps/api-data-advanced/package.json +8 -8
  119. package/templates/apps/api-durable/package.json +8 -8
  120. package/templates/apps/api-feature-flags/package.json +9 -9
  121. package/templates/apps/api-governance/package.json +8 -8
  122. package/templates/apps/api-kv/package.json +8 -8
  123. package/templates/apps/api-moderation/package.json +8 -8
  124. package/templates/apps/api-observability/package.json +8 -8
  125. package/templates/apps/api-ratelimit/package.json +8 -8
  126. package/templates/apps/api-rbac/package.json +8 -8
  127. package/templates/apps/api-rest/package.json +7 -7
  128. package/templates/apps/api-row-history/package.json +8 -8
  129. package/templates/apps/api-saas/package.json +11 -10
  130. package/templates/apps/api-saas-starter/package.json +10 -10
  131. package/templates/apps/api-search/package.json +8 -8
  132. package/templates/apps/api-status/package.json +8 -8
  133. package/templates/apps/api-webhooks/package.json +9 -9
  134. package/templates/apps/changelog/package.json +7 -6
  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/README.md +43 -24
  142. package/templates/apps/frontend-collab/app.config.ts +3 -3
  143. package/templates/apps/frontend-collab/package.json +14 -10
  144. package/templates/apps/frontend-collab/src/locales/de.ts +1 -2
  145. package/templates/apps/frontend-collab/src/locales/en.ts +1 -2
  146. package/templates/apps/frontend-collab/src/pages/page.test.tsx +72 -76
  147. package/templates/apps/frontend-collab/src/pages/page.tsx +45 -22
  148. package/templates/apps/frontend-collab/template.json +2 -2
  149. package/templates/apps/frontend-contact/package.json +7 -7
  150. package/templates/apps/frontend-dashboard/package.json +7 -7
  151. package/templates/apps/frontend-docs/package.json +8 -7
  152. package/templates/apps/frontend-i18n/package.json +6 -6
  153. package/templates/apps/frontend-landing/README.md +48 -0
  154. package/templates/apps/frontend-landing/app.config.ts +28 -0
  155. package/templates/apps/frontend-landing/package.json +7 -6
  156. package/templates/apps/frontend-landing/src/assets/hero.jpg +0 -0
  157. package/templates/apps/frontend-landing/src/fonts/Geist-Variable.woff2 +0 -0
  158. package/templates/apps/frontend-landing/src/fonts/LICENSE-Geist.txt +92 -0
  159. package/templates/apps/frontend-landing/src/globals.css +15 -0
  160. package/templates/apps/frontend-landing/src/globals.d.ts +17 -0
  161. package/templates/apps/frontend-landing/src/locales/de.ts +3 -2
  162. package/templates/apps/frontend-landing/src/locales/en.ts +5 -2
  163. package/templates/apps/frontend-landing/src/pages/page.test.tsx +79 -0
  164. package/templates/apps/frontend-landing/src/pages/page.tsx +26 -3
  165. package/templates/apps/frontend-landing/template.json +2 -2
  166. package/templates/apps/frontend-portal/package.json +8 -8
  167. package/templates/apps/frontend-saas/package.json +8 -8
  168. package/templates/apps/frontend-spa/package.json +7 -7
  169. package/templates/apps/frontend-ssr/package.json +7 -7
  170. package/templates/apps/frontend-ssr-api/package.json +8 -8
  171. package/templates/apps/frontend-static-blog/package.json +8 -7
  172. package/templates/apps/frontend-status/package.json +8 -8
  173. package/templates/apps/mobile-app/package.json +12 -11
  174. package/templates/apps/mobile-app/src/lib/deeplinks.ts +29 -17
  175. package/templates/apps/mobile-app/tests/deeplinks.test.ts +16 -0
  176. package/dist/agentsMd-SDDSkyl4.js +0 -2
  177. package/dist/apiBuild-DHtLXYx9.js +0 -2
  178. package/dist/codegen-BWpt3VgF.js +0 -2
  179. package/dist/codegenCommand-BOiWQ5hz.js +0 -137
  180. package/dist/dbCommand-B1EXBC6f.js +0 -2
  181. package/dist/doctorCommand-B0hX0tdz.js +0 -2
  182. package/dist/fileConventions-DASGEmj-.js +0 -35
  183. package/dist/frameworkTableAssembly-C_7Z-rMs.js +0 -2
  184. package/dist/inspect-CuoDInfZ.js +0 -2
  185. package/dist/interruptedReplace-C3O3M1MM.js +0 -28
  186. package/dist/interruptedReplace-CvmiAM9K.js +0 -2
  187. package/dist/manifestBuild-C4-J1-m_.js +0 -2
  188. package/dist/renderModeScan-CUbOeOAg.js +0 -122
  189. package/dist/serveCommand-DsnrVN3U.js +0 -2
  190. package/dist/start-BJzZLbt8.js +0 -3
  191. package/dist/updateCommand-Bqql_rsQ.js +0 -2
@@ -222,7 +222,7 @@ createVerifier({ secret: [process.env.WEBHOOK_SECRET, process.env.WEBHOOK_SECRET
222
222
  ...t === void 0 ? {} : { payload: t }
223
223
  };
224
224
  }, S = u({ scope: "voltro:webhooks" }), C = ["--out", "--name"], w = async (e) => {
225
- let { walk: t, loadDiscovered: n } = await import("./dev-GjJWAYo2.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-il9ti-HE.js");
225
+ let { walk: t, loadDiscovered: n } = await import("./dev-DfVZaoys.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-il9ti-HE.js");
226
226
  return r((await n(await t(e))).events.map((e) => ({
227
227
  file: e.file,
228
228
  descriptor: e.descriptor
@@ -0,0 +1,45 @@
1
+ import { dirname as e, join as t } from "node:path";
2
+ import { promises as n } from "node:fs";
3
+ //#region src/workspaceDeps.ts
4
+ var r = async (r, i) => {
5
+ let a = r;
6
+ for (;;) {
7
+ let r = t(a, "node_modules", i);
8
+ try {
9
+ if ((await n.stat(r)).isDirectory()) {
10
+ let e = await n.realpath(r);
11
+ return e.includes("/node_modules/") ? null : e;
12
+ }
13
+ } catch {}
14
+ let o = e(a);
15
+ if (o === a) return null;
16
+ a = o;
17
+ }
18
+ }, i = async (e) => {
19
+ let i;
20
+ try {
21
+ let r = await n.readFile(t(e, "package.json"), "utf8"), a = JSON.parse(r);
22
+ i = Object.keys({
23
+ ...a.dependencies,
24
+ ...a.devDependencies
25
+ });
26
+ } catch {
27
+ return [];
28
+ }
29
+ return (await Promise.all(i.map(async (i) => {
30
+ let a = await r(e, i);
31
+ if (a === null) return null;
32
+ let o = t(a, "src");
33
+ try {
34
+ if (!(await n.stat(o)).isDirectory()) return null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ return {
39
+ pkg: i,
40
+ dir: o
41
+ };
42
+ }))).filter((e) => e !== null);
43
+ };
44
+ //#endregion
45
+ export { i as n, r as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/cli",
3
- "version": "0.53.0",
3
+ "version": "0.55.0",
4
4
  "description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -754,6 +754,18 @@
754
754
  "title": "WidgetKind gained 'array' and 'reference' — total widget registries need two new entries",
755
755
  "kind": "manual"
756
756
  },
757
+ {
758
+ "version": "0.55.0",
759
+ "id": "0.55.0/01_target-relations-declare-columns",
760
+ "title": "A target's `relations:` names the junction's two columns now, not just the table",
761
+ "kind": "manual"
762
+ },
763
+ {
764
+ "version": "0.55.0",
765
+ "id": "0.55.0/02_widget-kind-gained-rich-text",
766
+ "title": "WidgetKind gained 'rich-text' — total widget registries need one new entry",
767
+ "kind": "manual"
768
+ },
757
769
  {
758
770
  "version": "0.6.0",
759
771
  "id": "0.6.0/01_no-dev-session-secret",
@@ -836,24 +848,24 @@
836
848
  "@effect/platform-node": "^0.108.0",
837
849
  "@effect/sql": "^0.52.0",
838
850
  "@effect/workflow": "^0.19.0",
839
- "@voltro/ai": "0.53.0",
840
- "@voltro/cache": "0.53.0",
841
- "@voltro/client": "0.53.0",
842
- "@voltro/content": "0.53.0",
843
- "@voltro/data-transfer": "0.53.0",
844
- "@voltro/database": "0.53.0",
845
- "@voltro/env": "0.53.0",
846
- "@voltro/kv": "0.53.0",
847
- "@voltro/logger": "0.53.0",
848
- "@voltro/plugin-auth": "0.53.0",
849
- "@voltro/plugin-broadcast": "0.53.0",
850
- "@voltro/plugin-mail": "0.53.0",
851
- "@voltro/plugin-storage": "0.53.0",
852
- "@voltro/plugin-webhooks": "0.53.0",
853
- "@voltro/protocol": "0.53.0",
854
- "@voltro/runtime": "0.53.0",
855
- "@voltro/serverless": "0.53.0",
856
- "@voltro/workflow": "0.53.0",
851
+ "@voltro/ai": "0.55.0",
852
+ "@voltro/cache": "0.55.0",
853
+ "@voltro/client": "0.55.0",
854
+ "@voltro/content": "0.55.0",
855
+ "@voltro/data-transfer": "0.55.0",
856
+ "@voltro/database": "0.55.0",
857
+ "@voltro/env": "0.55.0",
858
+ "@voltro/kv": "0.55.0",
859
+ "@voltro/logger": "0.55.0",
860
+ "@voltro/plugin-auth": "0.55.0",
861
+ "@voltro/plugin-broadcast": "0.55.0",
862
+ "@voltro/plugin-mail": "0.55.0",
863
+ "@voltro/plugin-storage": "0.55.0",
864
+ "@voltro/plugin-webhooks": "0.55.0",
865
+ "@voltro/protocol": "0.55.0",
866
+ "@voltro/runtime": "0.55.0",
867
+ "@voltro/serverless": "0.55.0",
868
+ "@voltro/workflow": "0.55.0",
857
869
  "chokidar": "^5.0.0",
858
870
  "ioredis": "^5.11.1",
859
871
  "tinyglobby": "^0.2.17",
@@ -368,6 +368,8 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
368
368
  | `*.agent.tsx` + `*.agent.server.tsx` | server-side LLM chat |
369
369
  | `*.tool.tsx` | tool an agent can call |
370
370
  | `*.entity.ts` / `*.schema.ts` / `schema.ts` | one table per file |
371
+ | `*.collection.ts` | content collection (`defineCollection`) — schema-typed markdown/JSON |
372
+ | `content/<name>/**` | that collection's files (markdown + frontmatter, or `.json`) |
371
373
  | `page.tsx` (under `src/pages/`) | the route its DIRECTORY serves + `page.test.tsx` |
372
374
  | `*.component.tsx` | exactly ONE component (+ types) |
373
375
  | `*.component.ui.tsx` | presentational: one component, READS only — never writes |
@@ -368,6 +368,8 @@ public REST routes, declared via `restRoutes` in `app.config.ts`).
368
368
  | `*.agent.tsx` + `*.agent.server.tsx` | server-side LLM chat |
369
369
  | `*.tool.tsx` | tool an agent can call |
370
370
  | `*.entity.ts` / `*.schema.ts` / `schema.ts` | one table per file |
371
+ | `*.collection.ts` | content collection (`defineCollection`) — schema-typed markdown/JSON |
372
+ | `content/<name>/**` | that collection's files (markdown + frontmatter, or `.json`) |
371
373
  | `page.tsx` (under `src/pages/`) | the route its DIRECTORY serves + `page.test.tsx` |
372
374
  | `*.component.tsx` | exactly ONE component (+ types) |
373
375
  | `*.component.ui.tsx` | presentational: one component, READS only — never writes |
@@ -728,7 +730,7 @@ each plugin's own README.
728
730
 
729
731
  | Topic | Open | Summary |
730
732
  |---|---|---|
731
- | **What's new in 0.53.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
733
+ | **What's new in 0.55.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
732
734
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
733
735
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
734
736
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -786,7 +788,7 @@ each plugin's own README.
786
788
  | auth-workos | `node_modules/@voltro/cli/templates/agent-docs/plugins/auth-workos.md` (or `node_modules/@voltro/plugin-auth-workos/README.md`) | WorkOS AuthStrategy — verifies WorkOS AuthKit / SSO JWTs via JWKS (no API key) and maps org_id → tenantId. |
787
789
  | billing | `node_modules/@voltro/cli/templates/agent-docs/plugins/billing.md` (or `node_modules/@voltro/plugin-billing/README.md`) | Subscriptions, plans, entitlements, and usage metering over a pluggable provider (Stripe + mock). Money is integer minor units. |
788
790
  | broadcast | `node_modules/@voltro/cli/templates/agent-docs/plugins/broadcast.md` (or `node_modules/@voltro/plugin-broadcast/README.md`) | Cross-replica reactivity over a pub/sub bus (Redis / NATS) for non-postgres dialects — closes the single-instance gap so a write on one pod surfaces on another. |
789
- | cdc-out | `node_modules/@voltro/cli/templates/agent-docs/plugins/cdc-out.md` (or `node_modules/@voltro/plugin-cdc-out/README.md`) | Declaratively mirror table changes outward to external sinks (webhook, plus a CdcSink interface for custom sinks) through a durable outbox — ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered. |
791
+ | cdc-out | `node_modules/@voltro/cli/templates/agent-docs/plugins/cdc-out.md` (or `node_modules/@voltro/plugin-cdc-out/README.md`) | Declaratively mirror table changes outward to external sinks (webhook, Kafka, plus a CdcSink interface for custom sinks) through a durable outbox — ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered. |
790
792
  | clickhouse | `node_modules/@voltro/cli/templates/agent-docs/plugins/clickhouse.md` (or `node_modules/@voltro/plugin-clickhouse/README.md`) | Production-grade OLAP AnalyticsSink over ClickHouse — self-hosted or ClickHouse Cloud — for billions of events with millisecond aggregates. |
791
793
  | comments | `node_modules/@voltro/cli/templates/agent-docs/plugins/comments.md` (or `node_modules/@voltro/plugin-comments/README.md`) | Comment threads on any app entity — replies, resolve/reopen, @-mentions with notifications, reactions, unread counters — live over the reactive engine, with an ejectable thread UI. |
792
794
  | datadog | `node_modules/@voltro/cli/templates/agent-docs/plugins/datadog.md` (or `node_modules/@voltro/plugin-datadog/README.md`) | Agentless Datadog metrics exporter — pushes the unified Metrics-API to Datadog's /api/v2/series HTTP intake. |
@@ -9,7 +9,7 @@ each plugin's own README.
9
9
 
10
10
  | Topic | Open | Summary |
11
11
  |---|---|---|
12
- | **What's new in 0.53.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
12
+ | **What's new in 0.55.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
13
13
  | AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
14
14
  | Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
15
15
  | Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
@@ -67,7 +67,7 @@ each plugin's own README.
67
67
  | auth-workos | `node_modules/@voltro/cli/templates/agent-docs/plugins/auth-workos.md` (or `node_modules/@voltro/plugin-auth-workos/README.md`) | WorkOS AuthStrategy — verifies WorkOS AuthKit / SSO JWTs via JWKS (no API key) and maps org_id → tenantId. |
68
68
  | billing | `node_modules/@voltro/cli/templates/agent-docs/plugins/billing.md` (or `node_modules/@voltro/plugin-billing/README.md`) | Subscriptions, plans, entitlements, and usage metering over a pluggable provider (Stripe + mock). Money is integer minor units. |
69
69
  | broadcast | `node_modules/@voltro/cli/templates/agent-docs/plugins/broadcast.md` (or `node_modules/@voltro/plugin-broadcast/README.md`) | Cross-replica reactivity over a pub/sub bus (Redis / NATS) for non-postgres dialects — closes the single-instance gap so a write on one pod surfaces on another. |
70
- | cdc-out | `node_modules/@voltro/cli/templates/agent-docs/plugins/cdc-out.md` (or `node_modules/@voltro/plugin-cdc-out/README.md`) | Declaratively mirror table changes outward to external sinks (webhook, plus a CdcSink interface for custom sinks) through a durable outbox — ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered. |
70
+ | cdc-out | `node_modules/@voltro/cli/templates/agent-docs/plugins/cdc-out.md` (or `node_modules/@voltro/plugin-cdc-out/README.md`) | Declaratively mirror table changes outward to external sinks (webhook, Kafka, plus a CdcSink interface for custom sinks) through a durable outbox — ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered. |
71
71
  | clickhouse | `node_modules/@voltro/cli/templates/agent-docs/plugins/clickhouse.md` (or `node_modules/@voltro/plugin-clickhouse/README.md`) | Production-grade OLAP AnalyticsSink over ClickHouse — self-hosted or ClickHouse Cloud — for billions of events with millisecond aggregates. |
72
72
  | comments | `node_modules/@voltro/cli/templates/agent-docs/plugins/comments.md` (or `node_modules/@voltro/plugin-comments/README.md`) | Comment threads on any app entity — replies, resolve/reopen, @-mentions with notifications, reactions, unread counters — live over the reactive engine, with an ejectable thread UI. |
73
73
  | datadog | `node_modules/@voltro/cli/templates/agent-docs/plugins/datadog.md` (or `node_modules/@voltro/plugin-datadog/README.md`) | Agentless Datadog metrics exporter — pushes the unified Metrics-API to Datadog's /api/v2/series HTTP intake. |
@@ -460,7 +460,7 @@
460
460
  {
461
461
  "slug": "cdc-out",
462
462
  "title": "CDC-out (reverse-ETL)",
463
- "description": "Declaratively mirror table changes outward to external sinks (webhook, plus a CdcSink interface for custom sinks) through a durable outbox — ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered.",
463
+ "description": "Declaratively mirror table changes outward to external sinks (webhook, Kafka, plus a CdcSink interface for custom sinks) through a durable outbox — ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered.",
464
464
  "pkg": "@voltro/plugin-cdc-out",
465
465
  "doc": "plugins/cdc-out.md",
466
466
  "module": "agent-docs/plugins/cdc-out.md"
@@ -116,7 +116,7 @@ Env vars `providerFromEnv()` reads:
116
116
  | `AI_PROVIDER` | `mock` | `mock` \| `anthropic` \| `openai` \| `gateway`. |
117
117
  | `AI_MODEL` | per provider (see below) | Override the model. Defaults: `claude-opus-4-8` (anthropic), `gpt-5.5` (openai), `mock` (mock). **REQUIRED for `gateway`** — a `creator/model` id; boot throws if unset. |
118
118
 
119
- Provider API keys are read by the underlying `@ai-sdk/*` packages from their standard env vars — `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `AI_GATEWAY_API_KEY`. The framework doesn't read a separate `AI_API_KEY`. (To give a SINGLE agent its own key from code instead of env, see [Per-config key + base URL](#per-config-key--base-url) below.)
119
+ Provider API keys are read by the underlying `@ai-sdk/*` packages from their standard env vars — `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `AI_GATEWAY_API_KEY`. The framework doesn't read a separate `AI_API_KEY`. (To give a SINGLE agent its own key from code instead of env, see [Per-config key + base URL](#per-config-key-base-url) below.)
120
120
 
121
121
  ## Anthropic
122
122
 
@@ -141,7 +141,7 @@ AI_MODEL=gpt-5.5
141
141
  OPENAI_API_KEY=sk-…
142
142
  ```
143
143
 
144
- Backed by `@ai-sdk/openai`. `AI_MODEL` is a plain OpenAI model id (`gpt-5.5`, `gpt-4o-mini`, …). For a self-hosted / Azure-style / proxy endpoint, set `baseURL` on a `ProviderConfig` (see [Per-config key + base URL](#per-config-key--base-url)) rather than an env var.
144
+ Backed by `@ai-sdk/openai`. `AI_MODEL` is a plain OpenAI model id (`gpt-5.5`, `gpt-4o-mini`, …). For a self-hosted / Azure-style / proxy endpoint, set `baseURL` on a `ProviderConfig` (see [Per-config key + base URL](#per-config-key-base-url)) rather than an env var.
145
145
 
146
146
  ## Vercel AI Gateway
147
147
 
@@ -239,7 +239,7 @@ export default (input: { prompt: string }) =>
239
239
  })
240
240
  ```
241
241
 
242
- `GenerateTextOptions` is `{ prompt, system?, provider?, fallbacks?, maxTokens? }`. There is no `messages`/`effort` shape — the prompt is a single string the SDK wraps as the user turn; `system` steers it. `fallbacks` is a [provider fallback chain](#fallback-chain--survive-a-provider-outage).
242
+ `GenerateTextOptions` is `{ prompt, system?, provider?, fallbacks?, maxTokens? }`. There is no `messages`/`effort` shape — the prompt is a single string the SDK wraps as the user turn; `system` steers it. `fallbacks` is a [provider fallback chain](#fallback-chain-survive-a-provider-outage).
243
243
 
244
244
  Structured output:
245
245
 
@@ -276,7 +276,7 @@ type ProviderConfig =
276
276
  | { name: 'gateway'; model: `${string}/${string}`; apiKey?: string; baseURL?: string } // creator/model
277
277
  ```
278
278
 
279
- The mock-only `mockText` / `script` can't appear on a real provider (the type rejects it), the gateway's `model` is a `creator/model`-typed string (a bare `'gpt-5.5'` is a compile error, not a boot crash), and the per-provider model-id types (`AnthropicModel` / `OpenAIModel`) are **open unions** — known ids autocomplete, but any string the provider ships tomorrow still type-checks. Use `provider` for per-request model selection (e.g. a cheaper model on a fallback path), or to pin a specific key/endpoint (see [Per-config key + base URL](#per-config-key--base-url)).
279
+ The mock-only `mockText` / `script` can't appear on a real provider (the type rejects it), the gateway's `model` is a `creator/model`-typed string (a bare `'gpt-5.5'` is a compile error, not a boot crash), and the per-provider model-id types (`AnthropicModel` / `OpenAIModel`) are **open unions** — known ids autocomplete, but any string the provider ships tomorrow still type-checks. Use `provider` for per-request model selection (e.g. a cheaper model on a fallback path), or to pin a specific key/endpoint (see [Per-config key + base URL](#per-config-key-base-url)).
280
280
 
281
281
  For runtime provider switching across a whole layer, bind an `AiServiceImpl` to the `AiService` Context tag at boot and read it with `yield* AiService` — `defaultAiService` (backed by `providerFromEnv`) is the default.
282
282
 
@@ -2602,6 +2602,121 @@ A membership that ends mid-subscription therefore stops serving rows — the
2602
2602
  caller's open ticket list drops the rows they can no longer see, without a
2603
2603
  refresh and without the subscription having to be torn down.
2604
2604
 
2605
+ **On every transport, and that list is complete: the WebSocket, an [SSE
2606
+ stream](/docs/data/rest-routes#live-updates-over-http-stream-sse), and a
2607
+ [gRPC](/docs/data/grpc) server-streaming rpc.** All three open their
2608
+ subscription through the same code and resolve the filter per delivery, so
2609
+ "stays open for hours" never becomes a way to hold a stale predicate. The same
2610
+ delivery also re-checks the query's `guards:`; a filter resolution that FAILS
2611
+ revokes the subscription on all three rather than falling back to an unfiltered
2612
+ or empty read.
2613
+
2614
+ ## Declare which tables it narrows — `tables:`
2615
+
2616
+ Optional, one line, and it buys back a feature the filter otherwise switches
2617
+ off for the whole app:
2618
+
2619
+ ```ts
2620
+ const OWNED = new Set(['documents', 'comments'])
2621
+
2622
+ setRowFilter({
2623
+ load,
2624
+ predicate: (ctx, table) => (OWNED.has(table) ? eq('ownerId', ctx.userId) : undefined),
2625
+ // Derived from the same set the predicate reads. Two hand-kept lists is the
2626
+ // shape in which a table lands in exactly one of them.
2627
+ tables: [...OWNED],
2628
+ })
2629
+ ```
2630
+
2631
+ **What it buys.** [Delta-resume](/docs/data/wire-protocol#reconnect-delta-resume)
2632
+ is excluded for a subscription whose row set is re-resolved per delivery —
2633
+ replaying deltas could serve rows the subject has since lost. Without a
2634
+ declaration the framework cannot tell which tables your predicate may reach, so
2635
+ it excludes them all: one registration disables cheap reconnects for every
2636
+ subscription in the process, including every one reading a table your predicate
2637
+ never returns anything for. With the declaration, only subscriptions on the
2638
+ listed tables are excluded.
2639
+
2640
+ **What it does not buy, and this bounds the whole feature.** A subscription only
2641
+ has a delta chain when its executor returns a **descriptor**. One that returns a
2642
+ mapped value or a page envelope —
2643
+
2644
+ ```ts
2645
+ export default async ({ database }) => {
2646
+ const rows = await database.notifications.where(...)
2647
+ return { notifications: rows.map(toDto), hasMore: rows.length === 20 }
2648
+ }
2649
+ ```
2650
+
2651
+ — re-runs an opaque handler and emits snapshots, with or without a filter. So
2652
+ the count `tables:` gives back is the count of descriptor-returning
2653
+ subscriptions, not the number of queries in your app. Declare it anyway (it
2654
+ costs nothing, and it applies the moment such a query returns the builder), but
2655
+ measure before expecting a change.
2656
+
2657
+ **How to see which of yours are which.** The two shapes are indistinguishable
2658
+ from the outside — a subscription that resumed and one that was never eligible
2659
+ both reconnect with rows on the screen. So the runtime records its own verdict
2660
+ at the moment it decides, per query label:
2661
+
2662
+ ```sh
2663
+ curl -s localhost:4000/_voltro/inspect/subscriptions | jq .resume
2664
+ ```
2665
+
2666
+ ```json
2667
+ [
2668
+ { "label": "documents.list", "resumable": 12, "excluded": {} },
2669
+ { "label": "notifications.list", "resumable": 0, "excluded": { "computed": 8 } },
2670
+ { "label": "comments.list", "resumable": 0, "excluded": { "row-filter": 3 } }
2671
+ ]
2672
+ ```
2673
+
2674
+ A label appears once something has subscribed to it, so click through the app
2675
+ first. `computed` means no declaration can ever help that query; `row-filter`
2676
+ means the filter narrows its source and the exclusion is the point;
2677
+ `eager-load` means dropping the `.with(...)` would flip it. `voltro dev` also
2678
+ logs each verdict once per label under the `voltro:resume` scope.
2679
+
2680
+ **Why a declaration and not a probe.** Resolving the scope at subscribe time
2681
+ and treating "returns `undefined` for this table" as safe is cheaper and
2682
+ unsound: your predicate is a function of freshly loaded context, so a table it
2683
+ does not narrow now may be narrowed on the next delivery — which is the entire
2684
+ reason the filter is re-resolved per delivery. A static list is a promise about
2685
+ every future resolution.
2686
+
2687
+ **It is verified, not trusted.** Returning a predicate for a table outside the
2688
+ list raises `RowFilterDeclarationViolated` at the read that did it — the
2689
+ request fails and a subscription is revoked. A declaration nobody checks is a
2690
+ comment, and this one is load-bearing: the resume grant is issued on its
2691
+ strength. Omit `tables:` entirely and nothing is verified and nothing resumes —
2692
+ the conservative default.
2693
+
2694
+ ## Eager loads are refused, not silently unfiltered
2695
+
2696
+ A relation pulled in with `.with(...)` is resolved **below** the seam that
2697
+ applies the filter: the stores expand the eager tree themselves (the memory
2698
+ store recurses through its own raw read; the SQL stores fold the relation into
2699
+ one join). So a filtered table reached through a relation would come back
2700
+ unfiltered.
2701
+
2702
+ Rather than serve those rows, the read **refuses**, naming the table and both
2703
+ ways out:
2704
+
2705
+ ```
2706
+ row filter: 'readers' is reached through an eager load on 'notes', and an eager
2707
+ relation is resolved BELOW the row filter — its rows would come back
2708
+ unfiltered. Refusing the read rather than serving it.
2709
+ → read 'readers' as its own query (it is filtered there), or
2710
+ → drop 'readers' from this .with(...) if the relation does not need
2711
+ row-level narrowing.
2712
+ ```
2713
+
2714
+ Only relations reaching a table your filter actually narrows are affected —
2715
+ every other eager load is untouched. Applying the filter inside eager
2716
+ compilation is the real fix and it is a per-dialect change; until then this is
2717
+ a refusal rather than a leak, for the same reason the module refuses to
2718
+ fail open.
2719
+
2605
2720
  ## Row filters vs. guards
2606
2721
 
2607
2722
  They answer different questions, and a complete policy usually wants both:
@@ -32,6 +32,7 @@ The dispatcher routes `voltro <command> [args]` to the matching subcommand and p
32
32
  | Integrate | [`webhooks`](/docs/plugins/webhooks#voltro-webhooks-consumer-the-package-your-subscribers-install) (`consumer` / `events`) — generate the ZERO-dependency Standard-Webhooks verification package your subscribers install, from your own declared events (`--out` / `--name`); list the events a subscriber can register for (`--json`) |
33
33
  | [Inspect & debug](/docs/cli/inspect) | `inspect`, `logs`, `traces`, `workflows`, `cluster`, `check` |
34
34
  | [Health & surface](/docs/cli/build-and-start) | [`doctor`](/docs/cli/build-and-start) — serve preflight + the hand-roll detector (names the shipped primitive at the spot you're rebuilding it); [`capabilities`](/docs/cli/build-and-start) (`--json`) — the export surface read from your installed `@voltro/*`, so it can be verified instead of recalled; `info` (`--json`) — CLI / node / package-manager / dialect + every installed `@voltro/*` version, flagging lockstep skew (exits 1 on skew) |
35
+ | Mobile | `mobile` (`codegen` / `links`) — the Expo app's build-time steps: the typed rpc client + deep-link table, and the `apple-app-site-association` / `assetlinks.json` a universal link needs. `voltro codegen` inside a mobile app runs the same generators. |
35
36
  | Harness | `test`, `e2e` |
36
37
  | Cloud | `cloud` (`login` / `whoami` / `projects` / `env` / `import`); `login` is a top-level alias of `cloud login` |
37
38
  | Secrets | `secret` (`generate [purpose]` — the right var+format per secret; `generate` alone → a generic secret; `list`) |
@@ -143,6 +144,8 @@ config value always winning:
143
144
  | `VOLTRO_HSTS` | `Strict-Transport-Security` value. `off` drops just this one. |
144
145
  | `VOLTRO_MAX_RPC_BODY_BYTES` | Cap on the buffered `POST /rpc` JSON body (default 8 MiB) — an oversized body is refused `413` and never buffered past the cap. File uploads ride plugin routes with their own limits. |
145
146
  | `VOLTRO_MAX_BODY_BYTES` | Cap on every OTHER body read — plugin HTTP routes, REST routes, incoming webhooks (default 8 MiB, matching the rpc cap). The config-file spelling is `http.maxBodyBytes` in `app.config.ts`; per-route overrides (`defineRestRoute({ maxBodyBytes })`, a webhook handler's `maxBodyBytes`) win over both. Oversize is `413` for `Content-Length` and chunked alike. |
147
+ | `VOLTRO_CRDT_COMPACT_MAX_BYTES` | Size above which a merged `crdtText()` / `crdtDoc()` blob is soft-compacted (default 512 KiB, `0` disables). The config-file spelling is `crdt.compactMaxBytes` in `app.config.ts`; this variable wins over it. See [local-first](/docs/local-first/overview#rich-text-crdtdoc-usecrdtdoc-usecrdteditor). |
148
+ | `VOLTRO_GRPC_DRAIN_MS` | How long the [gRPC surface](/docs/data/grpc) lets open calls finish on shutdown before force-closing them (default 5000, `0` forces immediately). The config-file spelling is `grpc.drainMs`; this variable wins over it. Keep it below your orchestrator's termination grace. |
146
149
 
147
150
  Response compression for the buffered non-rpc surfaces (and `voltro start`'s
148
151
  HTML) is configured in the same `http:` block — `http.compression.{enabled,minBytes}`
@@ -2031,7 +2034,7 @@ package's README.
2031
2034
 
2032
2035
  _voltro migrate — apply the declared schema through the declarative differ (an alias of voltro db apply)._
2033
2036
 
2034
- `voltro migrate` applies your declared schema (`*.entity.ts` / `*.schema.ts` / `schema.ts`) to the configured database. It is an **alias of [`voltro db apply`](#the-declarative-workflow)**: it diffs the declared schema against the live database and emits the ALTERs, so a changed column or a new index actually lands.
2037
+ `voltro migrate` applies your declared schema (`*.entity.ts` / `*.schema.ts` / `schema.ts`) to the configured database. It is an **alias of [`voltro db apply`](#the-declarative-diff-workflow-voltro-db)**: it diffs the declared schema against the live database and emits the ALTERs, so a changed column or a new index actually lands.
2035
2038
 
2036
2039
  > Before 0.11.4 this command was a create-only apply (`CREATE TABLE IF NOT EXISTS`, no diffing), which meant a column or type change reported success having applied **nothing**. If you need that bootstrap-only behaviour for a brand-new database, it is now `voltro migrate --create-only`.
2037
2040
 
@@ -2966,17 +2969,67 @@ the swap could not run: 2 row(s) in the bundle reference a row the bundle does n
2966
2969
  The target is UNCHANGED — the swap runs in one transaction and none of it committed.
2967
2970
  ```
2968
2971
 
2969
- Staging tables from a run that died mid-load are collected by the next
2970
- `replace` over the same tables. One over a DIFFERENT set leaves them, and
2971
- nothing else removes them:
2972
+ ### Staging tables a dead run left behind
2973
+
2974
+ A staged run RECORDS the scratch tables it creates, in the same
2975
+ `_voltro_replace_in_progress` table an interrupted destructive `replace` writes
2976
+ to — with one difference that matters: **a staging record never refuses a boot.**
2977
+ Nothing was destroyed, so there is nothing to refuse over. The boot reports
2978
+ instead:
2979
+
2980
+ ```
2981
+ staged data-import leftovers:
2982
+ - 3 staging table(s) from a `replace` over api, last active 74 minute(s) ago — DROPPED: the run
2983
+ is not resumable and has been silent long enough that nothing is loading into them.
2984
+ The target of a staged `replace` is untouched until one short swap at the end, so none of this is
2985
+ a reason to refuse the boot — it is a reason to know the disk is holding a copy of a bundle.
2986
+ ```
2987
+
2988
+ The run refreshes a heartbeat on that record every couple of seconds while rows
2989
+ land, which is what lets a boot tell the three cases apart:
2990
+
2991
+ | what the record says | what the boot does |
2992
+ |---|---|
2993
+ | silent past the threshold, started without `--no-atomic` | **drops** the tables it names |
2994
+ | still beating | leaves them — an import is loading into them right now, here or on another replica |
2995
+ | started `--no-atomic` | leaves them — its staging IS the resume point |
2996
+
2997
+ The threshold is `30` minutes by default. It is deliberately generous: the cost
2998
+ of collecting too early is that an in-flight import's swap fails with a missing
2999
+ table and you re-run it — the target is untouched either way — but the cost is
3000
+ still a re-run.
3001
+
3002
+ Declare a different one for a deployment whose imports routinely pause longer
3003
+ than that, waiting on an upstream export or a maintenance window:
3004
+
3005
+ ```ts
3006
+ // app.config.ts
3007
+ export default {
3008
+ dataTransfer: {
3009
+ stagingStaleMinutes: 90,
3010
+ },
3011
+ }
3012
+ ```
3013
+
3014
+ `VOLTRO_STAGING_STALE_MINUTES` overrides the declaration in turn — an operator
3015
+ acting on a running deployment outranks what the project declared. Note that the
3016
+ threshold decides only what a boot DROPS: leftover staging tables are named in
3017
+ the boot log either way.
3018
+
3019
+ What the boot does NOT collect, you can:
2972
3020
 
2973
3021
  ```bash
2974
3022
  voltro data clear-staging --yes
2975
3023
  ```
2976
3024
 
2977
- Deliberately a command and not a boot sweep: a booting process cannot tell a
2978
- leftover from a staging table another replica is loading into right now, and
2979
- deleting the second would destroy an import in flight.
3025
+ It now labels each table with what its own run says, so a resume point is
3026
+ distinguishable from a leftover before you drop it:
3027
+
3028
+ ```
3029
+ 2 staging table(s) from an earlier `--mode replace`:
3030
+ _voltro_staging_tasks — RESUMABLE: a `--no-atomic` re-run continues from it, last active 4 min ago
3031
+ _voltro_staging_notes — no run claims it (an orphan, or from before the marker)
3032
+ ```
2980
3033
 
2981
3034
  A **cycle** in the bundle's foreign keys is detected before the load, not after
2982
3035
  it. The swap inserts parents first, so two tables referencing each other cannot
@@ -3559,7 +3612,14 @@ A native run reports **blobs**, not rows: the vendor tool reports no row count w
3559
3612
 
3560
3613
  ### The provenance stamp — a restore that refuses the wrong DB
3561
3614
 
3562
- A native dump is opaque: it doesn't say which dialect made it, which schema shape it carries, or when. `backup` writes a sidecar `voltro-backup-stamp.json` next to the artifact recording exactly that — `dialect`, the live schema `fingerprint`, the `@voltro/cli` version, and the timestamp.
3615
+ A native dump is opaque: it doesn't say which dialect made it, which schema shape it carries, or when. `backup` writes a sidecar `voltro-backup-stamp.json` next to the artifact recording exactly that — `dialect`, the `@voltro/cli` version, the timestamp, and **two** schema fingerprints.
3616
+
3617
+ Two, because they are different facts and only one of them is a claim about the artifact:
3618
+
3619
+ - **`schemaFingerprint`** — the source database's whole live schema at backup time. This is what the skew warning below compares against a target.
3620
+ - **`dumpFingerprint`** — the schema the **artifact carries**: that same snapshot minus the tables the dump excludes. On postgres and the mysql family those are `_voltro_replace_in_progress` and `_voltro_data_transfers` (see above); on sqlite, turso and mssql nothing is excluded and the two values are equal.
3621
+
3622
+ The distinction is not bookkeeping. `voltro data backup` opens its own run row in `_voltro_data_transfers` *before* it dumps, so on any database the framework has run against, the artifact is two tables short of the live schema it was taken from. Anything comparing a restored schema against a stamped one has to compare against `dumpFingerprint` — the drill did not, and failed every healthy backup with *"the artifact is inconsistent."*
3563
3623
 
3564
3624
  `restore` reads the stamp **before touching the DB** and acts on two failures that are otherwise silent until they corrupt:
3565
3625
 
@@ -3575,13 +3635,39 @@ voltro data restore ./backups/2026-07-01 --drill --drill-url postgres://…/scra
3575
3635
  # or set DRILL_DB_URL and just: voltro data restore ./backups/2026-07-01 --drill
3576
3636
  ```
3577
3637
 
3578
- `--drill` restores the artifact into a **throwaway** database (from `--drill-url` / `DRILL_DB_URL`) and verifies it — **without ever touching the live DB**. It refuses a drill target that resolves to your live connection (a drill that `--clean`s production is the disaster it exists to rehearse against). After the restore it introspects the throwaway DB and compares its schema fingerprint to the backup's stamp:
3638
+ `--drill` restores the artifact into a **throwaway** database (from `--drill-url` / `DRILL_DB_URL`) and verifies it — **without ever touching the live DB**. It refuses a drill target that resolves to your live connection (a drill that `--clean`s production is the disaster it exists to rehearse against). After the restore it introspects the throwaway DB and probes its migration ledger:
3579
3639
 
3580
3640
  - **zero tables restored** → FAIL (the dump is empty or unreadable — this backup would not recover you),
3581
- - **fingerprint disagrees with the stamp** → FAIL (the restore didn't reproduce what was backed up),
3582
- - **tables + matching fingerprint** → PASS.
3641
+ - **schema fingerprint disagrees with the stamp's `dumpFingerprint`** → FAIL (the restore didn't reproduce what was backed up),
3642
+ - **`_voltro_migration_plans` restored EMPTY** → FAIL (see below),
3643
+ - **tables + matching fingerprint + a populated or absent ledger** → PASS.
3644
+
3645
+ A stamp too old to carry a `dumpFingerprint` gives a **PASS (partial)** that says the shape could not be cross-checked. It does not fall back to `schemaFingerprint`: that is the comparison that fails a healthy backup, and a check that is red on every real input gets switched off — taking its genuine failures with it.
3646
+
3647
+ It exits non-zero on any FAIL, so a scheduled CI job turns a silently-broken backup into a red build. Run it against your latest artifact on a cron — a backup you've never restored is a hypothesis, and this is how you keep it a fact.
3648
+
3649
+ #### The ledger check — the one thing a schema comparison cannot see
3650
+
3651
+ A fingerprint answers *"is the shape right?"*. A drill's real question is *"would my app come up against this?"*, and the gap between them is **content** — a framework table that restored with the right columns and the wrong rows.
3652
+
3653
+ `voltro serve`'s boot gate reads the newest row of `_voltro_migration_plans` and refuses with `prod-mismatch` when there is none. So a ledger table that restores with exactly the right columns and **zero rows** is a database no source tree can boot, and its schema fingerprint is identical to a healthy one's. The drill fails that, and names it:
3654
+
3655
+ ```
3656
+ FAIL — restored 30 table(s) with the right shape, but `_voltro_migration_plans`
3657
+ came back EMPTY.
3658
+ `voltro serve` reads the newest row of that table as its boot gate and
3659
+ refuses with `prod-mismatch` when there is none.
3660
+ ```
3661
+
3662
+ A restored database with **no ledger table at all** is not a voltro-managed schema (a hand-made dump, someone else's database) — the drill says so and claims nothing about booting it, rather than failing it.
3663
+
3664
+ What the drill deliberately does **not** judge is a ledger whose fingerprint differs from what your code declares. It has no way to know which commit you will deploy next to this database, and `voltro db apply` clears that state anyway; failing a backup for it would make the drill red for a reason that is not about the backup.
3665
+
3666
+ #### Why there is no full app boot
3667
+
3668
+ Booting a real app against the restored database sounds like the stronger check, and it would be a weaker one. There is no app in the drill's path — it would have to boot a **fixture**, and a fixture booting says nothing about whether *your* app boots. It moves the drill from *"proves your backup"* to *"proves our fixture"* while reading as the bigger claim.
3583
3669
 
3584
- It exits non-zero on any FAIL, so a scheduled CI job turns a silently-broken backup into a red build. Run it against your latest artifact on a cron a backup you've never restored is a hypothesis, and this is how you keep it a fact. (The verify is schema-level; a full app boot against the restored DB is a heavier check you can layer on top.)
3670
+ The part worth having does not need a process: the boot gate is a comparison, not a startup sequence, so the one boot-fatal condition that holds regardless of which code you deploy is reachable with a `SELECT`. That is the ledger check above.
3585
3671
 
3586
3672
  ### Point-in-time recovery (PITR) is your database's job, not the framework's
3587
3673