@voltro/cli 0.29.0 → 0.30.1
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.
- package/CHANGELOG.md +448 -0
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/dist/apiBuild-Ca6Sq5jy.js +2 -0
- package/dist/{apiBuild-BESZbTjA.js → apiBuild-DmkzRyv7.js} +2 -2
- package/dist/bin.js +3 -3
- package/dist/{checkCommand-BPAtyWs8.js → checkCommand-C4konUy9.js} +435 -304
- package/dist/checkCommand-DHD0aBeR.js +2 -0
- package/dist/{commands-82BDEktg.js → commands-BkJft9K2.js} +2413 -1814
- package/dist/{dbCommand-bCBbk6tz.js → dbCommand-qn__kLEt.js} +344 -265
- package/dist/dbCommand-wfGi3FgG.js +2 -0
- package/dist/{dev-Cg4BKLmi.js → dev-C_NPcLDp.js} +2716 -1782
- package/dist/{dev-DeEJj5T4.js → dev-DXhhZ7-f.js} +1 -1
- package/dist/frameworkTableAssembly-Bn_c4Mnl.js +2 -0
- package/dist/{frameworkTableAssembly-4Db40V23.js → frameworkTableAssembly-Cpgn2e88.js} +155 -141
- package/dist/index.d.ts +7 -2
- package/dist/index.js +1 -1
- package/dist/{inspect-BMvQpwqy.js → inspect-BUUjt773.js} +150 -14
- package/dist/inspect-mmBuRXmy.js +2 -0
- package/dist/{inspectMetrics-DHYygE4s.js → inspectMetrics-CkB2J0Zn.js} +566 -512
- package/dist/{manifestBuild-C4R1EdHn.js → manifestBuild-BLrVuSlM.js} +1 -1
- package/dist/manifestBuild-Dj8Jjoto.js +2 -0
- package/dist/{sdkgen-CW5NbYF0.js → sdkgen-wsWLrq4l.js} +1 -1
- package/dist/serveCommand-HtI4X5kj.js +1577 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-CsCIaW4-.js → start-egkbpugc.js} +177 -175
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.core.md +50 -0
- package/templates/AGENTS.md +53 -2
- package/templates/agent-docs/_index.md +3 -2
- package/templates/agent-docs/_manifest.json +16 -4
- package/templates/agent-docs/ai.md +56 -0
- package/templates/agent-docs/cli.md +46 -0
- package/templates/agent-docs/internationalization.md +8 -7
- package/templates/agent-docs/local-first-mobile.md +18 -6
- package/templates/agent-docs/routing.md +15 -1
- package/templates/agent-docs/security.md +17 -0
- package/templates/agent-docs/templates/apibackends.md +209 -0
- package/templates/agent-docs/templates/appshells.md +144 -1
- package/templates/agent-docs/templates/mobile.md +60 -0
- package/templates/agent-docs/templates/overview.md +17 -7
- package/templates/agent-docs/whats-new.md +36 -243
- package/templates/agent-docs/workflows.md +367 -0
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +10 -10
- package/templates/apps/api-collab/README.md +70 -0
- package/templates/apps/api-collab/app.config.ts +28 -0
- package/templates/apps/api-collab/database/schema.ts +63 -0
- package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +19 -0
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +36 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +20 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +30 -0
- package/templates/apps/api-collab/package.json +30 -0
- package/templates/apps/api-collab/queries/documents.query.server.ts +16 -0
- package/templates/apps/api-collab/queries/documents.query.ts +23 -0
- package/templates/apps/api-collab/template.json +6 -0
- package/templates/apps/api-collab/tests/documents.setBody.test.ts +89 -0
- package/templates/apps/api-collab/tsconfig.json +5 -0
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/app.config.ts +1 -1
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-admin/src/locales/index.ts +1 -1
- package/templates/apps/frontend-app/app.config.ts +1 -1
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-app/src/locales/index.ts +1 -1
- package/templates/apps/frontend-app/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-app/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-auth/src/locales/index.ts +1 -1
- package/templates/apps/frontend-blank/app.config.ts +1 -1
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-blank/src/locales/index.ts +1 -1
- package/templates/apps/frontend-blank/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-blank/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-cms/src/locales/index.ts +1 -1
- package/templates/apps/frontend-collab/README.md +80 -0
- package/templates/apps/frontend-collab/app.config.ts +38 -0
- package/templates/apps/frontend-collab/package.json +33 -0
- package/templates/apps/frontend-collab/src/locales/de.ts +29 -0
- package/templates/apps/frontend-collab/src/locales/en.ts +31 -0
- package/templates/apps/frontend-collab/src/locales/index.ts +15 -0
- package/templates/apps/frontend-collab/src/pages/layout.tsx +39 -0
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +150 -0
- package/templates/apps/frontend-collab/src/pages/page.tsx +186 -0
- package/templates/apps/frontend-collab/template.json +6 -0
- package/templates/apps/frontend-collab/tsconfig.json +11 -0
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/app.config.ts +1 -1
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-dashboard/src/locales/index.ts +1 -1
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-portal/src/locales/index.ts +1 -1
- package/templates/apps/frontend-saas/README.md +1 -1
- package/templates/apps/frontend-saas/app.config.ts +1 -1
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-saas/src/locales/index.ts +1 -1
- package/templates/apps/frontend-spa/app.config.ts +1 -1
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-spa/src/locales/index.ts +1 -1
- package/templates/apps/frontend-spa/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-spa/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-ssr/app.config.ts +1 -1
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr/src/locales/index.ts +1 -1
- package/templates/apps/frontend-ssr/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-ssr/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-ssr-api/app.config.ts +1 -1
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-ssr-api/src/locales/index.ts +1 -1
- package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/frontend-status/src/locales/index.ts +1 -1
- package/templates/apps/mobile-app/README.md +58 -0
- package/templates/apps/mobile-app/app.config.ts +33 -0
- package/templates/apps/mobile-app/babel.config.js +5 -0
- package/templates/apps/mobile-app/metro.config.js +20 -0
- package/templates/apps/mobile-app/package.json +33 -0
- package/templates/apps/mobile-app/src/app/_layout.tsx +61 -0
- package/templates/apps/mobile-app/src/app/index.tsx +62 -0
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +19 -0
- package/templates/apps/mobile-app/src/app/settings.tsx +51 -0
- package/templates/apps/mobile-app/src/client.ts +74 -0
- package/templates/apps/mobile-app/src/lib/deeplinks.ts +39 -0
- package/templates/apps/mobile-app/src/lib/notifications.ts +31 -0
- package/templates/apps/mobile-app/src/lib/sync.ts +29 -0
- package/templates/apps/mobile-app/src/persistence.ts +38 -0
- package/templates/apps/mobile-app/template.json +6 -0
- package/templates/apps/mobile-app/tests/deeplinks.test.ts +32 -0
- package/templates/apps/mobile-app/tests/notifications.test.ts +26 -0
- package/templates/apps/mobile-app/tests/sync.test.ts +34 -0
- package/templates/apps/mobile-app/tsconfig.json +10 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +11 -0
- package/templates/patches/@effect__cluster@0.60.0.patch +6 -6
- package/dist/apiBuild-C8d74T6g.js +0 -2
- package/dist/checkCommand-DBvZvCh1.js +0 -2
- package/dist/dbCommand-CaIVTp-a.js +0 -2
- package/dist/frameworkTableAssembly-BTNirAAk.js +0 -2
- package/dist/inspect-DHeaMZak.js +0 -2
- package/dist/manifestBuild-Ck_90gZy.js +0 -2
- package/dist/serveCommand-BXl8mhZD.js +0 -1489
package/dist/startEntry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { et as e } from "./inspectMetrics-
|
|
2
|
-
import { t } from "./start-
|
|
1
|
+
import { et as e } from "./inspectMetrics-CkB2J0Zn.js";
|
|
2
|
+
import { t } from "./start-egkbpugc.js";
|
|
3
3
|
export { e as loadDotEnv, t as runStartCommand };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.1",
|
|
4
4
|
"description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -62,22 +62,22 @@
|
|
|
62
62
|
"@effect/platform-node": "^0.108.0",
|
|
63
63
|
"@effect/sql": "^0.52.0",
|
|
64
64
|
"@effect/workflow": "^0.19.0",
|
|
65
|
-
"@voltro/ai": "0.
|
|
66
|
-
"@voltro/cache": "0.
|
|
67
|
-
"@voltro/data-transfer": "0.
|
|
68
|
-
"@voltro/database": "0.
|
|
69
|
-
"@voltro/env": "0.
|
|
70
|
-
"@voltro/kv": "0.
|
|
71
|
-
"@voltro/logger": "0.
|
|
72
|
-
"@voltro/plugin-auth": "0.
|
|
73
|
-
"@voltro/plugin-broadcast": "0.
|
|
74
|
-
"@voltro/plugin-mail": "0.
|
|
75
|
-
"@voltro/plugin-storage": "0.
|
|
76
|
-
"@voltro/plugin-webhooks": "0.
|
|
77
|
-
"@voltro/protocol": "0.
|
|
78
|
-
"@voltro/runtime": "0.
|
|
79
|
-
"@voltro/serverless": "0.
|
|
80
|
-
"@voltro/workflow": "0.
|
|
65
|
+
"@voltro/ai": "0.30.1",
|
|
66
|
+
"@voltro/cache": "0.30.1",
|
|
67
|
+
"@voltro/data-transfer": "0.30.1",
|
|
68
|
+
"@voltro/database": "0.30.1",
|
|
69
|
+
"@voltro/env": "0.30.1",
|
|
70
|
+
"@voltro/kv": "0.30.1",
|
|
71
|
+
"@voltro/logger": "0.30.1",
|
|
72
|
+
"@voltro/plugin-auth": "0.30.1",
|
|
73
|
+
"@voltro/plugin-broadcast": "0.30.1",
|
|
74
|
+
"@voltro/plugin-mail": "0.30.1",
|
|
75
|
+
"@voltro/plugin-storage": "0.30.1",
|
|
76
|
+
"@voltro/plugin-webhooks": "0.30.1",
|
|
77
|
+
"@voltro/protocol": "0.30.1",
|
|
78
|
+
"@voltro/runtime": "0.30.1",
|
|
79
|
+
"@voltro/serverless": "0.30.1",
|
|
80
|
+
"@voltro/workflow": "0.30.1",
|
|
81
81
|
"chokidar": "^5.0.0",
|
|
82
82
|
"ioredis": "^5.11.1",
|
|
83
83
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.core.md
CHANGED
|
@@ -67,6 +67,56 @@ Ask in order; stop at the first "yes":
|
|
|
67
67
|
Deep dive: **`data.md`** (queries/mutations/actions/streams/errors),
|
|
68
68
|
**`workflows.md`**, **`scheduling.md`**.
|
|
69
69
|
|
|
70
|
+
**Do NOT hand-roll debounce / one-at-a-time / rate limits around a workflow.**
|
|
71
|
+
They are fields on `workflow({...})`, enforced BEFORE a run exists:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
debounce: { key: (p) => `tour:${p.rowId}`, period: '15 minutes' }
|
|
75
|
+
singleton: { key: (p) => p.tenantId, mode: 'skip' | 'cancel' }
|
|
76
|
+
concurrency: { limit: 5, key: (p) => p.tenantId }
|
|
77
|
+
throttle: { limit: 100, period: '1 minute' } // queues the excess
|
|
78
|
+
rateLimit: { limit: 100, period: '1 minute' } // DROPS the excess
|
|
79
|
+
batch: { item: Item, key: (i) => i.tenantId, maxSize: 100, timeout: '30 seconds' }
|
|
80
|
+
timeouts: { start: '1 hour', finish: '10 minutes' }
|
|
81
|
+
onFailure: 'notifierWorkflowName'
|
|
82
|
+
cancelOn: [{ event: 'jira.issue.deleted', schema: Deleted,
|
|
83
|
+
match: (e, p) => e.issueKey === p.issueKey }]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The hand-rolled version (an idempotency key carrying a timestamp + a re-check
|
|
87
|
+
loop + a round cap) costs one durable cluster entity PER START to express "one
|
|
88
|
+
job, latest deadline". `voltro workflows flow` shows what is queued and why.
|
|
89
|
+
|
|
90
|
+
`cancelOn` reaches a run whose fiber is not executing anything — sleeping,
|
|
91
|
+
suspended, or still queued — which a race inside the body cannot. `match` is
|
|
92
|
+
REQUIRED (the omitted case would cancel every live run); a run that started AFTER
|
|
93
|
+
the event is never cancelled; queued starts of the same workflow are discarded
|
|
94
|
+
too, or the debounced duplicate fires seconds later against the deleted row.
|
|
95
|
+
|
|
96
|
+
Stopping or re-driving MANY runs is `voltro workflows cancel-many` /
|
|
97
|
+
`replay-many`. Both are a DRY RUN until `--commit`, `--limit` is required (the cap
|
|
98
|
+
IS the blast radius; `truncated` says whether more matched), and a cancel needs a
|
|
99
|
+
`--reason` that lands on every affected run.
|
|
100
|
+
|
|
101
|
+
A model call inside a workflow: `aiStep` / `aiObjectStep` from
|
|
102
|
+
`@voltro/ai/workflow`. Pass `store: ctx.store` and the spend is recorded per step;
|
|
103
|
+
the prompt is journaled as a DIGEST unless you write `recordPrompt: 'full'`. Add
|
|
104
|
+
`offload: true` and the run SUSPENDS instead of holding a worker while the model
|
|
105
|
+
thinks — a dispatcher owns the call and resumes the run. Costs one suspend/resume
|
|
106
|
+
round trip (~250ms), so offload the slow calls and leave a fast classification
|
|
107
|
+
inline.
|
|
108
|
+
|
|
109
|
+
**`idempotencyKey` is the execution's IDENTITY, permanently — not a dedupe
|
|
110
|
+
window.** A second start with the same key replays the first run's result
|
|
111
|
+
forever; after it completes the key is SPENT and a genuinely new invocation is a
|
|
112
|
+
silent no-op. So it must vary per unit of work (`` `tour:${rowId}:${editedAt}` ``,
|
|
113
|
+
not `` `tour:${rowId}` ``), while a flow-control `key` — the RESOURCE runs
|
|
114
|
+
compete for — stays stable. Those are two different fields; conflating them is
|
|
115
|
+
what makes "I need to re-arm a key" feel like a missing feature.
|
|
116
|
+
|
|
117
|
+
A start can now come back `queued` / `dropped` / `skipped` with
|
|
118
|
+
`executionId: null`, so narrow on `handle.status` before using the id.
|
|
119
|
+
|
|
70
120
|
### Pick the SERVER primitive (inside a handler)
|
|
71
121
|
|
|
72
122
|
The four above answer "which FILE do I write". This answers "what do I write
|
package/templates/AGENTS.md
CHANGED
|
@@ -67,6 +67,56 @@ Ask in order; stop at the first "yes":
|
|
|
67
67
|
Deep dive: **`data.md`** (queries/mutations/actions/streams/errors),
|
|
68
68
|
**`workflows.md`**, **`scheduling.md`**.
|
|
69
69
|
|
|
70
|
+
**Do NOT hand-roll debounce / one-at-a-time / rate limits around a workflow.**
|
|
71
|
+
They are fields on `workflow({...})`, enforced BEFORE a run exists:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
debounce: { key: (p) => `tour:${p.rowId}`, period: '15 minutes' }
|
|
75
|
+
singleton: { key: (p) => p.tenantId, mode: 'skip' | 'cancel' }
|
|
76
|
+
concurrency: { limit: 5, key: (p) => p.tenantId }
|
|
77
|
+
throttle: { limit: 100, period: '1 minute' } // queues the excess
|
|
78
|
+
rateLimit: { limit: 100, period: '1 minute' } // DROPS the excess
|
|
79
|
+
batch: { item: Item, key: (i) => i.tenantId, maxSize: 100, timeout: '30 seconds' }
|
|
80
|
+
timeouts: { start: '1 hour', finish: '10 minutes' }
|
|
81
|
+
onFailure: 'notifierWorkflowName'
|
|
82
|
+
cancelOn: [{ event: 'jira.issue.deleted', schema: Deleted,
|
|
83
|
+
match: (e, p) => e.issueKey === p.issueKey }]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The hand-rolled version (an idempotency key carrying a timestamp + a re-check
|
|
87
|
+
loop + a round cap) costs one durable cluster entity PER START to express "one
|
|
88
|
+
job, latest deadline". `voltro workflows flow` shows what is queued and why.
|
|
89
|
+
|
|
90
|
+
`cancelOn` reaches a run whose fiber is not executing anything — sleeping,
|
|
91
|
+
suspended, or still queued — which a race inside the body cannot. `match` is
|
|
92
|
+
REQUIRED (the omitted case would cancel every live run); a run that started AFTER
|
|
93
|
+
the event is never cancelled; queued starts of the same workflow are discarded
|
|
94
|
+
too, or the debounced duplicate fires seconds later against the deleted row.
|
|
95
|
+
|
|
96
|
+
Stopping or re-driving MANY runs is `voltro workflows cancel-many` /
|
|
97
|
+
`replay-many`. Both are a DRY RUN until `--commit`, `--limit` is required (the cap
|
|
98
|
+
IS the blast radius; `truncated` says whether more matched), and a cancel needs a
|
|
99
|
+
`--reason` that lands on every affected run.
|
|
100
|
+
|
|
101
|
+
A model call inside a workflow: `aiStep` / `aiObjectStep` from
|
|
102
|
+
`@voltro/ai/workflow`. Pass `store: ctx.store` and the spend is recorded per step;
|
|
103
|
+
the prompt is journaled as a DIGEST unless you write `recordPrompt: 'full'`. Add
|
|
104
|
+
`offload: true` and the run SUSPENDS instead of holding a worker while the model
|
|
105
|
+
thinks — a dispatcher owns the call and resumes the run. Costs one suspend/resume
|
|
106
|
+
round trip (~250ms), so offload the slow calls and leave a fast classification
|
|
107
|
+
inline.
|
|
108
|
+
|
|
109
|
+
**`idempotencyKey` is the execution's IDENTITY, permanently — not a dedupe
|
|
110
|
+
window.** A second start with the same key replays the first run's result
|
|
111
|
+
forever; after it completes the key is SPENT and a genuinely new invocation is a
|
|
112
|
+
silent no-op. So it must vary per unit of work (`` `tour:${rowId}:${editedAt}` ``,
|
|
113
|
+
not `` `tour:${rowId}` ``), while a flow-control `key` — the RESOURCE runs
|
|
114
|
+
compete for — stays stable. Those are two different fields; conflating them is
|
|
115
|
+
what makes "I need to re-arm a key" feel like a missing feature.
|
|
116
|
+
|
|
117
|
+
A start can now come back `queued` / `dropped` / `skipped` with
|
|
118
|
+
`executionId: null`, so narrow on `handle.status` before using the id.
|
|
119
|
+
|
|
70
120
|
### Pick the SERVER primitive (inside a handler)
|
|
71
121
|
|
|
72
122
|
The four above answer "which FILE do I write". This answers "what do I write
|
|
@@ -602,7 +652,7 @@ each plugin's own README.
|
|
|
602
652
|
|
|
603
653
|
| Topic | Open | Summary |
|
|
604
654
|
|---|---|---|
|
|
605
|
-
| **What's new in 0.
|
|
655
|
+
| **What's new in 0.30.1** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
606
656
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
607
657
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
608
658
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -635,7 +685,8 @@ each plugin's own README.
|
|
|
635
685
|
| templates.apiBackends | `node_modules/@voltro/cli/templates/agent-docs/templates/apibackends.md` | The minimal Voltro backend — app.config + schema + one streaming query + one tenant-guarded mutation. Tenant-aware out of the box. |
|
|
636
686
|
| templates.appShells | `node_modules/@voltro/cli/templates/agent-docs/templates/appshells.md` | A marketing landing page — hero, features, CTA. Static-rendered with zero JS on the wire by default. |
|
|
637
687
|
| templates.custom | `node_modules/@voltro/cli/templates/agent-docs/templates/custom.md` | Add a template the CLI can scaffold from — a directory under voltro-templates/apps/ with a manifest and the files to copy. |
|
|
638
|
-
|
|
|
688
|
+
| templates.mobile | `node_modules/@voltro/cli/templates/agent-docs/templates/mobile.md` | An Expo (React Native) app that is the third consumer of your api — the same typed hooks (useSubscription / useMutation), offline-first by default, typed deep links, and device registration for push. Expo owns Metro; voltro dev runs the sibling api. |
|
|
689
|
+
| Templates | `node_modules/@voltro/cli/templates/agent-docs/templates/overview.md` | Thirty-eight dogfooded starter templates ship with the framework — twenty-two backend shapes, fourteen frontend shapes, a serverless function library, and an Expo mobile app. Scaffold any of them with one CLI call. |
|
|
639
690
|
| templates.serverless | `node_modules/@voltro/cli/templates/agent-docs/templates/serverless.md` | A library of standalone *.serverless.ts functions — pure compute, request-header/geo, outbound HTTP, Web Crypto HMAC, an LLM call, status-controlled errors. Run with voltro serverless; ship to node / Cloudflare / Scaleway. No server, no port. |
|
|
640
691
|
| Testing | `node_modules/@voltro/cli/templates/agent-docs/testing.md` | Voltro's test story — the @voltro/testing package, the test pyramid (unit handlers/tools → workflow runner → e2e), and the voltro test / voltro e2e CLI commands. |
|
|
641
692
|
| Workflows | `node_modules/@voltro/cli/templates/agent-docs/workflows.md` | Durable Effect workflows in Voltro — what they are, when to use them, and the current runtime boundaries. |
|
|
@@ -9,7 +9,7 @@ each plugin's own README.
|
|
|
9
9
|
|
|
10
10
|
| Topic | Open | Summary |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
| **What's new in 0.
|
|
12
|
+
| **What's new in 0.30.1** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
13
13
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
14
14
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
15
15
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -42,7 +42,8 @@ each plugin's own README.
|
|
|
42
42
|
| templates.apiBackends | `node_modules/@voltro/cli/templates/agent-docs/templates/apibackends.md` | The minimal Voltro backend — app.config + schema + one streaming query + one tenant-guarded mutation. Tenant-aware out of the box. |
|
|
43
43
|
| templates.appShells | `node_modules/@voltro/cli/templates/agent-docs/templates/appshells.md` | A marketing landing page — hero, features, CTA. Static-rendered with zero JS on the wire by default. |
|
|
44
44
|
| templates.custom | `node_modules/@voltro/cli/templates/agent-docs/templates/custom.md` | Add a template the CLI can scaffold from — a directory under voltro-templates/apps/ with a manifest and the files to copy. |
|
|
45
|
-
|
|
|
45
|
+
| templates.mobile | `node_modules/@voltro/cli/templates/agent-docs/templates/mobile.md` | An Expo (React Native) app that is the third consumer of your api — the same typed hooks (useSubscription / useMutation), offline-first by default, typed deep links, and device registration for push. Expo owns Metro; voltro dev runs the sibling api. |
|
|
46
|
+
| Templates | `node_modules/@voltro/cli/templates/agent-docs/templates/overview.md` | Thirty-eight dogfooded starter templates ship with the framework — twenty-two backend shapes, fourteen frontend shapes, a serverless function library, and an Expo mobile app. Scaffold any of them with one CLI call. |
|
|
46
47
|
| templates.serverless | `node_modules/@voltro/cli/templates/agent-docs/templates/serverless.md` | A library of standalone *.serverless.ts functions — pure compute, request-header/geo, outbound HTTP, Web Crypto HMAC, an LLM call, status-controlled errors. Run with voltro serverless; ship to node / Cloudflare / Scaleway. No server, no port. |
|
|
47
48
|
| Testing | `node_modules/@voltro/cli/templates/agent-docs/testing.md` | Voltro's test story — the @voltro/testing package, the test pyramid (unit handlers/tools → workflow runner → e2e), and the voltro test / voltro e2e CLI commands. |
|
|
48
49
|
| Workflows | `node_modules/@voltro/cli/templates/agent-docs/workflows.md` | Durable Effect workflows in Voltro — what they are, when to use them, and the current runtime boundaries. |
|
|
@@ -269,7 +269,7 @@
|
|
|
269
269
|
"group": "templates.apiBackends",
|
|
270
270
|
"description": "The minimal Voltro backend — app.config + schema + one streaming query + one tenant-guarded mutation. Tenant-aware out of the box.",
|
|
271
271
|
"path": "agent-docs/templates/apibackends.md",
|
|
272
|
-
"files":
|
|
272
|
+
"files": 22
|
|
273
273
|
},
|
|
274
274
|
{
|
|
275
275
|
"id": "templates/appshells",
|
|
@@ -278,7 +278,7 @@
|
|
|
278
278
|
"group": "templates.appShells",
|
|
279
279
|
"description": "A marketing landing page — hero, features, CTA. Static-rendered with zero JS on the wire by default.",
|
|
280
280
|
"path": "agent-docs/templates/appshells.md",
|
|
281
|
-
"files":
|
|
281
|
+
"files": 14
|
|
282
282
|
},
|
|
283
283
|
{
|
|
284
284
|
"id": "templates/custom",
|
|
@@ -289,12 +289,21 @@
|
|
|
289
289
|
"path": "agent-docs/templates/custom.md",
|
|
290
290
|
"files": 1
|
|
291
291
|
},
|
|
292
|
+
{
|
|
293
|
+
"id": "templates/mobile",
|
|
294
|
+
"title": "templates.mobile",
|
|
295
|
+
"section": "Templates",
|
|
296
|
+
"group": "templates.mobile",
|
|
297
|
+
"description": "An Expo (React Native) app that is the third consumer of your api — the same typed hooks (useSubscription / useMutation), offline-first by default, typed deep links, and device registration for push. Expo owns Metro; voltro dev runs the sibling api.",
|
|
298
|
+
"path": "agent-docs/templates/mobile.md",
|
|
299
|
+
"files": 1
|
|
300
|
+
},
|
|
292
301
|
{
|
|
293
302
|
"id": "templates/overview",
|
|
294
303
|
"title": "Templates",
|
|
295
304
|
"section": "Templates",
|
|
296
305
|
"group": null,
|
|
297
|
-
"description": "Thirty-
|
|
306
|
+
"description": "Thirty-eight dogfooded starter templates ship with the framework — twenty-two backend shapes, fourteen frontend shapes, a serverless function library, and an Expo mobile app. Scaffold any of them with one CLI call.",
|
|
298
307
|
"path": "agent-docs/templates/overview.md",
|
|
299
308
|
"files": 1
|
|
300
309
|
},
|
|
@@ -323,7 +332,7 @@
|
|
|
323
332
|
"group": null,
|
|
324
333
|
"description": "Durable Effect workflows in Voltro — what they are, when to use them, and the current runtime boundaries.",
|
|
325
334
|
"path": "agent-docs/workflows.md",
|
|
326
|
-
"files":
|
|
335
|
+
"files": 10
|
|
327
336
|
}
|
|
328
337
|
],
|
|
329
338
|
"plugins": [
|
|
@@ -631,6 +640,7 @@
|
|
|
631
640
|
"api-backend-mariadb": "agent-docs/templates/apibackends.md",
|
|
632
641
|
"api-backend-storage": "agent-docs/templates/apibackends.md",
|
|
633
642
|
"api-backend": "agent-docs/templates/apibackends.md",
|
|
643
|
+
"api-collab": "agent-docs/templates/apibackends.md",
|
|
634
644
|
"api-data-advanced": "agent-docs/templates/apibackends.md",
|
|
635
645
|
"api-durable": "agent-docs/templates/apibackends.md",
|
|
636
646
|
"api-feature-flags": "agent-docs/templates/apibackends.md",
|
|
@@ -648,6 +658,7 @@
|
|
|
648
658
|
"app": "agent-docs/templates/appshells.md",
|
|
649
659
|
"blank": "agent-docs/templates/appshells.md",
|
|
650
660
|
"changelog": "agent-docs/templates/appshells.md",
|
|
661
|
+
"collab": "agent-docs/templates/appshells.md",
|
|
651
662
|
"contact": "agent-docs/templates/appshells.md",
|
|
652
663
|
"custom": "agent-docs/templates/custom.md",
|
|
653
664
|
"dashboard": "agent-docs/templates/appshells.md",
|
|
@@ -655,6 +666,7 @@
|
|
|
655
666
|
"edge-functions": "agent-docs/templates/serverless.md",
|
|
656
667
|
"i18n": "agent-docs/templates/appshells.md",
|
|
657
668
|
"landing": "agent-docs/templates/appshells.md",
|
|
669
|
+
"mobile-app": "agent-docs/templates/mobile.md",
|
|
658
670
|
"overview": "agent-docs/templates/overview.md",
|
|
659
671
|
"spa": "agent-docs/templates/appshells.md",
|
|
660
672
|
"ssr-api": "agent-docs/templates/appshells.md",
|
|
@@ -1821,6 +1821,62 @@ const groundedAnswer = (prompt: string) =>
|
|
|
1821
1821
|
- **A cache outage degrades to always-generate.** Both the lookup and the store are best-effort — a `CacheError` reads as a miss (or a swallowed put), never a failed call. The cache is an optimisation, not a dependency.
|
|
1822
1822
|
- **The object variant stores the DECODED object.** With the memory backend it round-trips by reference; with a RESP backend it is JSON, so a schema whose decoded form is not JSON-safe (class instances, non-plain branded carriers) will not survive a cross-process hit — cache the text form or a JSON-safe projection for those.
|
|
1823
1823
|
|
|
1824
|
+
## A model call inside a workflow — `aiStep`
|
|
1825
|
+
|
|
1826
|
+
`@voltro/ai/workflow` wraps a call as a durable step:
|
|
1827
|
+
|
|
1828
|
+
```ts
|
|
1829
|
+
import { aiStep, aiObjectStep } from '@voltro/ai/workflow'
|
|
1830
|
+
|
|
1831
|
+
const summary = yield* aiStep({
|
|
1832
|
+
name: 'summarise-thread',
|
|
1833
|
+
prompt: `Summarise:\n${thread}`,
|
|
1834
|
+
store: ctx.store,
|
|
1835
|
+
tenantId: payload.tenantId,
|
|
1836
|
+
})
|
|
1837
|
+
```
|
|
1838
|
+
|
|
1839
|
+
Journaling is **not** what this adds — every `step()` is already journaled, so a replay of a plain wrapped `generateText` returns the recorded completion rather than re-calling the model. Three things are different:
|
|
1840
|
+
|
|
1841
|
+
1. **It records what the run cost.** A model call inside a workflow was invisible to `_voltro_ai_usage` unless the app remembered to call `recordAiUsage` by hand — so the spend ledger was systematically missing exactly the calls that run unattended. Pass `store` and every call is recorded, attributed to the workflow and the step.
|
|
1842
|
+
2. **It does not copy the prompt into a second table.** `step({ input })` is written to `_voltro_workflow_run_steps` and rendered in the dashboard; for a prompt built from customer data that is a plaintext copy outside whatever boundary you established for the source. The default records a **digest** plus the length. `recordPrompt: 'full'` exists and has to be typed out.
|
|
1843
|
+
3. **Provider failures retry like provider failures.** The default policy handles a 429 with a `Retry-After` and a 5xx, rather than every app rediscovering that a bare call fails the whole durable run on a rate limit.
|
|
1844
|
+
|
|
1845
|
+
`aiObjectStep` is the schema-constrained form; the schema is the step's success schema too, so the journaled value decodes on replay exactly as it did on the first run.
|
|
1846
|
+
|
|
1847
|
+
Pass `offload: true` and the run stops occupying a worker while the model thinks — see the next section.
|
|
1848
|
+
|
|
1849
|
+
## Offloading the call — `offload: true`
|
|
1850
|
+
|
|
1851
|
+
An inline `aiStep` holds a runner fiber for the length of the model call. At six seconds a call and two hundred concurrent runs, that is two hundred parked workers waiting on a socket, and the cluster's concurrency is spent on latency rather than on work.
|
|
1852
|
+
|
|
1853
|
+
```ts
|
|
1854
|
+
const summary = yield* aiStep({
|
|
1855
|
+
name: 'summarise-thread',
|
|
1856
|
+
prompt: `Summarise:\n${thread}`,
|
|
1857
|
+
store: ctx.store,
|
|
1858
|
+
offload: true,
|
|
1859
|
+
})
|
|
1860
|
+
```
|
|
1861
|
+
|
|
1862
|
+
The run **suspends**: the worker is released, the wait lives as a row in `_voltro_ai_inferences`, and a dispatcher owns the socket. Two hundred waiting runs become two hundred rows and (by default) four in-flight requests.
|
|
1863
|
+
|
|
1864
|
+
Nothing about this needs a third party to operate an inference tier. It needs something to own the socket while the run sleeps — and a server process is something. The two pieces it is built from already existed: durable suspend/resume (`awaitSignalSuspending`, built for human-in-the-loop waits) and a leased work queue with a coordinated drainer (the same shape the admission queue has).
|
|
1865
|
+
|
|
1866
|
+
**The cost, so you can decide per call.** A suspend/resume round trip adds the dispatcher's poll interval (250 ms) plus one engine wake. On a six-second call that is under 5%; on a 200 ms classification call it doubles the latency. So it is a mode, not a default: offload the calls that are slow enough for a worker to be worth freeing — which is most of them — and leave the fast ones inline.
|
|
1867
|
+
|
|
1868
|
+
**What the queue guarantees.**
|
|
1869
|
+
|
|
1870
|
+
- The enqueue is idempotent. The row id is derived from the execution and the step name, so a replay cannot queue — and pay for — the same call twice.
|
|
1871
|
+
- The claim is a conditional update, not a read-then-write. Two dispatchers cannot both perform (and both bill) one call.
|
|
1872
|
+
- The order is **perform → resume the run → mark the row**. A crash between the resume and the mark leaves a row whose lease expires and is reclaimed, and the second resume of a resolved deferred is a no-op. The other order would leave a run waiting for a signal nobody will send again.
|
|
1873
|
+
- A give-up **resumes the run with the failure**. A queued call that was abandoned without telling its run is the one unrecoverable outcome here, and the ordering exists to rule it out.
|
|
1874
|
+
- Retries follow the same rules as the inline policy, `Retry-After` included, so the two modes do not back off differently.
|
|
1875
|
+
|
|
1876
|
+
`aiObjectStep({ offload: true })` renders your schema to JSON Schema for the dispatcher — a JavaScript Schema cannot be journaled — and still **decodes on the awaiting side**, where the real schema exists.
|
|
1877
|
+
|
|
1878
|
+
The Flow tab shows the queue: what is waiting and for how long, which calls have been waiting more than two minutes, which claims have a lease their dispatcher will never release, and the dispatcher's own last tick. A run parked on an offloaded call reads `suspended` in the run list with no step row yet, so this is the only view of the wait while it is happening.
|
|
1879
|
+
|
|
1824
1880
|
## Deliberately your call
|
|
1825
1881
|
|
|
1826
1882
|
The toolkit prices + records + gates; a few things stay explicit by design:
|
|
@@ -1181,6 +1181,33 @@ re-open a hole nor hide one, and it is consulted **last**: an executor that gain
|
|
|
1181
1181
|
a real guard is reported as guarded whether or not its line is still there. The
|
|
1182
1182
|
list can only shrink unless someone adds to it deliberately.
|
|
1183
1183
|
|
|
1184
|
+
**Two kinds of line, because "debt" and "reviewed" are different claims.** A bare
|
|
1185
|
+
tag is debt. A tag with `reviewed=<why>` says a human read the executor and found
|
|
1186
|
+
it genuinely open — constrained by something the scanner cannot see:
|
|
1187
|
+
|
|
1188
|
+
```txt
|
|
1189
|
+
teams.deleteSubTeam
|
|
1190
|
+
inquiries.publicFeed reviewed=public by design; returns only published rows
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
The reason is required: `reviewed=` with no why is the claim without the
|
|
1194
|
+
evidence, and doctor refuses it. A bare tag is always available and is the honest
|
|
1195
|
+
alternative. The two are counted and printed apart, and
|
|
1196
|
+
`--write-authz-allowlist` **preserves** reviewed lines rather than flattening
|
|
1197
|
+
them back to debt.
|
|
1198
|
+
|
|
1199
|
+
#### Reading the whole list
|
|
1200
|
+
|
|
1201
|
+
The human view prints the 20 most severe unchecked executors. The complete scan
|
|
1202
|
+
— every finding, the counts, the inferred guard vocabulary, and the debt/reviewed
|
|
1203
|
+
split — is in `voltro doctor --json` under `authz`:
|
|
1204
|
+
|
|
1205
|
+
```bash
|
|
1206
|
+
voltro doctor --json | jq '.authz.unchecked[] | {tag, why, path}'
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
Nothing is truncated there. If you are triaging, work from the JSON.
|
|
1210
|
+
|
|
1184
1211
|
#### Before you hand-roll another check
|
|
1185
1212
|
|
|
1186
1213
|
If your checks are imperative because a scope cannot express "may this subject
|
|
@@ -1853,16 +1880,35 @@ voltro workflows resume <runId>
|
|
|
1853
1880
|
voltro workflows signal <runId> --name approval # inject a named signal
|
|
1854
1881
|
voltro workflows update <runId> --name …
|
|
1855
1882
|
voltro workflows children <parentExecutionId>
|
|
1883
|
+
voltro workflows flow # the admission queue + ledger
|
|
1884
|
+
voltro workflows pause|unpause <workflowName> # stop/restart admission fleet-wide
|
|
1885
|
+
voltro workflows cancel-many --reason "…" # DRY RUN until --commit
|
|
1886
|
+
voltro workflows replay-many --mode redrive # DRY RUN until --commit
|
|
1887
|
+
voltro workflows inferences # offloaded model calls in flight
|
|
1856
1888
|
```
|
|
1857
1889
|
|
|
1890
|
+
`inferences` shows what nothing else can: a run parked on an offloaded model call reads `suspended` in the run list with no step row yet, so during a slow provider — the moment you would look — the run list has nothing to say.
|
|
1891
|
+
|
|
1858
1892
|
Underneath, workflow state lives in the `_voltro_workflow_runs` + `_voltro_workflow_run_steps` tables and is surfaced live by the dashboard's Workflows panel. The same data is reachable over HTTP:
|
|
1859
1893
|
|
|
1860
1894
|
```bash
|
|
1861
1895
|
curl -s localhost:$PORT/_voltro/inspect/workflows/runs | jq # recent runs
|
|
1862
1896
|
curl -s "localhost:$PORT/_voltro/inspect/workflows/runs/<runId>/steps" | jq # step-by-step
|
|
1863
1897
|
curl -s "localhost:$PORT/_voltro/inspect/workflows/runs/<runId>/events" | jq # the run's event log
|
|
1898
|
+
curl -s "localhost:$PORT/_voltro/inspect/workflows/stats?hours=24" | jq # bucketed run activity (the dashboard chart)
|
|
1864
1899
|
```
|
|
1865
1900
|
|
|
1901
|
+
The runs endpoint filters **server-side**, so a triage query over a large run history costs one narrow page instead of the whole table:
|
|
1902
|
+
|
|
1903
|
+
```bash
|
|
1904
|
+
# multi-status + tag search + source + id-prefix + time range — all composable
|
|
1905
|
+
curl -s "localhost:$PORT/_voltro/inspect/workflows/runs?statuses=failed,cancelled&q=orders&source=workflow-rpc&idPrefix=wfrun_&from=2026-08-01T00:00:00Z&to=2026-08-08T00:00:00Z" | jq
|
|
1906
|
+
```
|
|
1907
|
+
|
|
1908
|
+
`statuses` is a comma list; `q` is a case-insensitive tag substring; `idPrefix` matches the run id **or** the execution id (you never have to know which kind your log line carried); `from`/`to` bound `startedAt`. The dashboard's filter bar sends exactly these params.
|
|
1909
|
+
|
|
1910
|
+
`/workflows/stats` returns ~48 buckets over a trailing window (`hours`, default 24, max 168; optional `tag`), each with `started` / `succeeded` / `failed` / `cancelled` counts, plus per-workflow totals. When the window held more runs than the scan cap, the response says `truncated: true` — the chart renders that as a warning, because a silently-truncated chart shows throughput dropping at exactly the moment it spiked.
|
|
1911
|
+
|
|
1866
1912
|
Each run row carries ID, name, status (running / succeeded / failed / dead), step count, last completed step, and duration. The per-run action endpoint matches `…/workflows/runs/<runId>/<action>` for `cancel` / `retry` / `suspend` / `resume` / `signal` — the `voltro workflows` subcommands and the dashboard's run-detail buttons both POST to these:
|
|
1867
1913
|
|
|
1868
1914
|
```bash
|
|
@@ -45,7 +45,7 @@ The framework's generated `.framework/app.tsx` wraps the Router in `<I18nProvide
|
|
|
45
45
|
|
|
46
46
|
Server-side, the active locale is determined by, in priority order:
|
|
47
47
|
|
|
48
|
-
1. **`voltro:
|
|
48
|
+
1. **`voltro:locale` cookie** — the user's explicit choice (written by `@voltro/ui-shadcn`'s ProfileMenu).
|
|
49
49
|
2. **`Accept-Language` header** — the browser/OS preference, q-weighted and sorted per RFC 4647.
|
|
50
50
|
3. **`defaultLocale`** — last-resort fallback.
|
|
51
51
|
|
|
@@ -240,7 +240,7 @@ export const getCatalog = (locale?: string) => pickCatalog({ en, de }, locale, '
|
|
|
240
240
|
```
|
|
241
241
|
|
|
242
242
|
```tsx
|
|
243
|
-
// any page — meta gets the active locale (from the URL prefix, or the voltro:
|
|
243
|
+
// any page — meta gets the active locale (from the URL prefix, or the voltro:locale cookie)
|
|
244
244
|
import { getCatalog } from '../locales'
|
|
245
245
|
|
|
246
246
|
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
@@ -330,14 +330,14 @@ The wrap deliberately doesn't re-export everything, to keep the framework's bles
|
|
|
330
330
|
<!-- source: en/i18n/url-strategies.md -->
|
|
331
331
|
## URL strategies
|
|
332
332
|
|
|
333
|
-
_Cookie-only vs URL-prefix routing (Strategy A / B), the URL-prefix integration sketch, resolveLocale on the /server subpath, the voltro:
|
|
333
|
+
_Cookie-only vs URL-prefix routing (Strategy A / B), the URL-prefix integration sketch, resolveLocale on the /server subpath, the voltro:locale / voltro:theme cookie convention._
|
|
334
334
|
|
|
335
335
|
There are two ways an i18n app can encode the active locale. Pick **one**, based on what the app's URLs are *for*.
|
|
336
336
|
|
|
337
337
|
## Strategy A — cookie-only (default for product apps)
|
|
338
338
|
|
|
339
339
|
- All locales are served from the **same** URL (`/dashboard`, `/settings`).
|
|
340
|
-
- The `voltro:
|
|
340
|
+
- The `voltro:locale` cookie + `Accept-Language` determine which catalog renders.
|
|
341
341
|
- The ProfileMenu's language switch — or the standalone `<LocaleSwitcher>` from `@voltro/ui-shadcn` — writes the cookie and reloads.
|
|
342
342
|
- Use when URLs are **functional** (`/dashboard/projects/42/deployments`) and don't need to encode the language. This is most apps.
|
|
343
343
|
|
|
@@ -511,15 +511,16 @@ The `/server` subpath is convention. Both that path and the main entry are pure
|
|
|
511
511
|
|
|
512
512
|
## Cookie convention
|
|
513
513
|
|
|
514
|
-
The framework reads + writes **`voltro:
|
|
514
|
+
The framework reads + writes **`voltro:locale`** for the active language choice (lowercase IETF tag: `en`, `de`, `fr-CA`). `@voltro/ui-shadcn`'s ProfileMenu and the framework's auto-wired resolver agree on this name — **don't pick a different one in app code.**
|
|
515
515
|
|
|
516
|
-
For theme the parallel cookie is **`voltro:theme`** (values `'system' | 'light' | 'dark'`). Both are managed by the kit's ProfileMenu out of the box; the helpers live in `@voltro/ui-shadcn` (`THEME_COOKIE`, `
|
|
516
|
+
For theme the parallel cookie is **`voltro:theme`** (values `'system' | 'light' | 'dark'`). Both are managed by the kit's ProfileMenu out of the box; the helpers live in `@voltro/ui-shadcn` (`THEME_COOKIE`, `LOCALE_COOKIE`, `getCookie`, `setCookie`, `deleteCookie`, `parsePreferenceCookies`, `applyTheme`).
|
|
517
517
|
|
|
518
518
|
## Anti-patterns
|
|
519
519
|
|
|
520
520
|
- **Don't mix strategies within one app.** Pick cookie-only *or* URL-prefix per app; mixing them produces ambiguous canonical URLs and broken language switching.
|
|
521
521
|
- **Don't read `Accept-Language` on the client.** It's server-only — `navigator.languages` can diverge from what the server saw and cause a hydration mismatch.
|
|
522
|
-
- **Don't invent your own cookie name.** The kit and the resolver only agree on `voltro:
|
|
522
|
+
- **Don't invent your own cookie name.** The kit and the resolver only agree on `voltro:locale` / `voltro:theme`.
|
|
523
|
+
- **Don't hardcode the name either — import the constant.** `LOCALE_COOKIE` / `THEME_COOKIE` from `@voltro/ui-shadcn`. A cookie name the framework READS and your app WRITES is a public API, and it is the only kind where both sides can disagree with nothing failing: the resolver finds nothing, falls back to `Accept-Language`, and a user's language choice quietly stops working — but only for the users whose browser language differs from their choice, which is the least likely case anyone tests. With the constant, a rename in the framework is a compile error in your app. `voltro doctor` flags a written literal for exactly this reason.
|
|
523
524
|
|
|
524
525
|
|
|
525
526
|
|
|
@@ -265,11 +265,23 @@ code already speaks:
|
|
|
265
265
|
|
|
266
266
|
_"@voltro/react-native — the credential-free mobile plumbing: registerDevice + the _voltro_devices table, defineDeepLink + its matcher, useBackgroundSync, and offline-first client defaults + connection status."_
|
|
267
267
|
|
|
268
|
-
The
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
268
|
+
The React-client bindings are **import-safe** in React Native — every DOM touch
|
|
269
|
+
in `@voltro/client` is `typeof window`-guarded, so nothing crashes at import.
|
|
270
|
+
What is **not done yet** is wiring the client's runtime for RN: building the
|
|
271
|
+
`ApiHandle`s (runtime + subscription cache + rpc client) over a native
|
|
272
|
+
WebSocket, plus an RN persistence adapter and a NetInfo connection signal. That
|
|
273
|
+
is Phase **M0** of the mobile plan — until it lands, the reactive loop is
|
|
274
|
+
unproven on a device. `@voltro/react-native` ships the mobile-specific plumbing
|
|
275
|
+
that works **today**, limited to the parts that need **no per-tenant credentials
|
|
276
|
+
and no native runtime**: device registration, background-sync scheduling,
|
|
277
|
+
offline-first defaults, a connection-status surface, and the deep-link
|
|
278
|
+
declaration shape.
|
|
279
|
+
|
|
280
|
+
> **Scaffold a mobile app.** `voltro create-project acme --api=api-backend
|
|
281
|
+
> --mobile` (or `voltro add-app mobile --template mobile-app`) scaffolds an Expo
|
|
282
|
+
> app that consumes your api with the same typed hooks. Expo owns Metro
|
|
283
|
+
> (`expo start` / `expo run:ios`), not `voltro dev`. See
|
|
284
|
+
> [the `mobile-app` template](/docs/templates/mobile-app).
|
|
273
285
|
|
|
274
286
|
The package **root is RN-safe** — no `node:*`, no `@voltro/database`, and React
|
|
275
287
|
is reached only through the hooks (an optional peer). The `_voltro_devices` table
|
|
@@ -409,6 +421,6 @@ not built here:
|
|
|
409
421
|
|---|---|
|
|
410
422
|
| **APNs / FCM sender adapters** | Need per-tenant Apple Developer / Firebase credentials — genuinely external, managed via provider provisioning. Registration stores the token; sending to it is the seam. |
|
|
411
423
|
| **Native module bindings** (camera, biometrics, secure token storage) | Need a native runtime this TS package cannot provide. |
|
|
412
|
-
| **Swift / Kotlin SDK generators** |
|
|
424
|
+
| **Swift / Kotlin SDK generators** | **Built + golden-tested** — `voltro build api --target swift\|kotlin` emits a native SDK package. What is deferred is *compiling* the emitted package (`swiftc` / Gradle): that is a mobile-CI step, no cross-language toolchain lives in the framework repo. |
|
|
413
425
|
| **Universal-links / App-Links file automation** (`apple-app-site-association`, `assetlinks.json`) | A deployment-layer concern, not a client primitive. |
|
|
414
426
|
| **`*.deepLink.ts` codegen discovery** | One additive file after the release settles; the descriptor shape is final, so register links via `matchFirstDeepLink()` today. |
|
|
@@ -556,6 +556,20 @@ Use SSR for:
|
|
|
556
556
|
- **Personalised content** — recommendations, "your" anything.
|
|
557
557
|
- **Search result pages** — the query string changes per request.
|
|
558
558
|
|
|
559
|
+
### What the server render sees
|
|
560
|
+
|
|
561
|
+
An SSR render is given the request, not a guess at it: the matched `pathname`
|
|
562
|
+
and `params`, the request's cookies and headers (`useServerRequest()`), and the
|
|
563
|
+
**query string**. `useSearchParams()` is the supported reader and works on both
|
|
564
|
+
sides — on the server it reads the request URL, on the client
|
|
565
|
+
`window.location.search` — so a page keyed off `?tab=…` renders the same markup
|
|
566
|
+
in both places.
|
|
567
|
+
|
|
568
|
+
That last part is worth stating because it is the thing a mismatch is made of.
|
|
569
|
+
Anything the server renders from a value the client computes differently
|
|
570
|
+
hydrates with a warning, even when the DOM happens to agree; reach for the hook
|
|
571
|
+
rather than reading the router context directly.
|
|
572
|
+
|
|
559
573
|
Cost: every request triggers a fresh render. For very high-traffic pages, prefer ISR.
|
|
560
574
|
|
|
561
575
|
### Streaming SSR
|
|
@@ -1197,7 +1211,7 @@ export const meta = ({ loaderData }: { loaderData: { note?: { title: string; bod
|
|
|
1197
1211
|
})
|
|
1198
1212
|
```
|
|
1199
1213
|
|
|
1200
|
-
The third context field — `locale: string` — is the active i18n locale for this render. For `[locale]/…` routes it carries the URL-prefix locale (`'de'` on `/de/notes/42`). For bare-path routes it carries the active locale from the framework's `voltro:
|
|
1214
|
+
The third context field — `locale: string` — is the active i18n locale for this render. For `[locale]/…` routes it carries the URL-prefix locale (`'de'` on `/de/notes/42`). For bare-path routes it carries the active locale from the framework's `voltro:locale` cookie when set — so **cookie-based i18n works too** (an authed dashboard with no `[locale]` URL still gets a translated `<title>` that tracks the language switch) — otherwise the app's `defaultLocale`. Use it to localise title / description / canonical / OG per locale at SSG time so search engines see translated head tags on every variant, and to give cookie-i18n pages a translated tab title:
|
|
1201
1215
|
|
|
1202
1216
|
```tsx
|
|
1203
1217
|
import { getCatalog } from '../lib/locale'
|
|
@@ -88,6 +88,23 @@ DNS is not resolved. A public hostname that *resolves* to a private address (DNS
|
|
|
88
88
|
rebinding) still passes. That vector needs network-layer egress control; it is
|
|
89
89
|
stated here rather than silently implied.
|
|
90
90
|
|
|
91
|
+
## Auditing what your log tables actually hold
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
voltro db scan-credentials
|
|
95
|
+
voltro db scan-credentials --table my_events:actor
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Counts rows whose Subject carries a credential-shaped key — `token`, `secret`, `password`, `apikey`, `credential`, `privatekey` — in `_voltro_audit_log` and `_voltro_row_history`, plus any table you name with `--table <name>[:<column>]` (the column defaults to `subject`). Exit code `1` on a hit, so CI can gate on it.
|
|
99
|
+
|
|
100
|
+
**It is a command and not a documented query on purpose.** The same check once shipped as SQL you were asked to run yourself, in its postgres spelling (`subject::text ILIKE '%token%'`). On MySQL/MariaDB the natural translation is a bare `LIKE` — and against the `utf8mb4_bin` collation the migrator emits for a `json()` column, `LIKE` is case-**sensitive**. So `'%token%'` does not match `jiraToken`, and almost every JSON key that carries a credential is camelCase. A team ran the translated query over 141 rows, got `0`, and nearly reported themselves clean; 117 of those rows held a working credential. Every dialect now casts to its own text type before lowering, in code you do not have to translate.
|
|
101
|
+
|
|
102
|
+
**A `0` here never means two things.** Each line prints the number of rows *scanned* beside the number of hits. An empty table says so in words rather than reading as clean, a missing table reports as missing rather than as zero, and a run that examined nothing exits `2`.
|
|
103
|
+
|
|
104
|
+
Run it on every environment. A development database is not a sample of production.
|
|
105
|
+
|
|
106
|
+
If it finds something: purge the rows **and** rotate the credentials — assume anything written to a log table has been read — then move the credential off the Subject entirely with `connectionCredentials(...)`, which keeps it in the framework vault.
|
|
107
|
+
|
|
91
108
|
## Supply-chain assurance
|
|
92
109
|
|
|
93
110
|
Every release passes automated supply-chain gates in CI before a single package is published:
|