@voltro/cli 0.1.4 → 0.1.6

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 (59) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +1 -1
  3. package/dist/apiBuild-B-SMjwR_.js +62 -0
  4. package/dist/apiBuild-Blk0vma3.js +2 -0
  5. package/dist/bin.js +2 -1
  6. package/dist/commands-CN-DLT1Q.js +8466 -0
  7. package/dist/dev-BhBMpHkm.js +9695 -0
  8. package/dist/dev-BjylpfGQ.js +2 -0
  9. package/dist/index.js +1 -1
  10. package/dist/startup.js +1 -1
  11. package/dist/{startupRunner-CRhuUl91.js → startupRunner-DAexyPkM.js} +17 -15
  12. package/package.json +22 -22
  13. package/templates/agent-docs/ai.md +21 -21
  14. package/templates/agent-docs/authentication.md +9 -9
  15. package/templates/agent-docs/cli.md +1 -1
  16. package/templates/agent-docs/data.md +7 -7
  17. package/templates/agent-docs/database/migrations.md +13 -13
  18. package/templates/agent-docs/database/querying.md +6 -6
  19. package/templates/agent-docs/deployment.md +28 -0
  20. package/templates/agent-docs/internationalization.md +26 -1
  21. package/templates/agent-docs/workflows.md +34 -0
  22. package/templates/apps/api-ai/package.json +7 -7
  23. package/templates/apps/api-auth/package.json +7 -7
  24. package/templates/apps/api-backend/package.json +7 -7
  25. package/templates/apps/api-backend-deactivation/package.json +7 -7
  26. package/templates/apps/api-backend-mail/package.json +8 -8
  27. package/templates/apps/api-backend-mariadb/package.json +9 -9
  28. package/templates/apps/api-backend-storage/package.json +8 -8
  29. package/templates/apps/api-data-advanced/package.json +8 -8
  30. package/templates/apps/api-durable/package.json +8 -8
  31. package/templates/apps/api-feature-flags/package.json +8 -8
  32. package/templates/apps/api-governance/package.json +8 -8
  33. package/templates/apps/api-kv/package.json +8 -8
  34. package/templates/apps/api-moderation/package.json +8 -8
  35. package/templates/apps/api-observability/package.json +8 -8
  36. package/templates/apps/api-ratelimit/package.json +8 -8
  37. package/templates/apps/api-rbac/package.json +8 -8
  38. package/templates/apps/api-rest/package.json +6 -6
  39. package/templates/apps/api-saas/package.json +10 -10
  40. package/templates/apps/api-search/package.json +8 -8
  41. package/templates/apps/api-versioning/package.json +8 -8
  42. package/templates/apps/api-webhooks/package.json +7 -7
  43. package/templates/apps/changelog/package.json +6 -6
  44. package/templates/apps/edge-functions/package.json +2 -2
  45. package/templates/apps/frontend-admin/package.json +8 -8
  46. package/templates/apps/frontend-app/package.json +8 -8
  47. package/templates/apps/frontend-blank/package.json +7 -7
  48. package/templates/apps/frontend-contact/package.json +7 -7
  49. package/templates/apps/frontend-dashboard/package.json +7 -7
  50. package/templates/apps/frontend-docs/package.json +7 -7
  51. package/templates/apps/frontend-i18n/package.json +6 -6
  52. package/templates/apps/frontend-landing/package.json +7 -7
  53. package/templates/apps/frontend-spa/package.json +7 -7
  54. package/templates/apps/frontend-ssr/package.json +7 -7
  55. package/templates/apps/frontend-ssr-api/package.json +8 -8
  56. package/templates/apps/frontend-static-blog/package.json +6 -6
  57. package/templates/baselines/compose/docker/api.Dockerfile +3 -0
  58. package/templates/baselines/compose-mariadb/docker/api.Dockerfile +3 -0
  59. package/dist/commands-dErOyJ0j.js +0 -18067
@@ -0,0 +1,2 @@
1
+ import { o as e } from "./dev-BhBMpHkm.js";
2
+ export { e as loadApiConfig };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { i as e, n as t, r as n, t as r } from "./commands-dErOyJ0j.js";
1
+ import { i as e, n as t, r as n, t as r } from "./commands-CN-DLT1Q.js";
2
2
  //#region src/index.ts
3
3
  var i = "framework";
4
4
  //#endregion
package/dist/startup.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as e, t } from "./startupRunner-CRhuUl91.js";
1
+ import { n as e, t } from "./startupRunner-DAexyPkM.js";
2
2
  export { t as loadStartupFiles, e as runStartups };
@@ -1,30 +1,32 @@
1
1
  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
- //#region src/startupRunner.ts
5
- var r = (e) => e.replace(/\.startup\.tsx?$/, ""), i = async (n, i) => {
6
- let a = [];
7
- for (let o of n) {
8
- let n;
4
+ //#region src/appModuleLoader.ts
5
+ var r = /* @__PURE__ */ new Map(), i = (e) => {
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) => {
8
+ let r = [];
9
+ for (let i of t) {
10
+ let t;
9
11
  try {
10
- n = await import(t(o).href);
12
+ t = await a(i);
11
13
  } catch (e) {
12
- i.warn("startup: failed to import", { file: o }, e);
14
+ n.warn("startup: failed to import", { file: i }, e);
13
15
  continue;
14
16
  }
15
- let s = n.default;
17
+ let s = t.default;
16
18
  if (typeof s != "function") {
17
- i.warn("startup: no default-export function", { file: o });
19
+ n.warn("startup: no default-export function", { file: i });
18
20
  continue;
19
21
  }
20
- a.push({
21
- file: o,
22
- id: r(e(o)),
22
+ r.push({
23
+ file: i,
24
+ id: o(e(i)),
23
25
  fn: s
24
26
  });
25
27
  }
26
- return a;
27
- }, a = async (e, t, r) => {
28
+ return r;
29
+ }, c = async (e, t, r) => {
28
30
  let i = [];
29
31
  for (let a of e) {
30
32
  let e = r.child(`startup:${a.id}`), o = {
@@ -68,4 +70,4 @@ var r = (e) => e.replace(/\.startup\.tsx?$/, ""), i = async (n, i) => {
68
70
  } };
69
71
  };
70
72
  //#endregion
71
- export { a as n, i as t };
73
+ export { i, c as n, a as r, s as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -49,27 +49,27 @@
49
49
  "@effect/workflow": "^0.18.2",
50
50
  "@tailwindcss/vite": "^4.3.2",
51
51
  "@vitejs/plugin-react": "^6.0.3",
52
- "@voltro/ai": "0.1.4",
53
- "@voltro/cache": "0.1.4",
54
- "@voltro/data-transfer": "0.1.4",
55
- "@voltro/database": "0.1.4",
56
- "@voltro/env": "0.1.4",
57
- "@voltro/kv": "0.1.4",
58
- "@voltro/logger": "0.1.4",
59
- "@voltro/plugin-auth": "0.1.4",
60
- "@voltro/plugin-broadcast": "0.1.4",
61
- "@voltro/plugin-mail": "0.1.4",
62
- "@voltro/plugin-storage": "0.1.4",
63
- "@voltro/plugin-webhooks": "0.1.4",
64
- "@voltro/protocol": "0.1.4",
65
- "@voltro/runtime": "0.1.4",
66
- "@voltro/serverless": "0.1.4",
67
- "@voltro/sql-mssql": "0.1.4",
68
- "@voltro/sql-mysql": "0.1.4",
69
- "@voltro/sql-postgres": "0.1.4",
70
- "@voltro/sql-sqlite": "0.1.4",
71
- "@voltro/sql-turso": "0.1.4",
72
- "@voltro/workflow": "0.1.4",
52
+ "@voltro/ai": "0.1.6",
53
+ "@voltro/cache": "0.1.6",
54
+ "@voltro/data-transfer": "0.1.6",
55
+ "@voltro/database": "0.1.6",
56
+ "@voltro/env": "0.1.6",
57
+ "@voltro/kv": "0.1.6",
58
+ "@voltro/logger": "0.1.6",
59
+ "@voltro/plugin-auth": "0.1.6",
60
+ "@voltro/plugin-broadcast": "0.1.6",
61
+ "@voltro/plugin-mail": "0.1.6",
62
+ "@voltro/plugin-storage": "0.1.6",
63
+ "@voltro/plugin-webhooks": "0.1.6",
64
+ "@voltro/protocol": "0.1.6",
65
+ "@voltro/runtime": "0.1.6",
66
+ "@voltro/serverless": "0.1.6",
67
+ "@voltro/sql-mssql": "0.1.6",
68
+ "@voltro/sql-mysql": "0.1.6",
69
+ "@voltro/sql-postgres": "0.1.6",
70
+ "@voltro/sql-sqlite": "0.1.6",
71
+ "@voltro/sql-turso": "0.1.6",
72
+ "@voltro/workflow": "0.1.6",
73
73
  "chokidar": "^5.0.0",
74
74
  "esbuild": "^0.28.0",
75
75
  "ioredis": "^5.11.1",
@@ -18,27 +18,27 @@ The implementation is the [Vercel AI SDK](https://sdk.vercel.ai) wrapped behind
18
18
  ## The model
19
19
 
20
20
  ```text
21
- ┌─────────────────────────────────────────┐
22
- │ Vercel AI SDK
23
- │ anthropic / openai / gateway / mock
24
- └────────────────┬────────────────────────┘
25
-
26
-
27
- ┌─────────────────────────────────────────────────────────────────┐
28
- │ @voltro/ai
29
- │ generateText(...) / generateObject(...)
30
- │ streamText(...)
31
- │ embed(...) / embedMany(...)
32
- └────┬─────────────────────────┬──────────────────────────────────┘
33
-
34
-
35
- ┌──────────────────────────────┐ ┌──────────────────────────────────┐
36
- │ *.agent.tsx (descriptor) │ │ *.tool.tsx
37
- │ defineAgent({ name, input })│ │ defineTool({ name, input,
38
- │ *.agent.server.tsx (executor)│ │ output })
39
- │ defineAgentExecutor(desc, { │ │ typed + wired on the executor
40
- │ system, tools, model }) │ │
41
- └──────────────────────────────┘ └──────────────────────────────────┘
21
+ ┌───────────────────────────────────────┐
22
+ │ Vercel AI SDK
23
+ │ anthropic / openai / gateway / mock
24
+ └───────────────────┬───────────────────┘
25
+
26
+
27
+ ┌──────────────────────────────────────────────────────────────────────┐
28
+ │ @voltro/ai
29
+ │ generateText(...) / generateObject(...)
30
+ │ streamText(...)
31
+ │ embed(...) / embedMany(...)
32
+ └────────────────┬────────────────────────────────────┬────────────────┘
33
+
34
+
35
+ ┌────────────────────────────────┐ ┌─────────────────────────────────┐
36
+ │ *.agent.tsx (descriptor) │ │ *.tool.tsx
37
+ │ defineAgent({ name, input }) │ │ defineTool({ name, input,
38
+ │ *.agent.server.tsx (executor) │ │ output })
39
+ │ defineAgentExecutor(desc, { │ │ typed + wired on the executor
40
+ │ system, tools, model }) │ │
41
+ └────────────────────────────────┘ └─────────────────────────────────┘
42
42
  ```
43
43
 
44
44
  ## What's in this section
@@ -20,18 +20,18 @@ Voltro's auth story ships as a plugin: `@voltro/plugin-auth`. It's server-side p
20
20
  ## The pieces
21
21
 
22
22
  ```text
23
- ┌───────────────────────────────────────────────────────────────┐
24
- │ Browser
25
- │ POST /auth/signin ────────┐
26
- │ Cookie: voltro:session=... │
23
+ ┌────────────────────────────────────────────────────────────────┐
24
+ │ Browser
25
+ │ POST /auth/signin ────────┐
26
+ │ Cookie: voltro:session=... │
27
27
  └───────────────────────────────┬─┘ │
28
- │ Set-Cookie (HttpOnly, Secure)│
28
+ │ Set-Cookie (HttpOnly, Secure)
29
29
  ▼ │
30
- ┌───────────────────────────────────────────────────────────────┐
31
- │ api app
30
+ ┌────────────────────────────────────────────────────────────────┐
31
+ │ api app
32
32
  │ handleSignIn → verifyPassword → issueSession │
33
- │ readSession → HMAC verify → Subject
34
- └───────────────────────────────────────────────────────────────┘
33
+ │ readSession → HMAC verify → Subject
34
+ └────────────────────────────────────────────────────────────────┘
35
35
 
36
36
  │ ctx.subject (typed)
37
37
 
@@ -610,7 +610,7 @@ What it does for a web app:
610
610
  3. **Pre-builds the SSR bundle** — every page module compiled to `dist/server/ssrEntry.js` so `voltro start` doesn't need a Vite middleware loader at runtime.
611
611
  4. **Copies `public/`** into `dist/`.
612
612
 
613
- For api apps, `voltro build` bundles the discovery-walked sources + dependencies into a single Node ESM entry at `dist/api.js`. Production servers run it via `node dist/api.js`.
613
+ For api apps, `voltro build` precompiles the whole handler closure every procedure, workflow, subscriber, reaction, aggregate, agent, webhook, cron, startup, `app.config`, and their shared `database`/`lib` deps — into a single esbuild bundle at `.framework/dist-api/apiEntry.js` (the framework + npm deps stay external). `voltro serve` loads that bundle automatically at boot and resolves every handler from it, so production never transpiles TypeScript at runtime. Without a build, `voltro serve` still loads each source module on demand (via the `tsx` loader) exactly as `voltro dev` does — the build is an optimisation, not a requirement.
614
614
 
615
615
  `voltro build` takes a single optional app directory and parses no flags — the SSR bundle is always attempted, and the SSG pre-render always runs for `static`-mode pages.
616
616
 
@@ -36,16 +36,16 @@ sandbox; it resets on refresh):
36
36
  └──────────────────────────┬───────────────────────────────────────┘
37
37
  │ @effect/rpc over WebSocket
38
38
 
39
- ┌──────────────────────────────────────────────────────────────────┐
40
- │ api app
41
- │ *.query.ts + *.query.server.ts — reactive reads
42
- │ *.mutation.ts + *.mutation.server.ts — transactional writes
43
- │ *.action.ts + *.action.server.ts — external I/O / unary
44
- │ *.stream.ts + *.stream.server.ts — one-shot element push
39
+ ┌────────────────────────────────────────────────────────────────────┐
40
+ │ api app
41
+ │ *.query.ts + *.query.server.ts — reactive reads
42
+ │ *.mutation.ts + *.mutation.server.ts — transactional writes
43
+ │ *.action.ts + *.action.server.ts — external I/O / unary
44
+ │ *.stream.ts + *.stream.server.ts — one-shot element push
45
45
  │ *.route.tsx — public raw-HTTP route │
46
46
  │ *.workflow.tsx + *.trigger.tsx — durable work + events │
47
47
  │ *.agent.tsx — persisted AI chat │
48
- └──────────────────────────────────────────────────────────────────┘
48
+ └────────────────────────────────────────────────────────────────────┘
49
49
  ```
50
50
 
51
51
  Most of it goes through one WebSocket (REST routes are the public raw-HTTP exception). Queries and streams are both streaming RPCs, but they mean different things: a query emits reactive snapshot/delta envelopes; a stream emits plain elements and then finishes.
@@ -1175,38 +1175,38 @@ What prod boot DOES is compare the declared-schema fingerprint against the lates
1175
1175
  └────────────────┬────────────────┘
1176
1176
 
1177
1177
 
1178
- ┌─────────────────────────────────┐
1179
- │ 2. CI runs `voltro db plan`
1178
+ ┌──────────────────────────────────┐
1179
+ │ 2. CI runs `voltro db plan`
1180
1180
  │ against a staging-style │
1181
1181
  │ snapshot. The printed plan │
1182
1182
  │ (classes + fingerprints) goes │
1183
1183
  │ into the PR for review │
1184
- └────────────────┬────────────────┘
1184
+ └────────────────┬─────────────────┘
1185
1185
 
1186
1186
 
1187
- ┌─────────────────────────────────┐
1188
- │ 3. Reviewer reads the plan
1187
+ ┌──────────────────────────────────┐
1188
+ │ 3. Reviewer reads the plan
1189
1189
  │ classification, fingerprints │
1190
1190
  │ + DSL annotations │
1191
1191
  │ Approves PR │
1192
- └────────────────┬────────────────┘
1192
+ └────────────────┬─────────────────┘
1193
1193
 
1194
1194
 
1195
- ┌─────────────────────────────────┐
1196
- │ 4. Merge to main
1195
+ ┌──────────────────────────────────┐
1196
+ │ 4. Merge to main
1197
1197
  │ CI/CD deploys new image │
1198
1198
  │ BEFORE traffic switch a │
1199
1199
  │ one-shot job runs │
1200
1200
  │ `voltro db apply` against │
1201
1201
  │ the prod DB (re-diffs live) │
1202
- └────────────────┬────────────────┘
1202
+ └────────────────┬─────────────────┘
1203
1203
 
1204
1204
 
1205
- ┌─────────────────────────────────┐
1206
- │ 5. Traffic switches
1207
- │ Prod boot: fingerprint match
1205
+ ┌───────────────────────────────────┐
1206
+ │ 5. Traffic switches
1207
+ │ Prod boot: fingerprint match
1208
1208
  │ → serves │
1209
- └─────────────────────────────────┘
1209
+ └───────────────────────────────────┘
1210
1210
  ```
1211
1211
 
1212
1212
  There's a slot for the apply step in every common deploy tool (k8s init container, ECS task pre-deploy hook, Heroku release phase, Cloud Run job-on-deploy, Fly.io `release_command`). The shape is identical: run a one-shot container/process that holds the migration credentials + executes `voltro db apply`. It re-diffs the live DB against the deployed code's declared schema and applies the resulting plan — there is no pre-serialised plan file to pass; the apply re-computes the diff at run time. The serving process never gets the migration-grade credentials.
@@ -1499,14 +1499,14 @@ At runtime, every `ChangeEvent` consults this map BEFORE triggering a re-query.
1499
1499
  Stage 1 — the dependency graph tracks per-table dependency. When `posts` changes, the dispatcher finds every subscription whose dependent-table set includes `posts` and triggers a re-query. After the re-query, a shallow row-set compare (JSON.stringify per row) suppresses the delta if the result is shape-identical to the last delivered snapshot.
1500
1500
 
1501
1501
  ```text
1502
- ┌──────────────────────────────────────────────────────────────────┐
1503
- │ Subscription: users.with({ posts: { with: { author: true } } })
1504
-
1502
+ ┌───────────────────────────────────────────────────────────────────────┐
1503
+ │ Subscription: users.with({ posts: { with: { author: true } } })
1504
+
1505
1505
  │ change(users) → re-query → diff vs lastDelivered → maybe delta │
1506
1506
  │ change(posts) → re-query → diff vs lastDelivered → maybe delta │
1507
-
1508
- │ change(orgs) → no-op (not in dependency graph)
1509
- └──────────────────────────────────────────────────────────────────┘
1507
+
1508
+ │ change(orgs) → no-op (not in dependency graph)
1509
+ └───────────────────────────────────────────────────────────────────────┘
1510
1510
  ```
1511
1511
 
1512
1512
  This is CORRECT but expensive: every write to a dependent table costs a SQL round-trip, even when the write touched a column the subscription doesn't care about.
@@ -374,6 +374,34 @@ The app is only stopped when, continuously for the grace window:
374
374
 
375
375
  A long-sleeping workflow (`sleep('3 days')`) is exactly the case dormancy is built for: the app stops, and the orchestrator brings a fresh process up when the sleep is due — the workflow resumes from its journal.
376
376
 
377
+ ## Diagnosing cold-start latency
378
+
379
+ The flip side of scale-to-zero is a cold start on the first hit after an idle period. Every `voltro serve` boot logs how long it took to become ready:
380
+
381
+ ```text
382
+ serve: ready in 1910ms
383
+ ```
384
+
385
+ To see WHERE those milliseconds go, set `VOLTRO_BOOT_TIMING=1` — the same line gains a per-phase breakdown:
386
+
387
+ ```text
388
+ serve: ready in 1910ms { bootMs: 1910, phases: { modules: 1872, config: 0, discover: 18, store: 9, plugins: 1, workflow: 1, ready: 9 } }
389
+ ```
390
+
391
+ The phases, in boot order:
392
+
393
+ | Phase | What it covers |
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.** |
396
+ | `config` | loading `app.config.ts` + the typed-env gate |
397
+ | `discover` | walking the app + loading discovered primitives (plus the precompiled bundle, when `voltro build` produced one) |
398
+ | `store` | opening the data store + cache/kv facades — a real SQL dialect includes the connection handshake here |
399
+ | `plugins` | plugin bind + lifecycle activation |
400
+ | `workflow` | the durable-workflow engine |
401
+ | `ready` | serveApi's own setup — handler layers, subscribers, reactions, aggregates, the HTTP listener |
402
+
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
+
377
405
  ## Tiers (Voltro Cloud — coming soon)
378
406
 
379
407
  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.
@@ -13,7 +13,7 @@ _Voltro's i18n layer (@voltro/i18n) — an opinionated wrap over react-intl, aut
13
13
 
14
14
  Voltro ships internationalization in **`@voltro/i18n`** — an opinionated, thin wrap over [`react-intl`](https://formatjs.io/docs/react-intl/) plus framework auto-wiring. A web app gets a working `<I18nProvider>` from a single field in `app.config.ts`. You never write the provider, and you never import `@voltro/i18n` in your layout.
15
15
 
16
- The public surface is intentionally small — `<I18nProvider>`, `<T>`, `useT`, `useTFn`, `useLocale`, `defineCatalog`, `defineLocale`. Power users who need an API the wrap doesn't expose (custom formatters, rich-text with React-element values, `IntlProvider`'s `timeZone` / `formats` props) `import { … } from 'react-intl'` directly. The wrap is **opt-in, not lock-in** — the library is in your `node_modules`, the wrap is optional.
16
+ The public surface is intentionally small — `<I18nProvider>`, `<T>`, `useT`, `useTFn`, `useLocale`, `useMessages`, `defineCatalog`, `defineLocale`, `pickCatalog`. Power users who need an API the wrap doesn't expose (custom formatters, rich-text with React-element values, `IntlProvider`'s `timeZone` / `formats` props) `import { … } from 'react-intl'` directly. The wrap is **opt-in, not lock-in** — the library is in your `node_modules`, the wrap is optional.
17
17
 
18
18
  ## When to enable
19
19
 
@@ -142,6 +142,31 @@ const locale = useLocale()
142
142
  - **`useT(id, values?)`** — imperative form returning a plain string, for non-JSX call sites: `placeholder`, `aria-label`, `document.title`, toasts, error messages.
143
143
  - **`useTFn()`** — returns a `(id, values?) => string` translator the component captures once. Use when one component needs `t` inside helper functions or `.map()` callbacks — `react-intl` runs once, the closure replays.
144
144
  - **`useLocale()`** — the active locale code, as React state inside the provider.
145
+ - **`useMessages()`** — the active locale's RAW catalog (`useMessages()['some.id']`): the unformatted ICU template, not the formatted output. For when you need the raw string — e.g. to feed your own formatter, or a lookup that must not run ICU.
146
+
147
+ ## Reading a message outside React — `meta({ locale })`
148
+
149
+ `useT` is a hook — it only runs inside a component, against the *active* locale. A page's `meta({ locale })` runs OUTSIDE React (at build / SSR meta-resolution time) and is handed an arbitrary locale string. Use **`pickCatalog`** to resolve a message there:
150
+
151
+ ```ts
152
+ // src/locales/index.ts
153
+ import { pickCatalog } from '@voltro/i18n'
154
+ import en from './en'
155
+ import de from './de'
156
+
157
+ export const getCatalog = (locale?: string) => pickCatalog({ en, de }, locale, 'en')
158
+ ```
159
+
160
+ ```tsx
161
+ // any page — meta gets the active locale (from the URL prefix, or the voltro:lang cookie)
162
+ import { getCatalog } from '../locales'
163
+
164
+ export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
165
+ title: getCatalog(locale)['meta.home.title'],
166
+ })
167
+ ```
168
+
169
+ `pickCatalog(catalogs, locale, defaultLocale)` returns the concrete catalog type, so a known-key lookup is `string` (not `string | undefined`) — exactly what `PageMeta.title` needs. An unknown or `undefined` locale falls back to `defaultLocale`.
145
170
 
146
171
  ## The react-intl escape hatch
147
172
 
@@ -819,6 +819,8 @@ You do not call the cluster layer yourself — the CLI does. The boot log tells
819
819
  [voltro:dev] workflow engine: cluster-sql, dialect=mariadb
820
820
  ```
821
821
 
822
+ Two settings tune this for demanding topologies: **`VOLTRO_WORKFLOW_SHARD_LOCK`** keeps cross-pod handoff correct on a Galera / Percona XtraDB cluster (see [Galera and multi-primary clusters](#galera-and-multi-primary-clusters)), and **`VOLTRO_WORKFLOW_RUNNER_STORAGE`** opts out of SQL runner storage when you don't need handoff (see [Single-process runner storage](#single-process-runner-storage)).
823
+
822
824
  ### The deployment contract
823
825
 
824
826
  A real multi-instance deployment has to give each pod a routable identity. This is the entire operational surface of clustering, and it is what the framework's Helm baseline injects:
@@ -848,6 +850,38 @@ env:
848
850
 
849
851
  That is the whole contract: stable per-pod identity (`POD_NAME` → `server_id`), routable runner host (`POD_IP`), a pinned runner port, the shared `DB_URL`, and CDC on. No application code changes between one instance and four.
850
852
 
853
+ ### Galera and multi-primary clusters
854
+
855
+ Multi-primary MariaDB (Galera) and Percona XtraDB are first-class **and keep full cross-pod handoff** — no proxy, no single-writer endpoint, no loss of durability. This is handled automatically; the paragraphs below are the *why* for operators who need it.
856
+
857
+ By default `@effect/cluster` coordinates which runner owns which shard with **session advisory locks** (`GET_LOCK` on MySQL/MariaDB, `pg_advisory_lock` on Postgres). Advisory locks are **node-local** — a lock held on one Galera node is invisible on the others, and Galera never replicates them. So on a Galera cluster whose app pods reach the database through a load-balancing Service (connections spread across nodes), advisory-lock coordination can't see itself: pods split-brain shard ownership, and the runner-storage bootstrap can wedge before it creates `cluster_runners` (the pod stays **un-Ready** while a shard-lock refresher errors against a `cluster_runners` table it thinks is missing).
858
+
859
+ Voltro resolves this with the **`VOLTRO_WORKFLOW_SHARD_LOCK`** setting (`auto` | `row` | `advisory`, default `auto`):
860
+
861
+ - **`auto`** — probe the live connection (`@@wsrep_on`) and switch to the row-lease path on a Galera / PXC cluster, keep advisory locks everywhere else. No operator config needed.
862
+ - **`row`** — force the row-lease path. `SqlRunnerStorage` coordinates shard ownership with a **certified conditional upsert** on the `cluster_locks` table (`INSERT … ON DUPLICATE KEY UPDATE … WHERE acquired_at < <expiry>`) instead of `GET_LOCK`. Galera certifies that write across *all* nodes — the same reason the cron `advisoryLock` tier uses a claims row, not a session lock — so shard ownership is coordinated correctly no matter which node a connection lands on. Cross-pod handoff works: a dead pod's lease ages out and a surviving pod takes over the shard.
863
+ - **`advisory`** — force the classic `GET_LOCK` path (single-primary only).
864
+
865
+ The boot log reports the resolved mode, and `voltro cluster status` shows it per instance (`shard-lock=row`):
866
+
867
+ ```text
868
+ [voltro:serve] shard-lock coordination: row-based (dialect=mariadb, mode=auto, wsrep/Galera cluster detected)
869
+ ```
870
+
871
+ **Two honest caveats.** Row-lease failover is **expiry-based, not instant** — a dead pod's shards are reclaimed after the lease timeout (seconds), where advisory locks release the instant a connection drops. That's why `auto` keeps the faster advisory path on a single-primary server and only pays the timeout on Galera. And MySQL **Group Replication** (and other non-wsrep multi-primary topologies) aren't auto-detected — set `VOLTRO_WORKFLOW_SHARD_LOCK=row` explicitly there.
872
+
873
+ ### Single-process runner storage
874
+
875
+ If a deployment genuinely does **not** need cross-pod handoff — dev, a single-region single-replica service, or one where each pod owning its own workflows is acceptable — set **`VOLTRO_WORKFLOW_RUNNER_STORAGE=memory`** to skip the SQL runner storage entirely:
876
+
877
+ ```text
878
+ VOLTRO_WORKFLOW_RUNNER_STORAGE=memory
879
+ ```
880
+
881
+ This is the engine `sqlite` uses. It **skips `SqlRunnerStorage`** (no `cluster_runners` / `cluster_locks`, no advisory locks) while **durability is unaffected** — run/message/reply state still persists via `SqlMessageStorage`. The tradeoff is the point: **no cross-pod handoff** — a workflow started on a now-dead pod will not resume on a surviving one. Prefer the row-lease coordination above for multi-replica deployments that need handoff; reach for `memory` only when they don't.
882
+
883
+ `VOLTRO_WORKFLOW_RUNNER_STORAGE=sql` is rejected on `sqlite` / `turso` (no advisory-lock branch there), and an unrecognized `VOLTRO_WORKFLOW_SHARD_LOCK` value fails boot loudly — a misconfiguration never silently selects a broken engine.
884
+
851
885
  ## Durable workflows resume on any instance
852
886
 
853
887
  Workflow durability lives in `@effect/cluster`'s engine, not in any Voltro table. Each workflow's journal (steps, idempotency keys, signals) is persisted to the cluster's storage on the shared database. When a pod dies mid-step:
@@ -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.1.4",
15
- "@voltro/cli": "0.1.4",
16
- "@voltro/database": "0.1.4",
17
- "@voltro/env": "0.1.4",
18
- "@voltro/protocol": "0.1.4",
19
- "@voltro/runtime": "0.1.4",
14
+ "@voltro/ai": "0.1.6",
15
+ "@voltro/cli": "0.1.6",
16
+ "@voltro/database": "0.1.6",
17
+ "@voltro/env": "0.1.6",
18
+ "@voltro/protocol": "0.1.6",
19
+ "@voltro/runtime": "0.1.6",
20
20
  "effect": "^3.21.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@voltro/testing": "0.1.4",
23
+ "@voltro/testing": "0.1.6",
24
24
  "typescript": "^5.7.0",
25
25
  "vitest": "^3.0.0"
26
26
  }
@@ -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.1.4",
16
- "@voltro/database": "0.1.4",
17
- "@voltro/env": "0.1.4",
18
- "@voltro/plugin-auth": "0.1.4",
19
- "@voltro/protocol": "0.1.4",
20
- "@voltro/runtime": "0.1.4",
15
+ "@voltro/cli": "0.1.6",
16
+ "@voltro/database": "0.1.6",
17
+ "@voltro/env": "0.1.6",
18
+ "@voltro/plugin-auth": "0.1.6",
19
+ "@voltro/protocol": "0.1.6",
20
+ "@voltro/runtime": "0.1.6",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.1.4",
24
+ "@voltro/testing": "0.1.6",
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.1.4",
16
- "@voltro/database": "0.1.4",
17
- "@voltro/env": "0.1.4",
18
- "@voltro/plugin-multitenancy": "0.1.4",
19
- "@voltro/protocol": "0.1.4",
20
- "@voltro/runtime": "0.1.4",
15
+ "@voltro/cli": "0.1.6",
16
+ "@voltro/database": "0.1.6",
17
+ "@voltro/env": "0.1.6",
18
+ "@voltro/plugin-multitenancy": "0.1.6",
19
+ "@voltro/protocol": "0.1.6",
20
+ "@voltro/runtime": "0.1.6",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.1.4",
24
+ "@voltro/testing": "0.1.6",
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.1.4",
16
- "@voltro/database": "0.1.4",
17
- "@voltro/env": "0.1.4",
18
- "@voltro/plugin-deactivation": "0.1.4",
19
- "@voltro/protocol": "0.1.4",
20
- "@voltro/runtime": "0.1.4",
15
+ "@voltro/cli": "0.1.6",
16
+ "@voltro/database": "0.1.6",
17
+ "@voltro/env": "0.1.6",
18
+ "@voltro/plugin-deactivation": "0.1.6",
19
+ "@voltro/protocol": "0.1.6",
20
+ "@voltro/runtime": "0.1.6",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.1.4",
24
+ "@voltro/testing": "0.1.6",
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.1.4",
16
- "@voltro/database": "0.1.4",
17
- "@voltro/env": "0.1.4",
18
- "@voltro/plugin-mail": "0.1.4",
19
- "@voltro/plugin-multitenancy": "0.1.4",
20
- "@voltro/protocol": "0.1.4",
21
- "@voltro/runtime": "0.1.4",
15
+ "@voltro/cli": "0.1.6",
16
+ "@voltro/database": "0.1.6",
17
+ "@voltro/env": "0.1.6",
18
+ "@voltro/plugin-mail": "0.1.6",
19
+ "@voltro/plugin-multitenancy": "0.1.6",
20
+ "@voltro/protocol": "0.1.6",
21
+ "@voltro/runtime": "0.1.6",
22
22
  "effect": "^3.21.2",
23
23
  "react": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.1.4",
26
+ "@voltro/testing": "0.1.6",
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.1.4",
16
- "@voltro/database": "0.1.4",
17
- "@voltro/env": "0.1.4",
18
- "@voltro/plugin-multitenancy": "0.1.4",
19
- "@voltro/plugin-storage": "0.1.4",
20
- "@voltro/protocol": "0.1.4",
21
- "@voltro/runtime": "0.1.4",
22
- "@voltro/sql-mysql": "0.1.4",
15
+ "@voltro/cli": "0.1.6",
16
+ "@voltro/database": "0.1.6",
17
+ "@voltro/env": "0.1.6",
18
+ "@voltro/plugin-multitenancy": "0.1.6",
19
+ "@voltro/plugin-storage": "0.1.6",
20
+ "@voltro/protocol": "0.1.6",
21
+ "@voltro/runtime": "0.1.6",
22
+ "@voltro/sql-mysql": "0.1.6",
23
23
  "effect": "^3.21.2"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.1.4",
26
+ "@voltro/testing": "0.1.6",
27
27
  "typescript": "^5.7.0",
28
28
  "vitest": "^3.0.0"
29
29
  }