@voltro/cli 0.20.2 → 0.22.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 (75) hide show
  1. package/CHANGELOG.md +398 -0
  2. package/THIRD-PARTY-NOTICES.md +61 -61
  3. package/dist/{apiBuild-qZBEu59d.js → apiBuild-2zRxj2hJ.js} +93 -80
  4. package/dist/apiBuild-sqNmWmY5.js +2 -0
  5. package/dist/bin.js +41 -20
  6. package/dist/{commands-kYbfVoPO.js → commands-DGaeTn2f.js} +3055 -2640
  7. package/dist/dbCommand-CO3eSAZR.js +2 -0
  8. package/dist/{dbCommand-BPp9R0Wc.js → dbCommand-DVASmZj2.js} +305 -249
  9. package/dist/dev-BEYBSGJ7.js +3 -0
  10. package/dist/{dev-OHLsAq4K.js → dev-DeHVUxQx.js} +1732 -1686
  11. package/dist/index.js +1 -1
  12. package/dist/inspect-BA67TF6v.js +2 -0
  13. package/dist/inspect-_ldwsAwH.js +945 -0
  14. package/dist/{inspectMetrics-DvPNXmGA.js → inspectMetrics-4AVPn6IS.js} +584 -571
  15. package/dist/{manifestBuild-P9yuCY2d.js → manifestBuild-Bs1Uw22_.js} +1 -1
  16. package/dist/manifestBuild-i-fRHg_H.js +2 -0
  17. package/dist/serveCommand-DEVTtnLy.js +1161 -0
  18. package/dist/serveEntry.js +2 -2
  19. package/dist/{start-CXQ7WL1W.js → start-BjGUIIvj.js} +389 -389
  20. package/dist/startEntry.js +2 -2
  21. package/package.json +27 -27
  22. package/templates/AGENTS.md +1 -1
  23. package/templates/agent-docs/_index.md +1 -1
  24. package/templates/agent-docs/cli.md +181 -5
  25. package/templates/agent-docs/data.md +58 -0
  26. package/templates/agent-docs/database/migrations.md +175 -31
  27. package/templates/agent-docs/database/schema.md +7 -0
  28. package/templates/agent-docs/database/seedsdialects.md +3 -3
  29. package/templates/agent-docs/plugins.md +1 -1
  30. package/templates/agent-docs/schema-driven-ui.md +2 -2
  31. package/templates/agent-docs/templates/apibackends.md +1 -1
  32. package/templates/agent-docs/whats-new.md +236 -9
  33. package/templates/apps/api-ai/package.json +12 -12
  34. package/templates/apps/api-auth/package.json +13 -13
  35. package/templates/apps/api-backend/package.json +12 -12
  36. package/templates/apps/api-backend-deactivation/package.json +12 -12
  37. package/templates/apps/api-backend-mail/package.json +11 -11
  38. package/templates/apps/api-backend-mariadb/package.json +14 -14
  39. package/templates/apps/api-backend-storage/package.json +11 -11
  40. package/templates/apps/api-data-advanced/package.json +13 -13
  41. package/templates/apps/api-durable/package.json +13 -13
  42. package/templates/apps/api-feature-flags/package.json +14 -14
  43. package/templates/apps/api-governance/package.json +13 -13
  44. package/templates/apps/api-kv/package.json +13 -13
  45. package/templates/apps/api-moderation/package.json +13 -13
  46. package/templates/apps/api-observability/package.json +13 -13
  47. package/templates/apps/api-ratelimit/package.json +13 -13
  48. package/templates/apps/api-rbac/package.json +13 -13
  49. package/templates/apps/api-rest/package.json +12 -12
  50. package/templates/apps/api-saas/package.json +16 -16
  51. package/templates/apps/api-search/package.json +13 -13
  52. package/templates/apps/api-versioning/package.json +13 -13
  53. package/templates/apps/api-webhooks/package.json +14 -14
  54. package/templates/apps/changelog/package.json +8 -8
  55. package/templates/apps/edge-functions/package.json +5 -5
  56. package/templates/apps/frontend-admin/package.json +10 -10
  57. package/templates/apps/frontend-app/package.json +10 -10
  58. package/templates/apps/frontend-blank/package.json +9 -9
  59. package/templates/apps/frontend-contact/package.json +11 -11
  60. package/templates/apps/frontend-dashboard/package.json +9 -9
  61. package/templates/apps/frontend-docs/package.json +9 -9
  62. package/templates/apps/frontend-i18n/package.json +8 -8
  63. package/templates/apps/frontend-landing/package.json +9 -9
  64. package/templates/apps/frontend-spa/package.json +9 -9
  65. package/templates/apps/frontend-ssr/package.json +9 -9
  66. package/templates/apps/frontend-ssr-api/package.json +10 -10
  67. package/templates/apps/frontend-static-blog/package.json +8 -8
  68. package/dist/apiBuild-C9aHz4Yh.js +0 -2
  69. package/dist/dbCommand-DtUe-0dM.js +0 -2
  70. package/dist/dev-2AvdzDg2.js +0 -3
  71. package/dist/inspect-DcZ04OME.js +0 -2
  72. package/dist/inspect-Dwx0_tUj.js +0 -921
  73. package/dist/manifestBuild-D1MzJAiQ.js +0 -2
  74. package/dist/serveCommand-BKqTKWTX.js +0 -1136
  75. /package/templates/patches/{@effect__cluster@0.59.0.patch → @effect__cluster@0.60.0.patch} +0 -0
@@ -89,7 +89,8 @@ voltro db plan --against <url> # diff vs a remote env's inspect endpoint (see
89
89
  voltro db apply # execute (dev only — refuses on NODE_ENV=production)
90
90
  voltro db apply --note '...' # apply with a freeform note recorded in history
91
91
  voltro db plans [--limit 20] # history from _voltro_migration_plans, newest first
92
- voltro db drift # live-vs-applied fingerprint check — exit 4 on drift
92
+ voltro db drift # live-vs-baseline check — exit 0 match, 3 no baseline, 4 drift
93
+ voltro db drift --accept # record the CURRENT live schema as the baseline (refuses unless db plan is empty)
93
94
  voltro db squash --before <date> # consolidate history into one snapshot
94
95
  voltro db restore-snapshot <id> # restore VOLTRO_SOFT_DROP=1 columns from a plan
95
96
 
@@ -119,6 +120,21 @@ there is no `--json` or `--sql`, and `voltro db apply` takes only
119
120
  deploy step ([prod pipeline](./prod-pipeline.md)), not a pre-serialised
120
121
  plan file.
121
122
 
123
+ ## Framework tables ride the same differ
124
+
125
+ The `_voltro_*` tables the framework owns are planned, classified and applied by
126
+ exactly the same code as yours — on every dialect. A framework release that adds
127
+ a table, adds a column or reshapes one lands on the boot that follows your
128
+ upgrade, wherever your own schema changes land. There is no separate command and
129
+ no dialect-specific step.
130
+
131
+ One asymmetry is deliberate and worth knowing if you ever read a plan: a
132
+ framework-owned table that **nobody declares** — `cluster_*` from the workflow
133
+ engine, a plugin's table after you removed the plugin — is never planned for a
134
+ drop. "Nobody declared it, so do not drop it" and "we declare it, so keep it
135
+ current" are different rules; collapsing them is what once made framework tables
136
+ evolve on postgres and nowhere else.
137
+
122
138
  ## Where to go next
123
139
 
124
140
  | Topic | Page |
@@ -367,6 +383,92 @@ If the live DB doesn't have a column called `firstName`, the marker is a no-op:
367
383
 
368
384
  The marker isn't validated against the live DB at schema-build time — it would have to introspect during type-checking, which is expensive. The runtime check fires at plan time.
369
385
 
386
+ ## The indexes come with it
387
+
388
+ Renaming a column is a metadata-only operation. Its **indexes** used to not be: index names are derived (`<table>_<column>_idx`), and no dialect renames an index when the column under it is renamed — so the planner saw `users_firstName_idx` on one side and `users_givenName_idx` on the other, and planned `DROP INDEX` + `CREATE INDEX`. On a large table that is a full B-tree rebuild: minutes of IO, and without `CONCURRENTLY` a write lock, behind a rename that was supposed to be instant.
389
+
390
+ The planner now folds that into a `rename-index` operation, which is a catalog-only statement everywhere it is emitted:
391
+
392
+ ```
393
+ ✓ rename-column users.firstName → users.givenName # catalog-only
394
+ ✓ rename-index users_firstName_idx → users_givenName_idx # catalog-only, no rebuild
395
+ ```
396
+
397
+ You do not annotate anything for this — it follows from the column rename you already declared.
398
+
399
+ Four cases deliberately still plan as drop + create, because pairing an old index with a new one has no evidence to stand on in them:
400
+
401
+ - **sqlite** — it has no rename statement at all. The plan you read matches what runs.
402
+ - **UNIQUE indexes** — they are constraint objects, and the syntax to rename one diverges by dialect.
403
+ - **Expression / json-path indexes** — the database normalises their key text, so there is no shape to compare; only the name, which is the thing that changed.
404
+ - **Two same-shaped indexes renamed at once** — nothing says which became which. Rebuilding both is slower; renaming the wrong one is worse.
405
+
406
+ ## Renaming a TABLE
407
+
408
+ The same problem one level up, and with more at stake: a table rename and a
409
+ drop+create look identical to the differ — old table gone, new table present —
410
+ except that guessing wrong costs every row. So it needs a marker too, and it
411
+ reads like its column counterpart:
412
+
413
+ ```ts
414
+ // Before:
415
+ export const notes = table('notes', { id: id({ prefix: 'note' }), body: text() })
416
+
417
+ // After — without the marker:
418
+ export const notes = table('archive_notes', { id: id({ prefix: 'note' }), body: text() })
419
+ // → planner sees DROP TABLE notes + CREATE TABLE archive_notes
420
+ // → the DROP is `lossy` and blocked; nothing happens until you acknowledge it
421
+
422
+ // With the marker:
423
+ export const notes = table('archive_notes', { id: id({ prefix: 'note' }), body: text() })
424
+ .renamedFrom('notes')
425
+ // → one `rename-table` op, classified `safe`
426
+ // → `ALTER TABLE notes RENAME TO archive_notes` — catalog-only, the rows stay put
427
+ ```
428
+
429
+ Unlike an index rename, every dialect has this statement — sqlite included — so
430
+ there is no dialect on which this falls back to a rebuild.
431
+
432
+ **Its indexes come with it.** The same derivation that bites a column rename bites
433
+ harder here: `notes_pkey` and `notes_<col>_idx` are named after the table, and no
434
+ dialect renames them when the table is renamed. The planner emits a `rename-index`
435
+ for each so the catalog catches up:
436
+
437
+ ```
438
+ ✓ rename-table notes → archive_notes
439
+ ✓ rename-index notes_pkey → archive_notes_pkey
440
+ ```
441
+
442
+ Without that the plan would try to drop the primary-key index and re-add it as a
443
+ plain UNIQUE, which postgres refuses outright.
444
+
445
+ **Three cases where the planner will NOT fold the rename**, each because folding
446
+ it could destroy data rather than move it — and none of them is silent:
447
+
448
+ - **The old name is still declared by something.** If your schema still has a
449
+ `notes` table, it is yours and stays put; the new table is created empty. This
450
+ is a legitimate outcome (it is what lets a framework plugin reclaim a name
451
+ without taking yours), so the plan runs — and the `create-table` line says why
452
+ the marker was not applied.
453
+ - **The new name already exists in the database.** → **refuses to plan.** Both
454
+ tables exist and only you know which holds the real rows. The fix tells you to
455
+ move them and drop one, or drop the empty one so the rename can run. Until
456
+ then the old table is untouched.
457
+ - **Two tables both claim the same old name.** → **refuses to plan.** Nothing
458
+ says which should receive the rows; remove the marker from all but one.
459
+
460
+ The last two refuse rather than degrade, because the quiet outcome — an empty
461
+ plan reading "schema up to date" while the old table still holds every row — is
462
+ the one that loses data by inaction.
463
+
464
+ **Lifecycle.** Same as `.renamedFrom()` on a column: a marker whose old table is
465
+ not in the database is a silent no-op, so it stays in your source across a staged
466
+ rollout and comes out once every environment has applied it.
467
+
468
+ **One constraint worth knowing:** a table whose name starts with `_` cannot derive
469
+ a typeid prefix, so it needs an explicit `id({ prefix: '…' })`. You will hear about
470
+ it at declaration, not at runtime.
471
+
370
472
  ## Lifecycle — when to remove the marker
371
473
 
372
474
  Keep the marker until the rename has been applied in EVERY env you care about (dev, staging, prod). The framework tracks applied ops in `_voltro_migration_plans`:
@@ -2009,7 +2111,7 @@ Drift = the live database's schema doesn't match the fingerprint of the last app
2009
2111
 
2010
2112
  Three trigger paths:
2011
2113
 
2012
- 1. **Manual** — `voltro db drift` runs the check on demand, exits 4 on drift
2114
+ 1. **Manual** — `voltro db drift` runs the check on demand: exit 0 match, 3 no baseline, 4 drift
2013
2115
  2. **On boot (dev)** — every `voltro dev` boot runs the planner, which detects drift implicitly (the plan will be non-empty)
2014
2116
  3. **Periodic (cloud)** — the cloud dashboard polls each app's `/_voltro/inspect/migrations` endpoint; drift state is in the response
2015
2117
 
@@ -2018,16 +2120,39 @@ All three paths produce the same `DriftSnapshot` shape:
2018
2120
  ```ts
2019
2121
  {
2020
2122
  isDrifted: boolean,
2021
- liveFingerprint: string, // current introspected user-schema fingerprint
2022
- lastAppliedFingerprint?: string, // newest _voltro_migration_plans row's fp
2123
+ liveFingerprint: string, // current introspected schema fingerprint
2124
+ lastAppliedFingerprint?: string, // the BASELINE the newest row recorded
2023
2125
  lastAppliedAt?: string, // when it was applied
2024
2126
  lastAppliedId?: string, // plan id
2025
2127
  }
2026
2128
  ```
2027
2129
 
2028
- `isDrifted: false` ↔ `liveFingerprint === lastAppliedFingerprint`.
2130
+ `isDrifted: false` ↔ `liveFingerprint === lastAppliedFingerprint`, **or** no baseline was recorded — nothing compared is not the same as nothing changed, and it is never reported as drift.
2131
+
2132
+ ### What it compares, and what it does not
2133
+
2134
+ The baseline is `_voltro_migration_plans.liveFingerprint`: the fingerprint of the **live** schema as it was immediately after the last `voltro db apply`. Not `fingerprint`, which is the **declared** snapshot's hash — introspection cannot recover everything a declaration carries (generated expressions, `maxLength`, sensitivity markers), so a live hash and a declared hash never agree and comparing them reports drift on every clean database.
2135
+
2136
+ Both sides hash the **whole** live schema, framework tables included. A framework upgrade that adds a `_voltro_*` column therefore shows as drift until the next `db apply` records a new baseline — honest, since the live schema did change, and it self-heals on the apply the upgrade needs anyway.
2137
+
2138
+ ### No baseline yet
2139
+
2140
+ A ledger row written before `liveFingerprint` existed has no baseline, and so does a database whose schema was already current when it upgraded. `voltro db drift` says so and **exits 3**:
2141
+
2142
+ ```sh
2143
+ $ voltro db drift
2144
+ db drift: no drift baseline recorded yet — cannot compare
2145
+ ```
2146
+
2147
+ Run `voltro db apply` once — a no-op apply backfills the baseline too. Until then use `voltro db plan`, which compares declared against live directly.
2029
2148
 
2030
- User-schema only: the fingerprint excludes `_voltro_*` framework tables. Bookkeeping tables grow rows + change row counts as the app runs; including them in the fingerprint would make every boot look drifted.
2149
+ **Exit 3 is deliberately not 0.** "Did not compare" is not "clean", and a CI gate on the exit code has to be able to tell them apart — otherwise it passes vacuously on a stable schema, which is the failure drift detection exists to prevent.
2150
+
2151
+ | exit | meaning |
2152
+ |---|---|
2153
+ | 0 | compared, live matches the baseline |
2154
+ | 3 | no baseline — did NOT compare |
2155
+ | 4 | compared, live diverged |
2031
2156
 
2032
2157
  ## Common causes
2033
2158
 
@@ -2038,16 +2163,30 @@ Someone ran `ALTER TABLE ...` or `CREATE INDEX ...` via psql / DataGrip / Admine
2038
2163
  ```sh
2039
2164
  $ voltro db drift
2040
2165
  db drift: live schema DIVERGED from last applied state
2041
- last applied: 8f507ba1e1aadad5 at 2026-06-15 14:32:00 (plan_mig_5k78)
2042
- live now: a8f2c9d10b3f4e62
2166
+ baseline: 8f507ba1e1aadad5 at 2026-06-15 14:32:00 (plan_mig_5k78)
2167
+ live now: a8f2c9d10b3f4e62
2168
+
2169
+ Something changed the live schema after the last apply. This command can see
2170
+ THAT it changed, not what or who — the fingerprints are hashes, not a diff.
2043
2171
 
2044
- Probable causes:
2045
- - manual DDL ran out-of-band (psql session, another tool)
2046
- - someone applied a plan but the row never made it into _voltro_migration_plans
2172
+ Your DECLARED schema is already satisfied — `voltro db plan` reports 0 operations
2173
+ against this database. So the live schema is not wrong, only unrecorded: something
2174
+ applied a change without going through the planner (a hand-run ALTER, a DBA
2175
+ window, a restored dump), or it touched a table your code does not declare.
2047
2176
 
2048
- To reconcile, run `voltro db plan` to see what your code expects vs the live DB.
2177
+ If that was deliberate and the schema is right, record it:
2178
+
2179
+ voltro db drift --accept
2180
+
2181
+ It updates the latest ledger row's baseline to the live schema and invents no
2182
+ history entry. Drift then measures from here.
2049
2183
  ```
2050
2184
 
2185
+ When `db plan` is NOT empty it says that instead, with the count — so the two
2186
+ cases are told apart by the command rather than left to you. It used to close by
2187
+ asserting that a zero-operation plan meant "a table your code does not declare",
2188
+ which is one of two possibilities and the less likely one.
2189
+
2051
2190
  ### Out-of-band auto-applier
2052
2191
 
2053
2192
  Multiple tools applying to the same DB (the framework + a separate Flyway / Liquibase process / hand-written deploy script). The other tool's changes don't go through `_voltro_migration_plans`.
@@ -2062,31 +2201,36 @@ The drift detector ran against a read-replica that's lagging. Wait for the repli
2062
2201
 
2063
2202
  ## Reconciliation paths
2064
2203
 
2065
- ### Path 1 — adopt the live state by declaring it in TS
2204
+ ### Path 1 — accept the live state
2066
2205
 
2067
- When the live DB IS what you want (the manual DDL is correct, only
2068
- bypassing the planner was sloppy), bring the declared schema up to the
2069
- live shape: edit the `*.entity.ts` files so they describe exactly what
2070
- the live DB now has. The next `voltro db plan` then diffs empty, and a
2071
- `voltro db apply` records a fresh `_voltro_migration_plans` row at the
2072
- new fingerprint — re-baselining history without any DDL.
2206
+ When the live DB IS what you want (the manual DDL is correct, only bypassing the
2207
+ planner was sloppy), first make sure your declaration says so: edit the
2208
+ `*.entity.ts` files until `voltro db plan` diffs empty. Then record the live
2209
+ schema as the baseline:
2073
2210
 
2074
2211
  ```sh
2075
- voltro db plan # confirm the diff is now empty (declared == live)
2076
- voltro db apply --note 'accepting manual DDL from 2026-06-15 — see ticket #789'
2212
+ voltro db plan # must report 0 operations declared == live
2213
+ voltro db drift --accept
2077
2214
  ```
2078
2215
 
2079
- `voltro db apply` with an empty plan writes no DDL; it just locks in
2080
- the current fingerprint with your note. The history shows it:
2216
+ `--accept` writes the current live fingerprint onto the newest ledger row.
2217
+ Drift measures from there, and the next `voltro db drift` exits 0.
2081
2218
 
2082
- ```
2083
- plan_mig_5k79 fp=a8f2c9d1 env=dev src=auto-diff 0 op(s) 12ms ... by=alice
2084
- [note: accepting manual DDL from 2026-06-15 see ticket #789]
2085
- ```
2219
+ **It refuses unless `db plan` is empty**, and that guard is the whole point.
2220
+ Accepting a schema with operations still outstanding would record "this is what
2221
+ we applied" over a state nobody applied, and every later drift check would
2222
+ measure against that fiction. If operations are pending, run `voltro db apply` —
2223
+ that applies them AND writes a real baseline of its own.
2224
+
2225
+ It backfills the newest row rather than inserting one, because no migration ran
2226
+ and a history entry claiming otherwise would be worse than the gap it fills.
2086
2227
 
2087
- There is no metadata-only `--reconcile` flag re-baselining always
2088
- goes through the declare-then-apply loop, so the TS schema stays the
2089
- single source of truth.
2228
+ **An empty `voltro db apply` does NOT re-baseline an existing baseline.** It
2229
+ writes no DDL and no history row, so there is nothing for a `--note` to attach
2230
+ to it will tell you the note was ignored rather than swallow it. (It does fill
2231
+ a baseline that is still NULL, which is a different case: a database that never
2232
+ had one.) `--accept` is the command whose job is to say "the live schema is
2233
+ right, the ledger just did not know".
2090
2234
 
2091
2235
  ### Path 2 — corrective plan against drift
2092
2236
 
@@ -2184,7 +2328,7 @@ The hardest part of drift response is figuring out **what** changed + **who** di
2184
2328
  ```
2185
2329
  Detect: voltro db drift
2186
2330
  Fix code: voltro db apply (apply corrective plan from current diff)
2187
- Adopt DB: edit the *.entity.ts to match live, then voltro db apply (empty plan re-baselines)
2331
+ Adopt DB: edit the *.entity.ts to match live until db plan is empty, then voltro db drift --accept
2188
2332
  Backup: if data was lost, restore from your DB backup system — the framework can't help
2189
2333
  ```
2190
2334
 
@@ -73,6 +73,13 @@ Plain `text()` is **unbounded** — `TEXT` on Postgres/SQLite, `LONGTEXT` on MyS
73
73
 
74
74
  Reach for **`.maxLength(n)`** when the column is a short identifier (a kind, slug, external id, resource discriminator) that participates in a `.unique([...])` or composite `.index([...])`: an unbounded column can only back a HASH long-unique constraint (MariaDB) or a prefix index, whereas a bounded `VARCHAR(n)` takes a plain BTREE key. It's cross-dialect (`VARCHAR(n)` on Postgres/MySQL/MariaDB, `NVARCHAR(n)` on MSSQL, `TEXT` on SQLite) — unlike `raw('varchar(20)')`, which hardcodes one dialect's DDL.
75
75
 
76
+ **Changing the bound on an EXISTING column is a real migration.** The differ compares `maxLength` on both sides, so `text()` → `text().maxLength(64)`, or 64 → 128, produces an operation:
77
+
78
+ - **widening** (a larger bound, or dropping the bound) is `safe` — no value can be lost;
79
+ - **narrowing** is `needs-backfill`: the `ALTER` fails at the database for any existing value longer than the new bound. The plan says so and gives you the query to run first.
80
+
81
+ Not compared on SQLite, which has no length-enforced type — a declared bound there is real, the live column is always `TEXT`, and comparing them would re-plan the same ALTER on every boot.
82
+
76
83
  For **value** constraints (length ranges, regex, email, numeric min/max, cross-field invariants), use table-level `table().validate(Schema)` (see below) — that's the single validation surface, not a pile of per-column modifiers. There are no `.minLength()` / `.pattern()` column modifiers by design.
77
84
 
78
85
  ### `integer()`
@@ -996,7 +996,7 @@ If you write hand-rolled queries that read JSON columns through `unsafe()`, you'
996
996
  > then applies it. Idempotent, and a no-op on every other dialect. Without it,
997
997
  > workflow message/runner storage misbehaves on SQL Server.
998
998
 
999
- `@effect/cluster`'s mssql branch uses `sp_getapplock` for shard claims and `MERGE … WHEN NOT MATCHED THEN INSERT … OUTPUT INSERTED` for runner upserts. Four bugs in `@effect/cluster@0.59.0` mssql code paths fail under the framework's workflow stack; the framework carries a `pnpm patch` (shipped in the CLI at `packages/cli/templates/patches/@effect__cluster@0.59.0.patch`, written into your project by `voltro add mssql`):
999
+ `@effect/cluster`'s mssql branch uses `sp_getapplock` for shard claims and `MERGE … WHEN NOT MATCHED THEN INSERT … OUTPUT INSERTED` for runner upserts. Four bugs in `@effect/cluster@0.60.0` mssql code paths fail under the framework's workflow stack; the framework carries a `pnpm patch` (shipped in the CLI at `packages/cli/templates/patches/@effect__cluster@0.60.0.patch`, written into your project by `voltro add mssql`):
1000
1000
 
1001
1001
  ### Patch 1 — SqlRunnerStorage shard-lock MERGE alias
1002
1002
 
@@ -1024,7 +1024,7 @@ MSSQL forbids subqueries inside OUTPUT clauses (Msg 10705). The original `insert
1024
1024
 
1025
1025
  The cluster's `deliver_at` column is BIGINT (storing millisecond epoch). Tedious binds JS `number` parameters as INT — which overflows for any post-2001 timestamp. The patch casts `deliver_at` to `BigInt` once at the top of `envelopeToRow` so all three message-kind switch arms emit `bigint | null`; other dialects accept bigint fine.
1026
1026
 
1027
- All four patches are dialect-keyed (touch only the `mssql:` branch of `sql.onDialectOrElse`) so postgres / mysql / sqlite paths are bit-identical to upstream. The framework carries them and ships them to your project via `voltro add mssql` (see the note at the top of this section) — there is no newer `@effect/cluster` to bump to (0.59.0 is the latest), so the patch is how the fix reaches you.
1027
+ All four patches are dialect-keyed (touch only the `mssql:` branch of `sql.onDialectOrElse`) so postgres / mysql / sqlite paths are bit-identical to upstream. The framework carries them and ships them to your project via `voltro add mssql` (see the note at the top of this section) — the patch is how the fix reaches you. Upstream still carries these bugs on the current release — the framework re-verifies that on every bump (`git apply --check` against the new tarball) and re-keys the patch, because a `patchedDependencies` key is version-exact and a stale one fails the install.
1028
1028
 
1029
1029
  ## Read replicas — Always-On Availability Groups
1030
1030
 
@@ -1088,7 +1088,7 @@ For a full before-image or per-op fidelity, use **mariadb** (binlog CDC). For ze
1088
1088
  - `voltro/packages/sql-mssql/src/replicationAdapter.ts` — `end_of_log_lsn` / `last_hardened_lsn` compare for RYW
1089
1089
  - `voltro/packages/database/src/sqlCompiler.ts` — TOP / FETCH NEXT dispatch
1090
1090
  - `voltro/packages/database/src/migrate.ts` — mssql DDL branch (line 72)
1091
- - `packages/cli/templates/patches/@effect__cluster@0.59.0.patch` — the four upstream patches (shipped in the CLI; `voltro add mssql` writes it into your project)
1091
+ - `packages/cli/templates/patches/@effect__cluster@0.60.0.patch` — the four upstream patches (shipped in the CLI; `voltro add mssql` writes it into your project)
1092
1092
 
1093
1093
 
1094
1094
 
@@ -92,7 +92,7 @@ Status legend: ✓ shipped · ◐ partial · — planned.
92
92
  | `@voltro/plugin-openapi` | ✓ | OpenAPI 3.1 spec (`GET /openapi.json`) + Swagger-UI (`GET /docs`) generated from `defineRestRoute` descriptors AND (opt-in) rpc procedures (queries/mutations/actions/streams → `POST /rpc/<name>`) — input/output/error Schemas via `JSONSchema.make`. [→ details](/docs/plugins/openapi) |
93
93
  | `@voltro/plugin-versioning` | ✓ | Full row history + time-travel — value snapshot of every insert/update/delete on listed tables into `_voltro_row_history` (rides the ChangeEvent tap); `rowHistory` / `rowAsOf` queries + `restoreAsOf` / `diffVersions`; TTL + per-row cap retention. [→ details](/docs/plugins/versioning) |
94
94
  | `@voltro/plugin-presence` | ✓ | Ephemeral realtime presence — heartbeat roster per channel (`presence.heartbeat`/`list`/`leave` + `usePresence`), a `useTyping` typing indicator, swept `_voltro_presence` table, cross-instance. [→ details](/docs/plugins/presence) |
95
- | `@voltro/plugin-scim` | ✓ | SCIM 2.0 provisioning — Users + Groups REST at `/scim/v2` (bearer-gated) incl. group-membership PATCH/PUT + the RFC 7644 discovery trio (ServiceProviderConfig/Schemas/ResourceTypes), `userName`/`externalId`/`displayName eq` filters, pagination, unique `userName`, `active:false` deactivation; `scim_users`/`scim_groups`. [→ details](/docs/plugins/scim) |
95
+ | `@voltro/plugin-scim` | ✓ | SCIM 2.0 provisioning — Users + Groups REST at `/scim/v2` (bearer-gated) incl. group-membership PATCH/PUT + the RFC 7644 discovery trio (ServiceProviderConfig/Schemas/ResourceTypes), `userName`/`externalId`/`displayName eq` filters, pagination, unique `userName`, `active:false` deactivation; `_voltro_scim_users`/`_voltro_scim_groups`. [→ details](/docs/plugins/scim) |
96
96
  | `@voltro/plugin-sso-saml` | ✓ | Enterprise SAML 2.0 SSO — SP-initiated login + Single Logout (both directions) + ACS + SP metadata under `/saml`; IdP-metadata-URL auto cert rotation, encrypted assertions, clock-skew, SP request signing. Signature verify via `@node-saml/node-saml` (optional+lazy), mints a framework session. [→ details](/docs/plugins/sso-saml) |
97
97
 
98
98
  API keys are **first-class** (not a plugin): `apiKeys: true` in `app.config.ts` → Bearer-key auth + admin-gated `/v1/api-keys` management, hash-only storage. [→ details](/docs/configuration/api-keys)
@@ -143,7 +143,7 @@ create-vs-update: they're *different mutations with different input schemas*, so
143
143
  they're naturally different forms — no "CRUD mode" switch.
144
144
 
145
145
  ```tsx
146
- import { AutoForm } from '@voltro/web'
146
+ import { AutoForm } from '@voltro/ui'
147
147
 
148
148
  // fields from the mutation's input schema; client+server share the schema;
149
149
  // submits via the mutation with op-correct optimistic (insert prepends, etc.)
@@ -177,7 +177,7 @@ A combined create-or-edit screen is a three-line wrapper:
177
177
  object → a `custom` placeholder asking for a render-prop.
178
178
  - **Rung 1 — one custom widget** via a `<Field>` render-prop:
179
179
  ```tsx
180
- import { AutoForm, Field, AsyncSelect } from '@voltro/web'
180
+ import { AutoForm, Field, AsyncSelect } from '@voltro/ui'
181
181
 
182
182
  <AutoForm api="app" mutation="todos.create">
183
183
  {() => (
@@ -2056,7 +2056,7 @@ curl -s -X POST http://localhost:4000/_voltro/inspect/invoke \
2056
2056
  # → { "ok": false, "error": { "_tag": "EntitlementExceeded", "entitlement": "projects", "limit": 3, "used": 3 } }
2057
2057
  ```
2058
2058
 
2059
- Each successful create also logs `[notify:project] → …: Project created` (console channel) and persists a `notification_inbox` row.
2059
+ Each successful create also logs `[notify:project] → …: Project created` (console channel) and persists a `_voltro_notification_inbox` row.
2060
2060
 
2061
2061
  ## Enable durable analytics
2062
2062