@voltro/cli 0.22.1 → 0.24.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 (76) hide show
  1. package/CHANGELOG.md +427 -0
  2. package/bin/voltro.mjs +39 -4
  3. package/dist/{apiBuild-DiWxVz-M.js → apiBuild-B2m4XK_8.js} +45 -23
  4. package/dist/apiBuild-UEM3QBke.js +2 -0
  5. package/dist/bin.js +3 -3
  6. package/dist/{commands-BxRaIOBG.js → commands-CaIhTsC2.js} +2944 -2557
  7. package/dist/{dbCommand-DVASmZj2.js → dbCommand-CC61CsAc.js} +474 -263
  8. package/dist/dbCommand-Cr__4ATv.js +2 -0
  9. package/dist/{dev-Dd3EZzj5.js → dev-DPkQVUTP.js} +1 -1
  10. package/dist/{dev-BRiPgbKw.js → dev-NCuEhNxs.js} +1593 -1575
  11. package/dist/index.js +1 -1
  12. package/dist/{inspectMetrics-9ZSuDeqD.js → inspectMetrics-dcC7gnxu.js} +566 -552
  13. package/dist/{serveCommand-DXJOARkO.js → serveCommand-C4gzDZzc.js} +425 -411
  14. package/dist/serveEntry.js +2 -2
  15. package/dist/{start-CsIjcOi-.js → start-C_4VUDT8.js} +327 -325
  16. package/dist/startEntry.js +2 -2
  17. package/package.json +17 -17
  18. package/templates/AGENTS.md +1 -1
  19. package/templates/agent-docs/_index.md +1 -1
  20. package/templates/agent-docs/_manifest.json +1 -1
  21. package/templates/agent-docs/authentication.md +21 -0
  22. package/templates/agent-docs/caching.md +21 -5
  23. package/templates/agent-docs/cli.md +61 -0
  24. package/templates/agent-docs/data.md +208 -170
  25. package/templates/agent-docs/database/migrations.md +123 -0
  26. package/templates/agent-docs/database/seedsdialects.md +12 -0
  27. package/templates/agent-docs/deployment.md +66 -1
  28. package/templates/agent-docs/plugins.md +42 -0
  29. package/templates/agent-docs/routing.md +23 -1
  30. package/templates/agent-docs/templates/apibackends.md +2 -2
  31. package/templates/agent-docs/whats-new.md +281 -32
  32. package/templates/agent-docs/workflows.md +30 -0
  33. package/templates/apps/api-ai/package.json +8 -7
  34. package/templates/apps/api-auth/package.json +9 -8
  35. package/templates/apps/api-backend/package.json +8 -7
  36. package/templates/apps/api-backend-deactivation/package.json +8 -7
  37. package/templates/apps/api-backend-mail/package.json +9 -8
  38. package/templates/apps/api-backend-mariadb/package.json +10 -9
  39. package/templates/apps/api-backend-storage/package.json +9 -8
  40. package/templates/apps/api-data-advanced/package.json +9 -8
  41. package/templates/apps/api-durable/package.json +9 -8
  42. package/templates/apps/api-feature-flags/package.json +10 -9
  43. package/templates/apps/api-governance/package.json +9 -8
  44. package/templates/apps/api-kv/package.json +9 -8
  45. package/templates/apps/api-moderation/package.json +9 -8
  46. package/templates/apps/api-observability/package.json +9 -8
  47. package/templates/apps/api-ratelimit/package.json +9 -8
  48. package/templates/apps/api-rbac/package.json +9 -8
  49. package/templates/apps/api-rest/package.json +8 -7
  50. package/templates/apps/api-saas/package.json +12 -11
  51. package/templates/apps/api-search/package.json +9 -8
  52. package/templates/apps/api-versioning/app.config.ts +6 -4
  53. package/templates/apps/api-versioning/package.json +9 -8
  54. package/templates/apps/api-webhooks/package.json +10 -9
  55. package/templates/apps/changelog/package.json +7 -6
  56. package/templates/apps/edge-functions/package.json +3 -2
  57. package/templates/apps/frontend-admin/package.json +9 -8
  58. package/templates/apps/frontend-app/package.json +9 -8
  59. package/templates/apps/frontend-blank/package.json +8 -7
  60. package/templates/apps/frontend-contact/package.json +8 -7
  61. package/templates/apps/frontend-contact/src/globals.d.ts +6 -0
  62. package/templates/apps/frontend-dashboard/package.json +8 -7
  63. package/templates/apps/frontend-docs/package.json +8 -7
  64. package/templates/apps/frontend-docs/src/globals.d.ts +6 -0
  65. package/templates/apps/frontend-i18n/package.json +7 -6
  66. package/templates/apps/frontend-landing/package.json +8 -7
  67. package/templates/apps/frontend-landing/src/globals.d.ts +6 -0
  68. package/templates/apps/frontend-spa/package.json +8 -7
  69. package/templates/apps/frontend-spa/src/globals.d.ts +6 -0
  70. package/templates/apps/frontend-ssr/package.json +8 -7
  71. package/templates/apps/frontend-ssr/src/globals.d.ts +6 -0
  72. package/templates/apps/frontend-ssr-api/package.json +9 -8
  73. package/templates/apps/frontend-static-blog/package.json +7 -6
  74. package/templates/apps/frontend-static-blog/src/globals.d.ts +6 -0
  75. package/dist/apiBuild-COyPDf3R.js +0 -2
  76. package/dist/dbCommand-CO3eSAZR.js +0 -2
@@ -918,6 +918,44 @@ under `NODE_ENV=production` (it refuses unless both fingerprints still match the
918
918
  reviewed plan). A Job runs **once per release** vs an `initContainer`'s once per
919
919
  replica, so it's the better fit for a multi-replica rollout.
920
920
 
921
+ ### Expand/contract — the migration that's safe while old pods still serve
922
+
923
+ The pre-deploy Job applies the schema **before the new pods roll** — so during a
924
+ rolling update, old pods (old code) run against the already-migrated schema for
925
+ the length of the rollout. A migration that DROPS or RENAMES a column, NARROWS a
926
+ type, or ADDS a constraint breaks those old pods mid-rollout: they 500 reading a
927
+ column that's gone, or their writes are rejected by the new constraint. The
928
+ migration "succeeded" and the app served errors anyway.
929
+
930
+ `voltro db plan` flags these — the operations unsafe under a rolling deploy are
931
+ listed with a `⚠`, separately from the data-safety (lossy / blocked) gate, since
932
+ the two are orthogonal: a `dropped()` column is blessed for data loss and *still*
933
+ breaks an old reader.
934
+
935
+ ```text
936
+ ⚠ 1 operation(s) UNSAFE under a rolling deploy
937
+ (old + new instances overlap → old code breaks against the new schema):
938
+ • drop-column: old instances still SELECT/INSERT "orders"."legacy_total"; …
939
+ → stop reading the column in code and deploy that first; drop it in a LATER deploy
940
+ ```
941
+
942
+ Two ways to handle it:
943
+
944
+ 1. **No overlap window** — a maintenance-window or **scale-to-zero** deploy (old
945
+ pods gone before new ones start) has no simultaneous old code, so a single-step
946
+ drop/rename is fine. The advisory doesn't apply; ignore it.
947
+ 2. **Zero-downtime rollout** — split the breaking change into two releases, each
948
+ of which keeps *both* code versions working (**expand/contract**):
949
+ - **Expand** (release N): add the new shape — a nullable column, a new table,
950
+ a backfill, dual-write from the new code. Old code ignores it.
951
+ - **Cut over**: the new code reads/writes the new shape; deploy it.
952
+ - **Contract** (release N+1): once no pod runs the old code, drop/rename/narrow
953
+ the now-unused old shape. This step's `db plan` is clean.
954
+
955
+ Renaming `orders.total` → `orders.amount` under zero downtime is: add `amount`
956
+ (expand) → backfill + dual-write → cut reads over → drop `total` (contract) — three
957
+ releases, never one, so no in-flight pod ever references a column that isn't there.
958
+
921
959
  ### If you DO run `voltro dev` in a cluster (dev / staging only)
922
960
 
923
961
  `voltro dev` binds a small **boot-health surface** on its own port so a probe can
@@ -1059,6 +1097,33 @@ that route to a terminating pod before k8s finishes removing it from the Service
1059
1097
  endpoints. With it, that window is drained. `terminationGracePeriodSeconds` must
1060
1098
  be larger than the sleep plus the app's own teardown, or k8s SIGKILLs mid-drain.
1061
1099
 
1100
+ **Bound the app's own teardown with `VOLTRO_SHUTDOWN_GRACE_MS`.** After
1101
+ `SIGTERM`, the runtime runs its finalizers (pool close, plugin `onDeactivate`,
1102
+ analytics flush, trace persist) and then exits — but installing the signal
1103
+ handler removes node's default kill, so a finalizer that *never* completes (a
1104
+ pool drain against a database that is already gone, a wedged `onDeactivate`)
1105
+ would otherwise hang the process forever. A hard deadline caps that: teardown
1106
+ gets until the deadline, then the process exits regardless. It defaults to
1107
+ **10s**; set `VOLTRO_SHUTDOWN_GRACE_MS` (clamped to 1s–5min) to sit JUST UNDER
1108
+ your `terminationGracePeriodSeconds` minus the preStop sleep — so the app drains
1109
+ and exits *cleanly on its own* before k8s SIGKILLs it mid-drain:
1110
+
1111
+ ```yaml
1112
+ spec:
1113
+ terminationGracePeriodSeconds: 30
1114
+ containers:
1115
+ - name: api
1116
+ env:
1117
+ # preStop sleep (5s) + app teardown (≤22s) < 30s grace, with headroom.
1118
+ - name: VOLTRO_SHUTDOWN_GRACE_MS
1119
+ value: "22000"
1120
+ ```
1121
+
1122
+ The close is clean on the client side too: on shutdown each live WebSocket is
1123
+ closed with a proper close frame (not an abrupt socket drop), and the web
1124
+ client's supervisor reconnects on any close — so an open dashboard re-attaches
1125
+ to a healthy replica across a rolling deploy without a page reload.
1126
+
1062
1127
  ## 8. Multiple replicas
1063
1128
 
1064
1129
  Cache and KV default to **in-process** (per-replica). For a shared backend across replicas:
@@ -1087,7 +1152,7 @@ Schedules and aggregates auto-coordinate via an advisory lock on SQL stores —
1087
1152
  - [ ] `auth.anonymousTenantRequired: true` (unless the app serves anonymous public data)
1088
1153
  - [ ] `OTEL_EXPORTER_OTLP_ENDPOINT` + `OTEL_SERVICE_NAME` pointed at your collector
1089
1154
  - [ ] `VOLTRO_LOG_FORMAT=json`; `sentryPlugin()` + `SENTRY_DSN` for errors
1090
- - [ ] `terminationGracePeriodSeconds` generous for graceful drain
1155
+ - [ ] `terminationGracePeriodSeconds` generous for graceful drain; `VOLTRO_SHUTDOWN_GRACE_MS` set just under it (minus the preStop sleep)
1091
1156
  - [ ] `CACHE_BACKEND` / `KV_BACKEND` + a cross-replica change bus when running >1 replica
1092
1157
 
1093
1158
 
@@ -132,6 +132,48 @@ Order matters: the framework composes outer→inner, so the rate-limit intercept
132
132
  | `onInstall` / `onActivate` / `onDeactivate` / `onUninstall` | Lifecycle hooks at first-install, boot, shutdown, and removal. |
133
133
  | Schema mixins (`defineMixin`) | The OTHER plugin shape — `audit()`, `tenant()`, `softDelete()` — declared in `@voltro/database`, not via the runtime contract. |
134
134
 
135
+ ## Pointing YOUR table at a plugin's row
136
+
137
+ Every table-carrying plugin exports its table handles, so a column in your schema
138
+ can reference one exactly like it references your own:
139
+
140
+ ```ts
141
+ import { aiFlowsTable } from '@voltro/plugin-ai-flows'
142
+ import { id, reference, table, text } from '@voltro/database'
143
+
144
+ export const flowFavourites = table('flow_favourites', {
145
+ id: id({ prefix: 'fav' }),
146
+ employeeId: reference(() => employees, { onDelete: 'cascade' }),
147
+ // A real foreign key across the plugin boundary. Deleting the flow removes
148
+ // the favourite; the DATABASE enforces it, so no cleanup subscriber exists to
149
+ // forget.
150
+ flowId: reference(() => aiFlowsTable, { onDelete: 'cascade' }),
151
+ note: text().nullable(),
152
+ })
153
+ ```
154
+
155
+ This is not a special primitive — it is `reference()`, with the same
156
+ `onDelete` semantics and the same index defaults. `plugin-storage`'s `assetRef()`
157
+ has always been exactly this under the hood: a `reference(() => _voltroStorageRefsTable,
158
+ { onDelete: 'setNull' })`.
159
+
160
+ **Referencing the table as a VALUE rather than its name as a string is what makes
161
+ this safe across a plugin's own migrations.** When ten plugin tables moved into
162
+ the `_voltro_` namespace in 0.22.0, a `reference(() => table)` followed the rename
163
+ (catalog-only, the constraint travels with the table); a hand-written
164
+ `text()` column holding ids would not have told you anything had changed.
165
+
166
+ **`fk: false`-style decoupling is still available** — declare a plain `text()`
167
+ column instead. Choose it when you deliberately want the app schema independent
168
+ of the plugin's, and accept that nothing then enforces the link. What you should
169
+ NOT do is reach for it by default: an unenforced id column plus a hand-written
170
+ `defineSubscriber` that cleans up on delete is referential integrity re-implemented
171
+ per app, and it is silently wrong the first time somebody forgets it.
172
+
173
+ **`orphanPolicy` is not part of this.** It is migration metadata — how existing
174
+ orphan rows are cleaned up *before* the FK constraint is added — and has no
175
+ runtime semantics. Runtime behaviour comes from `onDelete`.
176
+
135
177
  ## When NOT to write a plugin
136
178
 
137
179
  - **One-off side effect** — just call it from the mutation directly.
@@ -961,6 +961,7 @@ React alone.
961
961
  export const loader = async (ctx: {
962
962
  readonly params: Readonly<Record<string, string>> // URL params from [name] segments
963
963
  readonly pathname: string // matched path (no query string)
964
+ readonly search: string // raw query string incl. `?`, or '' — filled on every path
964
965
  readonly signal: AbortSignal // Aborts if the client disconnects mid-render
965
966
  readonly headers?: Readonly<Record<string, string>> // Request headers (SSR/ISR only — empty for SSG/client)
966
967
  // Call the backend rpc directly — present ONLY when the loader runs
@@ -970,7 +971,28 @@ export const loader = async (ctx: {
970
971
  }) => Promise<unknown>
971
972
  ```
972
973
 
973
- The loader context carries `pathname`, not a `request` object. For the query string during SSR, read it from `useServerRequest().url` inside the component.
974
+ The loader context carries `pathname` and `search`, not a `request` object.
975
+
976
+ `pathname` is deliberately query-free — a loader keyed on `?tab=2` would cache badly. `search` carries the raw query string (with its leading `?`, or `''`), filled identically on client navigation, `voltro dev` SSR and `voltro start` SSR. Parse it with `new URLSearchParams(ctx.search)`.
977
+
978
+ **Reach for `search` when the loader makes a decision, not when it fetches data.** The case it exists for is a redirect target that depends on a parameter:
979
+
980
+ ```ts
981
+ import { RedirectError } from '@voltro/web'
982
+
983
+ export const loader = async (ctx) => {
984
+ const player = await ctx.query('players.byCode', { code: ctx.params['playerCode'] })
985
+ if (!player) {
986
+ // Preserve kiosk mode across the redirect — otherwise a kiosk terminal
987
+ // drops back to normal mode after every failed scan.
988
+ const mode = new URLSearchParams(ctx.search).get('mode')
989
+ throw new RedirectError(`/?error=${ctx.params['playerCode']}${mode ? `&mode=${mode}` : ''}`)
990
+ }
991
+ return { player }
992
+ }
993
+ ```
994
+
995
+ Do not reconstruct this from `window.location.search`: that exists only on the client-navigation path, so a fresh SSR request loses the value — which is the bug the field was added to remove.
974
996
 
975
997
  Use `signal` for any fetch that could outlive the request — pass it to `fetch(url, { signal })` so cancelled requests don't waste CPU.
976
998
 
@@ -2850,7 +2850,7 @@ voltro create-project acme --api=api-versioning
2850
2850
  // app.config.ts
2851
2851
  import { versioningPlugin } from '@voltro/plugin-versioning'
2852
2852
  export default { type: 'api', name: 'AcmeVer', store: 'memory',
2853
- plugins: [versioningPlugin({ tables: ['documents'] })] }
2853
+ plugins: [versioningPlugin({})] }
2854
2854
  ```
2855
2855
 
2856
2856
  The snapshotting is automatic. The point is to **read history through handlers** — the template ships two actions:
@@ -2898,7 +2898,7 @@ The memory history store is single-process. With a SQL store, history persists i
2898
2898
 
2899
2899
  ## Anti-patterns
2900
2900
 
2901
- - **Listing `versioningPlugin({ tables })` and stopping there.** That records history but never shows it. The value is in the READS — wire `rowHistory` / `rowAsOf` into a handler (or the dashboard) like this template does.
2901
+ - **Listing `versioningPlugin()` and stopping there.** That records history but never shows it. The value is in the READS — wire `rowHistory` / `rowAsOf` into a handler (or the dashboard) like this template does.
2902
2902
  - **Versioning every table.** Snapshots cost storage proportional to write volume. Pick the tables whose past values you actually need.
2903
2903
 
2904
2904