@voltro/cli 0.2.1 → 0.3.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 (64) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/THIRD-PARTY-NOTICES.md +234 -1
  3. package/bin/voltro.mjs +90 -1
  4. package/dist/apiBuild-DdgYydVJ.js +193 -0
  5. package/dist/apiBuild-cadmH8ca.js +2 -0
  6. package/dist/bin.js +2 -2
  7. package/dist/{commands-BgOfGwLc.js → commands-CXESev-z.js} +2360 -2998
  8. package/dist/dev-DemiMSSl.js +2 -0
  9. package/dist/{dev-BGYmeKEx.js → dev-x_VqbV_8.js} +1538 -1523
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +1 -1
  12. package/dist/serveCommand-Dttqe5Ms.js +1077 -0
  13. package/dist/serveEntry.d.ts +22 -0
  14. package/dist/serveEntry.js +4 -0
  15. package/dist/startup.js +1 -1
  16. package/dist/{startupRunner-DAexyPkM.js → startupRunner-DhlX9nqd.js} +21 -9
  17. package/package.json +27 -19
  18. package/templates/AGENTS.core.md +14 -0
  19. package/templates/AGENTS.md +14 -0
  20. package/templates/agent-docs/_manifest.json +1 -1
  21. package/templates/agent-docs/cli.md +59 -0
  22. package/templates/agent-docs/data.md +39 -0
  23. package/templates/agent-docs/database/advancedqueries.md +27 -0
  24. package/templates/agent-docs/deployment.md +14 -1
  25. package/templates/apps/api-ai/package.json +7 -7
  26. package/templates/apps/api-auth/package.json +8 -8
  27. package/templates/apps/api-backend/package.json +7 -7
  28. package/templates/apps/api-backend-deactivation/package.json +7 -7
  29. package/templates/apps/api-backend-mail/package.json +8 -8
  30. package/templates/apps/api-backend-mariadb/package.json +9 -9
  31. package/templates/apps/api-backend-storage/package.json +8 -8
  32. package/templates/apps/api-data-advanced/package.json +8 -8
  33. package/templates/apps/api-durable/package.json +8 -8
  34. package/templates/apps/api-feature-flags/package.json +9 -9
  35. package/templates/apps/api-governance/package.json +8 -8
  36. package/templates/apps/api-kv/package.json +8 -8
  37. package/templates/apps/api-moderation/package.json +8 -8
  38. package/templates/apps/api-observability/package.json +8 -8
  39. package/templates/apps/api-ratelimit/package.json +8 -8
  40. package/templates/apps/api-rbac/package.json +8 -8
  41. package/templates/apps/api-rest/package.json +7 -7
  42. package/templates/apps/api-saas/package.json +11 -11
  43. package/templates/apps/api-search/package.json +8 -8
  44. package/templates/apps/api-versioning/package.json +8 -8
  45. package/templates/apps/api-webhooks/package.json +8 -8
  46. package/templates/apps/changelog/package.json +6 -6
  47. package/templates/apps/edge-functions/package.json +2 -2
  48. package/templates/apps/frontend-admin/package.json +8 -8
  49. package/templates/apps/frontend-app/package.json +8 -8
  50. package/templates/apps/frontend-blank/package.json +7 -7
  51. package/templates/apps/frontend-contact/package.json +7 -7
  52. package/templates/apps/frontend-dashboard/package.json +7 -7
  53. package/templates/apps/frontend-docs/package.json +7 -7
  54. package/templates/apps/frontend-i18n/package.json +6 -6
  55. package/templates/apps/frontend-landing/package.json +7 -7
  56. package/templates/apps/frontend-spa/package.json +7 -7
  57. package/templates/apps/frontend-ssr/package.json +7 -7
  58. package/templates/apps/frontend-ssr-api/package.json +8 -8
  59. package/templates/apps/frontend-static-blog/package.json +6 -6
  60. package/templates/baselines/compose/docker/api.Dockerfile +10 -5
  61. package/templates/baselines/compose-mariadb/docker/api.Dockerfile +10 -5
  62. package/dist/apiBuild-B0dd-oDo.js +0 -62
  63. package/dist/apiBuild-D09wjKKO.js +0 -2
  64. package/dist/dev-DoTIxucX.js +0 -2
@@ -0,0 +1,22 @@
1
+ declare type AppModule = Record<string, unknown>;
2
+
3
+ /** A lazy loader — used by the serve bundle so an app module is EVALUATED on
4
+ * first `importAppModule` (i.e. during `runServe`, AFTER `registerCoreTables`),
5
+ * not eagerly at bundle-import time (which would run before the core-table
6
+ * registration and blow up store construction). esbuild code-splitting keeps the
7
+ * loaded chunk on the SAME framework/effect instance as the bundle. */
8
+ declare type LazyAppModule = () => Promise<AppModule>;
9
+
10
+ export declare const loadDotEnv: (argv: ReadonlyArray<string>) => void;
11
+
12
+ /** Register prebuilt app modules keyed by ABSOLUTE source path. Values may be the
13
+ * module namespace directly (the eager `apiEntry.js` bundle) OR a lazy
14
+ * `() => import(...)` loader (the serve bundle). Called once by `voltro serve`. */
15
+ export declare const registerAppModules: (mods: Readonly<Record<string, AppModule | LazyAppModule>>) => void;
16
+
17
+ /** Register a driver module pre-bundled by `voltro build` (keyed by package spec). */
18
+ export declare const registerDriver: (spec: string, mod: Record<string, unknown>) => void;
19
+
20
+ export declare const runServe: (args: ReadonlyArray<string>) => Promise<number>;
21
+
22
+ export { }
@@ -0,0 +1,4 @@
1
+ import { At as e, nt as t } from "./dev-x_VqbV_8.js";
2
+ import { a as n } from "./startupRunner-DhlX9nqd.js";
3
+ import { t as r } from "./serveCommand-Dttqe5Ms.js";
4
+ export { e as loadDotEnv, n as registerAppModules, t as registerDriver, r as runServe };
package/dist/startup.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as e, t } from "./startupRunner-DAexyPkM.js";
1
+ import { n as e, t } from "./startupRunner-DhlX9nqd.js";
2
2
  export { t as loadStartupFiles, e as runStartups };
@@ -2,31 +2,43 @@ import { basename as e } from "node:path";
2
2
  import { pathToFileURL as t } from "node:url";
3
3
  import { publishServerError as n } from "@voltro/protocol";
4
4
  //#region src/appModuleLoader.ts
5
- var r = /* @__PURE__ */ new Map(), i = (e) => {
5
+ var r = /* @__PURE__ */ new Map(), i = /* @__PURE__ */ new Map(), a = (e) => {
6
6
  for (let [t, n] of Object.entries(e)) r.set(t, n);
7
- }, a = async (e) => r.get(e) || await import(t(e).href), o = (e) => e.replace(/\.startup\.tsx?$/, ""), s = async (t, n) => {
7
+ }, o = () => r.size > 0, s = async (e) => {
8
+ let n = i.get(e);
9
+ if (n) return n;
10
+ let a = r.get(e);
11
+ if (a !== void 0) {
12
+ if (typeof a == "function") {
13
+ let t = await a();
14
+ return i.set(e, t), t;
15
+ }
16
+ return a;
17
+ }
18
+ return await import(t(e).href);
19
+ }, c = (e) => e.replace(/\.startup\.tsx?$/, ""), l = async (t, n) => {
8
20
  let r = [];
9
21
  for (let i of t) {
10
22
  let t;
11
23
  try {
12
- t = await a(i);
24
+ t = await s(i);
13
25
  } catch (e) {
14
26
  n.warn("startup: failed to import", { file: i }, e);
15
27
  continue;
16
28
  }
17
- let s = t.default;
18
- if (typeof s != "function") {
29
+ let a = t.default;
30
+ if (typeof a != "function") {
19
31
  n.warn("startup: no default-export function", { file: i });
20
32
  continue;
21
33
  }
22
34
  r.push({
23
35
  file: i,
24
- id: o(e(i)),
25
- fn: s
36
+ id: c(e(i)),
37
+ fn: a
26
38
  });
27
39
  }
28
40
  return r;
29
- }, c = async (e, t, r) => {
41
+ }, u = async (e, t, r) => {
30
42
  let i = [];
31
43
  for (let a of e) {
32
44
  let e = r.child(`startup:${a.id}`), o = {
@@ -70,4 +82,4 @@ var r = /* @__PURE__ */ new Map(), i = (e) => {
70
82
  } };
71
83
  };
72
84
  //#endregion
73
- export { i, c as n, a as r, s as t };
85
+ export { a, s as i, u as n, o as r, l as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.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",
@@ -35,6 +35,11 @@
35
35
  "types": "./dist/startup.d.ts",
36
36
  "import": "./dist/startup.js",
37
37
  "default": "./dist/startup.js"
38
+ },
39
+ "./serveEntry": {
40
+ "types": "./dist/serveEntry.d.ts",
41
+ "import": "./dist/serveEntry.js",
42
+ "default": "./dist/serveEntry.js"
38
43
  }
39
44
  },
40
45
  "main": "./dist/index.js",
@@ -47,27 +52,30 @@
47
52
  "@effect/platform-node": "^0.107.0",
48
53
  "@effect/sql": "^0.51.1",
49
54
  "@effect/workflow": "^0.18.2",
55
+ "@voltro/ai": "0.3.0",
56
+ "@voltro/cache": "0.3.0",
57
+ "@voltro/data-transfer": "0.3.0",
58
+ "@voltro/database": "0.3.0",
59
+ "@voltro/env": "0.3.0",
60
+ "@voltro/kv": "0.3.0",
61
+ "@voltro/logger": "0.3.0",
62
+ "@voltro/plugin-auth": "0.3.0",
63
+ "@voltro/plugin-broadcast": "0.3.0",
64
+ "@voltro/plugin-mail": "0.3.0",
65
+ "@voltro/plugin-storage": "0.3.0",
66
+ "@voltro/plugin-webhooks": "0.3.0",
67
+ "@voltro/protocol": "0.3.0",
68
+ "@voltro/runtime": "0.3.0",
69
+ "@voltro/serverless": "0.3.0",
70
+ "@voltro/workflow": "0.3.0",
71
+ "chokidar": "^5.0.0",
72
+ "ioredis": "^5.11.1",
73
+ "ts-morph": "^28.0.0"
74
+ },
75
+ "optionalDependencies": {
50
76
  "@tailwindcss/vite": "^4.3.2",
51
77
  "@vitejs/plugin-react": "^6.0.3",
52
- "@voltro/ai": "0.2.1",
53
- "@voltro/cache": "0.2.1",
54
- "@voltro/data-transfer": "0.2.1",
55
- "@voltro/database": "0.2.1",
56
- "@voltro/env": "0.2.1",
57
- "@voltro/kv": "0.2.1",
58
- "@voltro/logger": "0.2.1",
59
- "@voltro/plugin-auth": "0.2.1",
60
- "@voltro/plugin-broadcast": "0.2.1",
61
- "@voltro/plugin-mail": "0.2.1",
62
- "@voltro/plugin-storage": "0.2.1",
63
- "@voltro/plugin-webhooks": "0.2.1",
64
- "@voltro/protocol": "0.2.1",
65
- "@voltro/runtime": "0.2.1",
66
- "@voltro/serverless": "0.2.1",
67
- "@voltro/workflow": "0.2.1",
68
- "chokidar": "^5.0.0",
69
78
  "esbuild": "^0.28.0",
70
- "ioredis": "^5.11.1",
71
79
  "tsx": "^4.23.0",
72
80
  "vite": "^8.1.4"
73
81
  },
@@ -220,8 +220,22 @@ primitive → just save; the supervised dev loop respawns and regenerates.
220
220
  But DO call `assertOwnTenant(input.tenantId, ctx.request.subject)` (from
221
221
  `@voltro/plugin-multitenancy/guard`) in custom mutations that write raw rows,
222
222
  and declare `error: TenantMismatch` — subscriptions are auto-scoped, writes are not.
223
+ - **Gate authorization declaratively with `guards:`.** Add
224
+ `guards: [{ scope: 'notes:write' }]` to `defineMutation`/`defineQuery`/`defineAction`
225
+ — the framework enforces it BEFORE the executor (before the txn opens), fails
226
+ with a typed `ScopeError` (auto-merged into the wire error union), and checks the
227
+ caller's EFFECTIVE scopes (raw ∪ rbac roles). Guards are browser-safe DATA (scope
228
+ strings + a pure `resource: (input) => id` extractor — never a server fn). Use the
229
+ in-handler `ctx.access.has(scope)` / `yield* ctx.access.require(scope)` (or rbac's
230
+ `permission()`) only for checks that need LOADED data (row ownership).
223
231
  - **Don't store secrets in the schema or in `Subject`.** Declare env via
224
232
  `defineEnv` (`configuration.md`); carry only ids in `Subject`.
233
+ - **In production, `voltro build` BEFORE `voltro serve`.** A production
234
+ (`NODE_ENV=production`) serve REQUIRES the precompiled serve bundle and fails
235
+ loud if it's missing — production never transpiles on demand. The generated
236
+ Dockerfiles already do `voltro build` then `voltro serve`; if you write your
237
+ own prod start, build first. (`voltro dev` + a non-prod local `serve` still use
238
+ tsx.) Depth: the deployment topic.
225
239
  - **Don't copy prod data down unmasked.** `voltro data export` (and
226
240
  `--target api`) reads REAL rows — PII included. Copying prod → dev/stage MUST
227
241
  go through a masking profile (`--profile`; classify columns `.sensitive()` /
@@ -220,8 +220,22 @@ primitive → just save; the supervised dev loop respawns and regenerates.
220
220
  But DO call `assertOwnTenant(input.tenantId, ctx.request.subject)` (from
221
221
  `@voltro/plugin-multitenancy/guard`) in custom mutations that write raw rows,
222
222
  and declare `error: TenantMismatch` — subscriptions are auto-scoped, writes are not.
223
+ - **Gate authorization declaratively with `guards:`.** Add
224
+ `guards: [{ scope: 'notes:write' }]` to `defineMutation`/`defineQuery`/`defineAction`
225
+ — the framework enforces it BEFORE the executor (before the txn opens), fails
226
+ with a typed `ScopeError` (auto-merged into the wire error union), and checks the
227
+ caller's EFFECTIVE scopes (raw ∪ rbac roles). Guards are browser-safe DATA (scope
228
+ strings + a pure `resource: (input) => id` extractor — never a server fn). Use the
229
+ in-handler `ctx.access.has(scope)` / `yield* ctx.access.require(scope)` (or rbac's
230
+ `permission()`) only for checks that need LOADED data (row ownership).
223
231
  - **Don't store secrets in the schema or in `Subject`.** Declare env via
224
232
  `defineEnv` (`configuration.md`); carry only ids in `Subject`.
233
+ - **In production, `voltro build` BEFORE `voltro serve`.** A production
234
+ (`NODE_ENV=production`) serve REQUIRES the precompiled serve bundle and fails
235
+ loud if it's missing — production never transpiles on demand. The generated
236
+ Dockerfiles already do `voltro build` then `voltro serve`; if you write your
237
+ own prod start, build first. (`voltro dev` + a non-prod local `serve` still use
238
+ tsx.) Depth: the deployment topic.
225
239
  - **Don't copy prod data down unmasked.** `voltro data export` (and
226
240
  `--target api`) reads REAL rows — PII included. Copying prod → dev/stage MUST
227
241
  go through a masking profile (`--profile`; classify columns `.sensitive()` /
@@ -35,7 +35,7 @@
35
35
  "group": null,
36
36
  "description": "The voltro CLI — every command, grouped by purpose, with the flags that actually matter.",
37
37
  "path": "agent-docs/cli.md",
38
- "files": 8
38
+ "files": 9
39
39
  },
40
40
  {
41
41
  "id": "configuration",
@@ -25,6 +25,7 @@ The dispatcher routes `voltro <command> [args]` to the matching subcommand and p
25
25
  | [Build & run](/docs/cli/build-and-start) | `build`, `start`, `serve` |
26
26
  | Deploy | `deploy` (`plan` — auto-detect the target tier per app + function), [`serverless`](/docs/deployment/serverless-functions) (`list` / `dev` / `serve` / `build` / `deploy`), [`static`](/docs/deployment/static-sites) (`hosts` / `deploy`) |
27
27
  | [Database](/docs/cli/migrate) | `migrate`, `db` (`plan` / `apply` / `plans` / `drift` / `squash` / `restore-snapshot` / `migrate` / `rollback` / `status` / `seed`) |
28
+ | [Update](/docs/cli/update) | `update` (`--to` / `--dry-run` / `--force` / `--exact`) — bump every `@voltro/*`, install, run the codemods that adapt your source to the new version |
28
29
  | [Data transfer](/docs/cli/data) | `data` (`export` / `import` / `unpack` / `inspect` / `backup` / `restore`) — directory + single-file `.vbundle` bundles, streaming assets, masking, at-rest encryption |
29
30
  | Ops / infra | `cache` (`status` / `flush` / `invalidate`), `add` (`redis`), `baseline` (`list` / `status` / `set`), `schedule-manifest`, [`storage`](/docs/plugins/storage) (`doctor` / `cors`) |
30
31
  | AI / data | `embeddings backfill <table> --text <field> --vector <field>` — (re)embed rows the `vectorEmbedding()` mixin missed (pre-existing rows / a model change); `--dry-run` to preview |
@@ -149,6 +150,7 @@ The HTTP surface is reachable directly too — e.g. `curl -s localhost:4000/_vol
149
150
  - [Dev](/docs/cli/dev) — what happens during `voltro dev`
150
151
  - [Build & start](/docs/cli/build-and-start) — production paths
151
152
  - [Migrate](/docs/cli/migrate) — schema changes end-to-end
153
+ - [Update](/docs/cli/update) — upgrade the framework + run codemods
152
154
  - [Inspect & test](/docs/cli/inspect) — debugging + harness
153
155
 
154
156
 
@@ -1648,3 +1650,60 @@ The manifest is read through a TTL-cached source (~10 seconds): a procedure you
1648
1650
  ## Protocol scope
1649
1651
 
1650
1652
  MCP over JSON-RPC 2.0. `initialize` negotiates the protocol revision (`2025-06-18`, `2025-03-26`, `2024-11-05`) and advertises the `tools`, `resources`, and `prompts` capabilities; methods are `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/read`, `prompts/list`, `prompts/get`. The stdio bin frames this as newline-delimited JSON-RPC; the HTTP bin serves it over Streamable HTTP. Both transports route to the same pure protocol core (`handleMcpRequest`, `callTool`, `listResources`/`readResource`, `listPrompts`/`getPrompt`, `routeHttp`), all exported from `@voltro/mcp`.
1653
+
1654
+
1655
+
1656
+ ---
1657
+
1658
+ <!-- source: en/cli/update.md -->
1659
+ ## Update
1660
+
1661
+ _voltro update — bump the framework to the latest version and run the codemods that adapt your source to any changed APIs._
1662
+
1663
+ `voltro update` upgrades an app to the latest framework release. It does three things in order:
1664
+
1665
+ 1. **Bump** every `@voltro/*` dependency in `package.json` to the target version.
1666
+ 2. **Install** with your package manager (detected from the lockfile — pnpm / npm / yarn / bun).
1667
+ 3. **Run the codemods** shipped with the target version — automatic source rewrites for any breaking API change, plus printed manual steps for anything that can't be automated.
1668
+
1669
+ ```bash
1670
+ voltro update # bump to the latest published version, install, run codemods
1671
+ voltro update --to 0.4.0 # pin an explicit target version
1672
+ voltro update --dry-run # preview the bump + which codemods would run — writes nothing
1673
+ voltro update --force # allow a dirty working tree (not recommended)
1674
+ voltro update --exact # pin exact versions (drop the ^ / ~ range prefix)
1675
+ ```
1676
+
1677
+ ## The clean-tree guard
1678
+
1679
+ Codemods **rewrite your source**, so you need a clean diff to review afterwards. `voltro update` refuses to run on a dirty git working tree — commit or stash first. Use `--dry-run` to preview without touching anything, or `--force` to override the guard (you accept a mixed diff).
1680
+
1681
+ ## What gets bumped
1682
+
1683
+ Every `@voltro/*` entry in `dependencies` and `devDependencies`, with the range style preserved (`^0.3.0` stays caret, `~0.3.0` stays tilde) unless you pass `--exact`. Non-registry specs (`workspace:*`, `catalog:`, `link:`, …) are left untouched — they're already resolved by your monorepo or catalog.
1684
+
1685
+ ## Codemods
1686
+
1687
+ Each breaking public-API change in a release ships a **codemod**. When you update across that release, `voltro update` applies it:
1688
+
1689
+ - A **transform codemod** rewrites your source automatically — renamed imports, moved modules, changed component props, restructured call signatures. The rewrite is scoped to files that actually import the affected symbol.
1690
+ - A **manual codemod** prints written steps during the update, only when your app is affected — for changes that can't be mechanically transformed (a behavior change, a descriptor/executor restructure). Where the affected sites can be found but the fix needs your judgment, a codemod inserts `// TODO(voltro-migration): …` markers so you can locate every spot.
1691
+
1692
+ Codemods that span multiple versions run in order (e.g. upgrading `0.2.0 → 0.4.0` runs the `0.3.0` and `0.4.0` codemods in sequence). Review the resulting diff before committing.
1693
+
1694
+ ## The database is separate
1695
+
1696
+ `voltro update` does **not** touch your database. Framework-owned `_voltro_*` tables (workflow runs, schedules, …) are reconciled by the declarative differ, not by codemods: when a release changes one of those tables, your next `voltro db apply` (or `voltro dev` boot, which auto-applies) picks up the change. After an update:
1697
+
1698
+ ```bash
1699
+ voltro update
1700
+ voltro db apply # reconcile any changed framework tables — NOT voltro db migrate
1701
+ # then run your typecheck to confirm your code compiles against the new API
1702
+ ```
1703
+
1704
+ Use `voltro db apply` (the declarative diff), not `voltro db migrate` (the imperative file-runner) — only the former reconciles framework tables.
1705
+
1706
+ ## Where to read next
1707
+
1708
+ - [Migrate](/docs/cli/migrate) — schema changes end-to-end
1709
+ - [Build & start](/docs/cli/build-and-start) — production paths
@@ -389,6 +389,17 @@ The descriptor is the wire contract. The `.mutation.server.ts` file is the serve
389
389
  5. Commit the transaction.
390
390
  6. Drain the batched change events so matching query subscriptions receive new snapshots or deltas.
391
391
 
392
+ ## Partial updates: `ctx.store.applyDefined`
393
+
394
+ A partial-update mutation should set only the fields the caller actually sent — not overwrite an omitted field with `undefined`. Instead of hand-writing `if (input.x !== undefined) patch.x = input.x` per field, use `ctx.store.applyDefined(input, keys)`:
395
+
396
+ ```ts
397
+ const execute = async (input: UpdateNote, ctx: AppContext) =>
398
+ ctx.store.update('notes', input.id, ctx.store.applyDefined(input, ['title', 'body', 'dueAt']))
399
+ ```
400
+
401
+ It returns a patch containing only the listed keys whose value is not `undefined` (a defined falsy value like `0` / `''` / `false` IS kept). Also importable standalone (`import { applyDefined } from '@voltro/runtime'`) for seeds/tests.
402
+
392
403
  ## Calling From React
393
404
 
394
405
  ```tsx
@@ -465,6 +476,34 @@ const create = useMutation('app', 'notes.create').withOptimistic((cache, input)
465
476
 
466
477
  Use `.withoutOptimistic()` for effects that should not preview locally.
467
478
 
479
+ ### Nested / path-targeted optimistic
480
+
481
+ By default a `target` patches the **flat top-level row array** a query returns, keyed by `id`. When a query returns a **nested array** — a JSON array column (`snapshot.projects`) or a computed/shaped value — add `path` (and, if the item key isn't `id`, `by`) to patch at **item** granularity, with no hand-written `.withOptimistic` reducer:
482
+
483
+ ```ts
484
+ target: {
485
+ table: 'projectRoadmaps', op: 'update',
486
+ path: 'snapshot.projects', // dot-path to the nested array in the value
487
+ identify: (input) => input.projectId, // which item to patch (default input.id)
488
+ }
489
+ ```
490
+
491
+ - `op: 'insert'` appends (or `order: 'prepend'`) a new item into the nested array — safe even on a computed query (a path insert targets a KNOWN document, not a blind top-level add).
492
+ - `op: 'delete'` filters the item out by its key.
493
+ - `by` overrides the item-key field (default `'id'`).
494
+
495
+ Add `match` to patch **only** the entries whose current value satisfies a predicate — the guard that stops a patch bleeding across sibling subscriptions sharing a source table:
496
+
497
+ ```ts
498
+ target: {
499
+ table: 'projectRoadmaps', op: 'update', path: 'snapshot.projects',
500
+ identify: (i) => i.projectId,
501
+ match: (value, input) => value.id === input.roadmapId, // only THIS roadmap's subscription
502
+ }
503
+ ```
504
+
505
+ `path`, `by`, and `match` are browser-safe descriptor data (a dot-path string + a pure predicate) — the same discipline as `identify`/`shape`.
506
+
468
507
  ## Typed Errors
469
508
 
470
509
  ```ts
@@ -219,6 +219,33 @@ text().unique() // single-column, on the column
219
219
 
220
220
  Multi-column uniqueness is declared at the table level with `.unique(name, [cols])` — see [Composite UNIQUE constraints](#composite-unique-constraints) below.
221
221
 
222
+ ## Unique among ACTIVE rows — `.uniqueActive([...])`
223
+
224
+ Enforce uniqueness only among the rows that aren't soft-deleted — "one active roadmap per (project, year)", where a soft-deleted roadmap frees the key for a new one:
225
+
226
+ ```ts
227
+ table('project_roadmaps', {
228
+ id: id(), projectId: text(), year: integer(), deletedAt: timestamp(),
229
+ })
230
+ .softDelete()
231
+ .uniqueActive(['projectId', 'year']) // partial UNIQUE among deletedAt IS NULL
232
+ ```
233
+
234
+ Emits a `CREATE UNIQUE INDEX … WHERE "deletedAt" IS NULL`, so a soft-deleted row leaves the active set and a NEW row with the same key inserts cleanly — no hand-written `generatedAs("CASE WHEN …")` column, and no [resurrection bug](/docs/database/migrations/troubleshooting) where a re-imported soft-deleted key collides. The predicate defaults to the `softDelete()` active set; override it for a custom one:
235
+
236
+ ```ts
237
+ .uniqueActive('byActiveSlug', ['orgId', 'slug'], { where: `"status" = 'open'` })
238
+ ```
239
+
240
+ Cross-dialect:
241
+
242
+ | Dialect | Support |
243
+ |---------------------|----------------------------------------------------------------|
244
+ | postgres / sqlite / mssql | native partial `CREATE UNIQUE INDEX … WHERE` |
245
+ | mysql / mariadb | FAILS LOUDLY at migrate — no partial-index support; a full unique index would forbid re-creating a soft-deleted key. Use a generated STORED column + `.unique([...])` there. |
246
+
247
+ The predicate is emitted verbatim (ANSI double-quoted identifiers, valid on the three supported dialects).
248
+
222
249
  ## When NOT to index
223
250
 
224
251
  - Tables with <10k rows on a fast disk — the cost of maintaining the index outweighs the seq-scan cost.
@@ -392,7 +392,7 @@ The phases, in boot order:
392
392
 
393
393
  | Phase | What it covers |
394
394
  | --- | --- |
395
- | `modules` | node init + loading and compiling the JS module graph (the framework + your app). On a scale-to-zero container this is almost always the dominant phase — it is the cost of evaluating the dependency graph on a cold process, and it scales inversely with your CPU allotment. No app-level change shrinks it — **precompiling the app (`voltro build`) removes only the transpile of your own source files, not the framework graph load.** |
395
+ | `modules` | node init + loading and compiling the JS module graph (the framework + your app). On a scale-to-zero container this is almost always the dominant phase — it is the cost of evaluating the dependency graph on a cold process, and it scales inversely with your CPU allotment. **Precompiling the app (`voltro build`) collapses this phase** see [Precompiled boot](#precompiled-boot) below. |
396
396
  | `config` | loading `app.config.ts` + the typed-env gate |
397
397
  | `discover` | walking the app + loading discovered primitives (plus the precompiled bundle, when `voltro build` produced one) |
398
398
  | `store` | opening the data store + cache/kv facades — a real SQL dialect includes the connection handshake here |
@@ -402,6 +402,19 @@ The phases, in boot order:
402
402
 
403
403
  Read this from the container's own logs at its real CPU allotment, not a beefy dev machine: a `modules` phase that dwarfs everything else means the cold start is dominated by evaluating the dependency graph (raise the container's CPU, or keep one instance warm); a fat `store` phase points at the database connection instead. `VOLTRO_BOOT_TIMING` adds a handful of `performance.now()` calls and one log line — it is safe to leave on in production.
404
404
 
405
+ ## Precompiled boot
406
+
407
+ `voltro build` precompiles the whole serve path — the framework, Effect, and your app — into a single **serve bundle**, and `voltro serve` boots from it directly. Instead of resolving and compiling the full module graph on every cold process, the boot loads one prebuilt file with your app modules as lazy chunks. This collapses the `modules` phase — `serve: ready` drops from ~1000 ms to ~180 ms; the win is larger on a scale-to-zero container with a cold filesystem, where per-module resolution costs the most. It applies whether your app uses a SQL driver or the memory store: your declared driver (e.g. `@voltro/sql-postgres`) is inlined into the bundle, and only its native binding (`pg`) stays external.
408
+
409
+ ```bash
410
+ voltro build ./apps/api # produces .framework/dist-api/serveBundle/
411
+ voltro serve ./apps/api # boots from the bundle automatically
412
+ ```
413
+
414
+ Building an API app produces the bundle, and `voltro serve` boots from it. In **production** (`NODE_ENV=production`) the bundle is **required** — you run `voltro build` before `voltro serve`, and a bundle-build failure is fatal: production **never transpiles on demand**, so it fails loud rather than silently falling back to the slow tsx path. (`voltro dev` and a non-production local `voltro serve` still fall back to tsx as a convenience.) The generated Dockerfiles already do this: `voltro build` at build time, `voltro serve` at start.
415
+
416
+ Because production never transpiles, the serve image needs none of the build toolchain. The framework declares `tsx`, `esbuild`, `vite`, and Tailwind as **optional** dependencies of `@voltro/cli`, and the production Dockerfiles isolate the app with `pnpm --prod --no-optional deploy` — which drops that whole tree (and its native binaries) from the image. A serve image ships only what it runs at runtime: your app, the framework, and the one SQL driver you declared.
417
+
405
418
  ## Tiers (Voltro Cloud — coming soon)
406
419
 
407
420
  Managed cloud hosting is not yet available; the Pro / Enterprise rows below are the planned managed tiers. Today you self-host and run `voltro dormancy` yourself.
@@ -11,16 +11,16 @@
11
11
  "dependencies": {
12
12
  "@effect/platform": "^0.96.1",
13
13
  "@effect/rpc": "^0.75.1",
14
- "@voltro/ai": "0.2.1",
15
- "@voltro/cli": "0.2.1",
16
- "@voltro/database": "0.2.1",
17
- "@voltro/env": "0.2.1",
18
- "@voltro/protocol": "0.2.1",
19
- "@voltro/runtime": "0.2.1",
14
+ "@voltro/ai": "0.3.0",
15
+ "@voltro/cli": "0.3.0",
16
+ "@voltro/database": "0.3.0",
17
+ "@voltro/env": "0.3.0",
18
+ "@voltro/protocol": "0.3.0",
19
+ "@voltro/runtime": "0.3.0",
20
20
  "effect": "^3.21.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@voltro/testing": "0.2.1",
23
+ "@voltro/testing": "0.3.0",
24
24
  "typescript": "^5.7.0",
25
25
  "vitest": "^3.0.0"
26
26
  }
@@ -12,17 +12,17 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.2.1",
16
- "@voltro/database": "0.2.1",
17
- "@voltro/env": "0.2.1",
18
- "@voltro/plugin-auth": "0.2.1",
19
- "@voltro/protocol": "0.2.1",
20
- "@voltro/runtime": "0.2.1",
21
- "@voltro/sql-postgres": "0.2.1",
15
+ "@voltro/cli": "0.3.0",
16
+ "@voltro/database": "0.3.0",
17
+ "@voltro/env": "0.3.0",
18
+ "@voltro/plugin-auth": "0.3.0",
19
+ "@voltro/protocol": "0.3.0",
20
+ "@voltro/runtime": "0.3.0",
21
+ "@voltro/sql-postgres": "0.3.0",
22
22
  "effect": "^3.21.2"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.2.1",
25
+ "@voltro/testing": "0.3.0",
26
26
  "typescript": "^5.7.0",
27
27
  "vitest": "^3.0.0"
28
28
  }
@@ -12,16 +12,16 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.2.1",
16
- "@voltro/database": "0.2.1",
17
- "@voltro/env": "0.2.1",
18
- "@voltro/plugin-multitenancy": "0.2.1",
19
- "@voltro/protocol": "0.2.1",
20
- "@voltro/runtime": "0.2.1",
15
+ "@voltro/cli": "0.3.0",
16
+ "@voltro/database": "0.3.0",
17
+ "@voltro/env": "0.3.0",
18
+ "@voltro/plugin-multitenancy": "0.3.0",
19
+ "@voltro/protocol": "0.3.0",
20
+ "@voltro/runtime": "0.3.0",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.2.1",
24
+ "@voltro/testing": "0.3.0",
25
25
  "typescript": "^5.7.0",
26
26
  "vitest": "^3.0.0"
27
27
  }
@@ -12,16 +12,16 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.2.1",
16
- "@voltro/database": "0.2.1",
17
- "@voltro/env": "0.2.1",
18
- "@voltro/plugin-deactivation": "0.2.1",
19
- "@voltro/protocol": "0.2.1",
20
- "@voltro/runtime": "0.2.1",
15
+ "@voltro/cli": "0.3.0",
16
+ "@voltro/database": "0.3.0",
17
+ "@voltro/env": "0.3.0",
18
+ "@voltro/plugin-deactivation": "0.3.0",
19
+ "@voltro/protocol": "0.3.0",
20
+ "@voltro/runtime": "0.3.0",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.2.1",
24
+ "@voltro/testing": "0.3.0",
25
25
  "typescript": "^5.7.0",
26
26
  "vitest": "^3.0.0"
27
27
  }
@@ -12,18 +12,18 @@
12
12
  "dependencies": {
13
13
  "@react-email/components": "^1.0.12",
14
14
  "@react-email/render": "^1.4.0",
15
- "@voltro/cli": "0.2.1",
16
- "@voltro/database": "0.2.1",
17
- "@voltro/env": "0.2.1",
18
- "@voltro/plugin-mail": "0.2.1",
19
- "@voltro/plugin-multitenancy": "0.2.1",
20
- "@voltro/protocol": "0.2.1",
21
- "@voltro/runtime": "0.2.1",
15
+ "@voltro/cli": "0.3.0",
16
+ "@voltro/database": "0.3.0",
17
+ "@voltro/env": "0.3.0",
18
+ "@voltro/plugin-mail": "0.3.0",
19
+ "@voltro/plugin-multitenancy": "0.3.0",
20
+ "@voltro/protocol": "0.3.0",
21
+ "@voltro/runtime": "0.3.0",
22
22
  "effect": "^3.21.2",
23
23
  "react": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.2.1",
26
+ "@voltro/testing": "0.3.0",
27
27
  "typescript": "^5.7.0",
28
28
  "vitest": "^3.0.0"
29
29
  }
@@ -12,18 +12,18 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.2.1",
16
- "@voltro/database": "0.2.1",
17
- "@voltro/env": "0.2.1",
18
- "@voltro/plugin-multitenancy": "0.2.1",
19
- "@voltro/plugin-storage": "0.2.1",
20
- "@voltro/protocol": "0.2.1",
21
- "@voltro/runtime": "0.2.1",
22
- "@voltro/sql-mysql": "0.2.1",
15
+ "@voltro/cli": "0.3.0",
16
+ "@voltro/database": "0.3.0",
17
+ "@voltro/env": "0.3.0",
18
+ "@voltro/plugin-multitenancy": "0.3.0",
19
+ "@voltro/plugin-storage": "0.3.0",
20
+ "@voltro/protocol": "0.3.0",
21
+ "@voltro/runtime": "0.3.0",
22
+ "@voltro/sql-mysql": "0.3.0",
23
23
  "effect": "^3.21.2"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.2.1",
26
+ "@voltro/testing": "0.3.0",
27
27
  "typescript": "^5.7.0",
28
28
  "vitest": "^3.0.0"
29
29
  }
@@ -10,17 +10,17 @@
10
10
  "test": "voltro test"
11
11
  },
12
12
  "dependencies": {
13
- "@voltro/cli": "0.2.1",
14
- "@voltro/database": "0.2.1",
15
- "@voltro/env": "0.2.1",
16
- "@voltro/plugin-multitenancy": "0.2.1",
17
- "@voltro/plugin-storage": "0.2.1",
18
- "@voltro/protocol": "0.2.1",
19
- "@voltro/runtime": "0.2.1",
13
+ "@voltro/cli": "0.3.0",
14
+ "@voltro/database": "0.3.0",
15
+ "@voltro/env": "0.3.0",
16
+ "@voltro/plugin-multitenancy": "0.3.0",
17
+ "@voltro/plugin-storage": "0.3.0",
18
+ "@voltro/protocol": "0.3.0",
19
+ "@voltro/runtime": "0.3.0",
20
20
  "effect": "^3.21.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@voltro/testing": "0.2.1",
23
+ "@voltro/testing": "0.3.0",
24
24
  "typescript": "^5.7.0",
25
25
  "vitest": "^3.0.0"
26
26
  }
@@ -11,17 +11,17 @@
11
11
  "dependencies": {
12
12
  "@effect/platform": "^0.96.1",
13
13
  "@effect/rpc": "^0.75.1",
14
- "@voltro/cli": "0.2.1",
15
- "@voltro/database": "0.2.1",
16
- "@voltro/env": "0.2.1",
17
- "@voltro/plugin-governance": "0.2.1",
18
- "@voltro/plugin-multitenancy": "0.2.1",
19
- "@voltro/protocol": "0.2.1",
20
- "@voltro/runtime": "0.2.1",
14
+ "@voltro/cli": "0.3.0",
15
+ "@voltro/database": "0.3.0",
16
+ "@voltro/env": "0.3.0",
17
+ "@voltro/plugin-governance": "0.3.0",
18
+ "@voltro/plugin-multitenancy": "0.3.0",
19
+ "@voltro/protocol": "0.3.0",
20
+ "@voltro/runtime": "0.3.0",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.2.1",
24
+ "@voltro/testing": "0.3.0",
25
25
  "typescript": "^5.7.0",
26
26
  "vitest": "^3.0.0"
27
27
  }