@voltro/cli 0.11.0 → 0.11.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 +50 -0
- package/THIRD-PARTY-NOTICES.md +1047 -22
- package/bin/voltro.mjs +16 -7
- package/dist/apiBuild-BGd-BnHq.js +2 -0
- package/dist/{apiBuild-BUbgGz73.js → apiBuild-D_kEJTxc.js} +10 -10
- package/dist/bin.js +2 -2
- package/dist/bootTiming-BdyP9nYw.js +38 -0
- package/dist/{commands-C1qBfIBI.js → commands-BiQsbV50.js} +2162 -2039
- package/dist/dev-DH13Ysgs.js +2 -0
- package/dist/{dev-ITlO-epO.js → dev-zJGsTTNb.js} +2202 -2189
- package/dist/index.js +1 -1
- package/dist/{inspectMetrics-BeMjgt8x.js → inspectMetrics-BuBwg1yW.js} +1169 -1137
- package/dist/{serveCommand-mpsE3vLY.js → serveCommand-BU454-ay.js} +312 -340
- package/dist/serveEntry.js +3 -3
- package/dist/{start-C_M4fGS2.js → start-ChN6PO-c.js} +338 -323
- package/dist/startEntry.js +2 -2
- package/package.json +23 -17
- package/templates/agent-docs/data.md +72 -1
- package/templates/agent-docs/deployment.md +49 -0
- package/templates/agent-docs/internationalization.md +36 -0
- package/templates/agent-docs/multi-tenancy.md +33 -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-storage/package.json +8 -8
- 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-search/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +8 -8
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- 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-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/dist/apiBuild-BCv3bekE.js +0 -2
- package/dist/dev-DKASElZP.js +0 -2
package/dist/startEntry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t } from "./start-
|
|
1
|
+
import { bt as e } from "./inspectMetrics-BuBwg1yW.js";
|
|
2
|
+
import { t } from "./start-ChN6PO-c.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.11.
|
|
3
|
+
"version": "0.11.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",
|
|
@@ -45,6 +45,11 @@
|
|
|
45
45
|
"types": "./dist/serveEntry.d.ts",
|
|
46
46
|
"import": "./dist/serveEntry.js",
|
|
47
47
|
"default": "./dist/serveEntry.js"
|
|
48
|
+
},
|
|
49
|
+
"./startEntry": {
|
|
50
|
+
"types": "./dist/startEntry.d.ts",
|
|
51
|
+
"import": "./dist/startEntry.js",
|
|
52
|
+
"default": "./dist/startEntry.js"
|
|
48
53
|
}
|
|
49
54
|
},
|
|
50
55
|
"main": "./dist/index.js",
|
|
@@ -57,22 +62,22 @@
|
|
|
57
62
|
"@effect/platform-node": "^0.107.0",
|
|
58
63
|
"@effect/sql": "^0.51.1",
|
|
59
64
|
"@effect/workflow": "^0.18.2",
|
|
60
|
-
"@voltro/ai": "0.11.
|
|
61
|
-
"@voltro/cache": "0.11.
|
|
62
|
-
"@voltro/data-transfer": "0.11.
|
|
63
|
-
"@voltro/database": "0.11.
|
|
64
|
-
"@voltro/env": "0.11.
|
|
65
|
-
"@voltro/kv": "0.11.
|
|
66
|
-
"@voltro/logger": "0.11.
|
|
67
|
-
"@voltro/plugin-auth": "0.11.
|
|
68
|
-
"@voltro/plugin-broadcast": "0.11.
|
|
69
|
-
"@voltro/plugin-mail": "0.11.
|
|
70
|
-
"@voltro/plugin-storage": "0.11.
|
|
71
|
-
"@voltro/plugin-webhooks": "0.11.
|
|
72
|
-
"@voltro/protocol": "0.11.
|
|
73
|
-
"@voltro/runtime": "0.11.
|
|
74
|
-
"@voltro/serverless": "0.11.
|
|
75
|
-
"@voltro/workflow": "0.11.
|
|
65
|
+
"@voltro/ai": "0.11.1",
|
|
66
|
+
"@voltro/cache": "0.11.1",
|
|
67
|
+
"@voltro/data-transfer": "0.11.1",
|
|
68
|
+
"@voltro/database": "0.11.1",
|
|
69
|
+
"@voltro/env": "0.11.1",
|
|
70
|
+
"@voltro/kv": "0.11.1",
|
|
71
|
+
"@voltro/logger": "0.11.1",
|
|
72
|
+
"@voltro/plugin-auth": "0.11.1",
|
|
73
|
+
"@voltro/plugin-broadcast": "0.11.1",
|
|
74
|
+
"@voltro/plugin-mail": "0.11.1",
|
|
75
|
+
"@voltro/plugin-storage": "0.11.1",
|
|
76
|
+
"@voltro/plugin-webhooks": "0.11.1",
|
|
77
|
+
"@voltro/protocol": "0.11.1",
|
|
78
|
+
"@voltro/runtime": "0.11.1",
|
|
79
|
+
"@voltro/serverless": "0.11.1",
|
|
80
|
+
"@voltro/workflow": "0.11.1",
|
|
76
81
|
"chokidar": "^5.0.0",
|
|
77
82
|
"ioredis": "^5.11.1",
|
|
78
83
|
"tinyglobby": "^0.2.17",
|
|
@@ -80,6 +85,7 @@
|
|
|
80
85
|
},
|
|
81
86
|
"optionalDependencies": {
|
|
82
87
|
"@tailwindcss/vite": "^4.3.2",
|
|
88
|
+
"@vercel/nft": "^1.10.2",
|
|
83
89
|
"@vitejs/plugin-react": "^6.0.3",
|
|
84
90
|
"esbuild": "^0.28.0",
|
|
85
91
|
"tsx": "^4.23.0",
|
|
@@ -293,6 +293,28 @@ export default async (_input: Record<string, never>, ctx) => {
|
|
|
293
293
|
|
|
294
294
|
For computed queries, `source` is the reactivity trigger. When any row in the source table changes, the runtime re-runs the executor and emits the new value if it changed. If the executor reads more than one table (a join or matrix), declare `source` as an array — the executor re-runs when **any** listed table changes (e.g. `source: ['skills', 'ratings']`).
|
|
295
295
|
|
|
296
|
+
### Type the computed return against `output` — `defineExecutor`
|
|
297
|
+
|
|
298
|
+
A computed executor's return is encoded through the descriptor's `output` schema — but the bare default export's return is **not** type-checked against it. So a handler that builds `{ publishedAt: row.publishedAt.getTime() }` where `output` is `timestampMs` (Type `Date`) compiles green and throws `Expected DateFromSelf, actual 1784…` at *encode* time, which **Dies the subscription**. For a nullable date it's a time-bomb: fine while the value is null, exploding the instant it becomes non-null (a publish, say).
|
|
299
|
+
|
|
300
|
+
Wrap the handler in `defineExecutor(descriptor, fn)` — it pins the return to `Schema.Type<output>`, turning that into a compile error at the handler:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
// apps/api/queries/notes.summary.query.server.ts
|
|
304
|
+
import { defineExecutor } from '@voltro/runtime'
|
|
305
|
+
import { notesSummary } from './notes.summary.query'
|
|
306
|
+
|
|
307
|
+
export default defineExecutor(notesSummary, async (_input, ctx) => {
|
|
308
|
+
const notes = await ctx.store.select('notes').all()
|
|
309
|
+
return {
|
|
310
|
+
open: notes.filter((note) => !note.done).length,
|
|
311
|
+
done: notes.filter((note) => note.done).length,
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
It's a runtime identity (returns the handler unchanged) — the whole value is the compile check. The Effect error and requirement channels stay inferred; only the success value is constrained. A **descriptor-return** (reactive) executor is allowed through unchecked: the store produces the rows, so a value-level return type can't express the row-vs-`output` check. `defineExecutor` works the same for `defineMutation` / `defineAction` handlers.
|
|
317
|
+
|
|
296
318
|
## Auto-optimistic source
|
|
297
319
|
|
|
298
320
|
`source` also connects query caches to mutation `target` metadata:
|
|
@@ -625,6 +647,48 @@ The descriptor is the wire contract. The `.mutation.server.ts` file is the serve
|
|
|
625
647
|
5. Commit the transaction.
|
|
626
648
|
6. Drain the batched change events so matching query subscriptions receive new snapshots or deltas.
|
|
627
649
|
|
|
650
|
+
## Missing required columns fail LOUD, at the call
|
|
651
|
+
|
|
652
|
+
`ctx.store.insert` / `upsert` / `insertIgnore` check the payload against the
|
|
653
|
+
table before the statement runs. A column that is NOT NULL, has no default, and
|
|
654
|
+
isn't auto-stamped — a business FK like `teamId`, a plain `timestamp()` like
|
|
655
|
+
`lastRefreshedAt` — must be present, or the write raises a typed
|
|
656
|
+
`TableValidationFailed` naming it:
|
|
657
|
+
|
|
658
|
+
```txt
|
|
659
|
+
TableValidationFailed: missing required column 'lastRefreshedAt'
|
|
660
|
+
— NOT NULL with no default and not auto-stamped
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
Without this the omission slips past `tsc` and boot and surfaces only as a raw
|
|
664
|
+
dialect `SqlError: Failed to execute statement` — and only on the INSERT path, so
|
|
665
|
+
it stays dormant until the first row with no existing cache entry. An
|
|
666
|
+
`upsert` / `insertIgnore` whose payload omits one of its own `conflictColumns` is
|
|
667
|
+
named the same way (an absent conflict key can't match its target). The check
|
|
668
|
+
runs after stamping, so auto-id, `tenantId`, and audit columns never trip it, and
|
|
669
|
+
it skips nullable and defaulted columns — the ones you may legitimately omit.
|
|
670
|
+
|
|
671
|
+
### Catch it at COMPILE time — `insertRow` / `upsertRow`
|
|
672
|
+
|
|
673
|
+
The runtime guard above is the backstop. To catch a missing column at compile
|
|
674
|
+
time, use `insertRow` / `upsertRow` — they take the **table object** (not a
|
|
675
|
+
string name), so the payload is typed against the table's required columns:
|
|
676
|
+
|
|
677
|
+
```ts
|
|
678
|
+
import { insertRow } from '@voltro/database'
|
|
679
|
+
|
|
680
|
+
// ✗ compile error — lastRefreshedAt is NOT NULL with no default
|
|
681
|
+
await insertRow(ctx.store, roadmapEpicStats, { teamId })
|
|
682
|
+
// ✓
|
|
683
|
+
await insertRow(ctx.store, roadmapEpicStats, { teamId, lastRefreshedAt: new Date() })
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
`InferInsertRow<T>` makes a column optional exactly when you may omit it — it's
|
|
687
|
+
nullable, has a `.default()`, or is a framework-filled `id` / `tenantId` / audit
|
|
688
|
+
column — and required otherwise. `upsertRow`'s `conflictColumns` are constrained
|
|
689
|
+
to the table's own columns, so a typo is a compile error too. The string-keyed
|
|
690
|
+
`ctx.store.insert` / `upsert` still work unchanged; the typed seam is opt-in.
|
|
691
|
+
|
|
628
692
|
## Partial updates: `ctx.store.applyDefined`
|
|
629
693
|
|
|
630
694
|
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)`:
|
|
@@ -1046,11 +1110,18 @@ cancellation email. Declare the overlap instead of relying on luck:
|
|
|
1046
1110
|
sequence()
|
|
1047
1111
|
.step('draft', () => createDraft.run(input), { undo: (d) => discardDraft.run({ id: d.id }) })
|
|
1048
1112
|
.step('jira', (c) => createTicket.run({ draftId: c.draft.id }), {
|
|
1049
|
-
undo: (t) => deleteJiraDraftTicket.run({ key: t.key }),
|
|
1113
|
+
undo: (t, ctx) => deleteJiraDraftTicket.run({ key: t.key, draftId: ctx.draft.id }),
|
|
1050
1114
|
covers: ['draft'],
|
|
1051
1115
|
})
|
|
1052
1116
|
```
|
|
1053
1117
|
|
|
1118
|
+
An `undo` receives `(result, ctx)` — its own result *and* the context up to that
|
|
1119
|
+
step, the same context `covers` and `when` see. An inverse that needs an id from
|
|
1120
|
+
an earlier step (here `deleteJiraDraftTicket` wants both the jira key and the
|
|
1121
|
+
`draftId`) reads it from `ctx` rather than the step re-returning it just so the
|
|
1122
|
+
undo can reach it. A later step's result is not in `ctx`: it is rolled back
|
|
1123
|
+
before this one, so reading it would be reading something already reversed.
|
|
1124
|
+
|
|
1054
1125
|
If the covering undo *fails*, the covered steps are neither run nor claimed —
|
|
1055
1126
|
whether the cascade got that far is unknown, and both guesses are wrong. They
|
|
1056
1127
|
come back in `compensationUncertain` so you can reconcile.
|
|
@@ -234,6 +234,32 @@ your-product.com {
|
|
|
234
234
|
|
|
235
235
|
The path is **relative** in the bundle, so the host resolves at runtime — **one built image works across every environment**; only the path is fixed. Keep the socket **same-origin** with the web app: the session cookie rides the WS upgrade, and a cross-origin socket wouldn't carry it (every subscription would run anonymous). For an api on a genuinely separate origin, use the api spec's absolute `url` on the web side plus a token-based `headers` resolver instead of a cookie. (One custom `wsPath` per origin — the framework fails the build if two same-origin apis would collide on it.)
|
|
236
236
|
|
|
237
|
+
### Split web/api deployment — the SSR api origin — `serverUrl`
|
|
238
|
+
|
|
239
|
+
When `web` and `api` deploy as **separate services** (e.g. different k8s namespaces, one ingress routing `/api*` → api), the **browser** reaches the api by its relative `wsPath` against the page origin — but a `renderMode:'ssr'` page's `ctx.query` runs in the **web pod**, which has no page origin and must reach the api by **internal DNS**. These are genuinely different endpoints, so `url` (which is *also* the browser's ws origin) can't express the internal one without shipping an unreachable host to the browser.
|
|
240
|
+
|
|
241
|
+
Set the server-only `serverUrl` (or the env override) to the api's internal origin:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
// web/app.config.ts
|
|
245
|
+
apis: {
|
|
246
|
+
app: {
|
|
247
|
+
package: '@app/api',
|
|
248
|
+
transport: { wsPath: '/api/sync' }, // browser → relative, same-origin
|
|
249
|
+
serverUrl: process.env.VOLTRO_API_ORIGIN, // web pod → internal DNS (SSR only)
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# or set it purely from ops, no app.config edit — per-api, name upper-cased:
|
|
256
|
+
VOLTRO_API_ORIGIN_APP=http://api.my-namespace.svc.cluster.local
|
|
257
|
+
# or a single shared origin for every api:
|
|
258
|
+
VOLTRO_API_ORIGIN=http://api.my-namespace.svc.cluster.local
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`serverUrl` / `VOLTRO_API_ORIGIN*` is used **only** for the SSR `POST /rpc` and is **never** emitted into the browser bundle. Resolution order: env > `serverUrl` > (under `voltro dev` only) the dev proxy target > an external api's absolute `url`. Under `voltro start`, a package api with **none** of these set makes the SSR query **fail loud** — naming the api and the config to set — instead of dialing the dev `localhost` port and surfacing a cryptic `ECONNREFUSED` in a 500.
|
|
262
|
+
|
|
237
263
|
### `allowedHosts` — fronting the web app on a real domain
|
|
238
264
|
|
|
239
265
|
Vite 8's DNS-rebind guard returns HTTP **403** for any request whose `Host`
|
|
@@ -275,6 +301,7 @@ cause.
|
|
|
275
301
|
| `AI_PROVIDER` | when using `@voltro/ai` | `anthropic` / `openai` / `mock` |
|
|
276
302
|
| `AI_API_KEY` | with `AI_PROVIDER` | Provider's API key |
|
|
277
303
|
| `SSR_CACHE` | optional | `memory` (default) or `postgres` (needs `PG_*` set too) for the ISR cache |
|
|
304
|
+
| `VOLTRO_API_ORIGIN` / `VOLTRO_API_ORIGIN_<NAME>` | split web/api deploy with SSR pages | The api's internal origin the WEB pod uses for SSR `ctx.query` (`http://api.<ns>.svc.cluster.local`). Per-api `_<NAME>` (name upper-cased) wins over the shared one and over `serverUrl`. Never sent to the browser |
|
|
278
305
|
| `PORT` | optional | App's listen port (overrides `app.config.ts.port`) |
|
|
279
306
|
| `VOLTRO_INSPECT` | optional | `off` to disable `_voltro/inspect/*` in prod |
|
|
280
307
|
| `VOLTRO_INSPECT_TOKEN` | recommended | Bearer token guard on inspect endpoints |
|
|
@@ -402,6 +429,24 @@ The phases, in boot order:
|
|
|
402
429
|
|
|
403
430
|
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
431
|
|
|
432
|
+
A **web app** (`voltro start`) reports its own phases under the same flag, on its ready line:
|
|
433
|
+
|
|
434
|
+
```text
|
|
435
|
+
boot phases (ready in 640ms) { bootMs: 640, phases: { modules: 590, config: 6, scan: 12, provider: 22, routes: 4, cdc: 0, ready: 6 } }
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
| Phase | What it covers |
|
|
439
|
+
| --- | --- |
|
|
440
|
+
| `modules` | node init + module-graph load/compile — the dominant cold-start phase, same as the api. Note the web start path has **no serve-bundle equivalent** yet, so `voltro build` does not collapse this phase for a web app the way it does for `voltro serve`; it is the framework graph evaluated unbundled on every cold process |
|
|
441
|
+
| `config` | `app.config.ts` + the typed-env gate |
|
|
442
|
+
| `scan` | reading the built shell, resolving the api origin, the ISR cache backend, and walking `src/pages` |
|
|
443
|
+
| `provider` | importing the precompiled SSR bundle (`dist/server/ssrEntry.js`). Stays small because page modules load lazily — a fat `provider` phase means the SSR bundle regressed to eager imports |
|
|
444
|
+
| `routes` | building per-route metadata. Cheap from the build-time `pageMeta` manifest; fat only when it fell back to importing every page module |
|
|
445
|
+
| `cdc` | postgres `LISTEN` wiring for ISR cache invalidation (zero without it) |
|
|
446
|
+
| `ready` | the HTTP listener flip |
|
|
447
|
+
|
|
448
|
+
**What boot timing cannot see:** `performance.now()` starts at *process* start, so everything before node runs — the container's scale-from-zero scheduling and image pull — is invisible to it. If a request that took many seconds end-to-end shows a `ready in` well under a second, the tail is infrastructure (image size, cold scheduling), not framework boot — split it with your platform's own request/container timestamps, not with more marks.
|
|
449
|
+
|
|
405
450
|
## Precompiled boot
|
|
406
451
|
|
|
407
452
|
`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.
|
|
@@ -415,6 +460,10 @@ Building an API app produces the bundle, and `voltro serve` boots from it. In **
|
|
|
415
460
|
|
|
416
461
|
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
462
|
|
|
463
|
+
**Web apps get the same treatment.** `voltro build` also precompiles the `voltro start` runtime into a **start bundle** (`.framework/dist-web/startBundle/`, framework inlined), and `voltro start` boots from it automatically — so a cold web boot loads one artefact instead of resolving the whole framework graph. This collapses the web `modules` phase the same way the serve bundle does (~17× on a small app in practice), which matters most exactly where it hurts: a scale-from-zero container on a fraction of a vCPU. It is transparent — no config, no separate command, and it travels in `.framework` with everything else the deploy already copies. If the bundle ever fails to build or load, `voltro start` falls back to the ordinary per-module boot: slower, never broken.
|
|
464
|
+
|
|
465
|
+
**And the image itself shrinks.** Because the SSR bundle, the start bundle, and the precompiled config all inline + tree-shake the framework, a booted web app needs from `node_modules` only the runtime-external **native** leaves it actually reaches (a SQL driver an ISR or config path touches) — everything else is already compiled into the bundles. The standalone web Dockerfiles trace the real reachable set (`@vercel/nft`, the same tool behind Next.js `output: standalone`) and drop the rest. It's automatic — a native driver you use is traced and kept, one you don't is dropped, no per-app allow-list — and for a static/SSR marketing site with no native runtime dependency, `node_modules` collapses to nothing: the image is just the base plus `.framework`. On a real marketing app the shipped app tree fell from ~210 MB to ~65 MB. A smaller image is a faster cold pull on a fresh node — the other half of the scale-from-zero latency the boot bundle can't touch.
|
|
466
|
+
|
|
418
467
|
## Tiers (Voltro Cloud — coming soon)
|
|
419
468
|
|
|
420
469
|
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.
|
|
@@ -115,6 +115,18 @@ export default defineLocale<typeof en>()({
|
|
|
115
115
|
|
|
116
116
|
**Don't roll your own ICU MessageFormat.** Pluralization, gender, and ordinals across 50+ languages is a solved problem; reimplementing it produces bugs that only surface in specific locales (Turkish dotted-i, Arabic plurals, Finnish cases).
|
|
117
117
|
|
|
118
|
+
**Check placeholder parity, not just key parity.** `defineLocale` enforces that every locale has the same KEYS, but not that each message has the same `{var}` set — a translation that drops or renames a `{var}` compiles and boots, then throws `The intl string context variable "date" was not provided` only in that locale, only when the message renders. `assertCatalogParity` catches it up front:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { assertCatalogParity } from '@voltro/i18n'
|
|
122
|
+
import en from './locales/en'
|
|
123
|
+
import de from './locales/de'
|
|
124
|
+
|
|
125
|
+
assertCatalogParity({ en, de }) // throws, listing every {var} drift
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Call it in a test (or at boot with `{ onMismatch: 'warn' }`). Plural argument names are compared; a plural's inner `{# item}` branches are not mistaken for placeholders.
|
|
129
|
+
|
|
118
130
|
## Reading translations in components
|
|
119
131
|
|
|
120
132
|
```tsx
|
|
@@ -144,6 +156,30 @@ const locale = useLocale()
|
|
|
144
156
|
- **`useLocale()`** — the active locale code, as React state inside the provider.
|
|
145
157
|
- **`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
158
|
|
|
159
|
+
## Compile-time keys AND params — `createTypedMessages`
|
|
160
|
+
|
|
161
|
+
`defineLocale` catches a missing KEY at compile time, but the bare `useT` / `useTFn` don't type the KEY or the PARAMS at the *call site* — so `useT('home.greeting')` (a message that needs `{name}`) compiles and then throws at render (`The intl string context variable "name" was not provided`). `createTypedMessages<typeof en>()` binds the catalog's literal message types to `useT` / `useTFn` / `<T>` so both are compile errors:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
// src/i18n.ts — call once with your base catalog, re-export the result
|
|
165
|
+
import { createTypedMessages } from '@voltro/i18n'
|
|
166
|
+
import type en from './locales/en'
|
|
167
|
+
|
|
168
|
+
export const { useT, useTFn, T } = createTypedMessages<typeof en>()
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
// now, everywhere you import from './i18n' instead of '@voltro/i18n':
|
|
173
|
+
useT('home.greeting') // ✗ compile error — expected { name: … }
|
|
174
|
+
useT('home.greeting', { name }) // ✓
|
|
175
|
+
useT('header.search') // ✓ no placeholders → no values arg
|
|
176
|
+
useT('does.not.exist') // ✗ compile error — not a catalog key
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
It's pure type refinement — the runtime is the same `useT` / `useTFn` / `<T>`, only the signatures narrow to your catalog. Requires the base catalog to be `as const` (so its message strings survive as literal types).
|
|
180
|
+
|
|
181
|
+
**Scope.** Simple `{name}` and single-argument `{count, number}` placeholders are extracted and required. Messages with *nested* inline ICU (`{n, plural, one {…} other {…}}` / `select`) accept a loose values bag instead of a wrong strict one — pluralise in JS over simple `{count}` messages (`items.map((i) => t('cart.one', { count: i.n }))`) to stay fully typed. `<T>` gets a typed key with loose values, because its rich-text `<tag>` renderers can't be modelled by placeholder extraction.
|
|
182
|
+
|
|
147
183
|
## Reading a message outside React — `meta({ locale })`
|
|
148
184
|
|
|
149
185
|
`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:
|
|
@@ -337,6 +337,39 @@ tenant. Use this for per-tenant marketing pages, public listings filtered
|
|
|
337
337
|
by tenant slug, status pages. Never expose any table that should require
|
|
338
338
|
auth this way — the header is client-controlled and unauthenticated.
|
|
339
339
|
|
|
340
|
+
## The empty-tenant sentinel — why a scoped read silently returns empty
|
|
341
|
+
|
|
342
|
+
A subject that authenticated but has **no resolved org** carries the
|
|
343
|
+
empty-string tenant `''`, not `null`. This is deliberate: `null` means a
|
|
344
|
+
`system` subject and reads *unscoped*, so an org-less user must NOT be
|
|
345
|
+
treated as one — that would stream every tenant's rows. Instead the
|
|
346
|
+
auto-merged filter becomes `eq('tenantId', '')`, which matches no real
|
|
347
|
+
row, so the read **fails closed**: it returns empty rather than leaking.
|
|
348
|
+
|
|
349
|
+
The cost is a debugging trap — an empty result with no error looks
|
|
350
|
+
identical to "no such row". So `voltro dev` warns once per tenant-scoped
|
|
351
|
+
table when it happens:
|
|
352
|
+
|
|
353
|
+
```txt
|
|
354
|
+
[warn] tenant-scoped read with an EMPTY tenant — every row is filtered out
|
|
355
|
+
table: weeklyUpdates
|
|
356
|
+
cause: the subject authenticated but has no resolved org (tenantId=''),
|
|
357
|
+
so the auto-merged eq('tenantId', '') matches no real row —
|
|
358
|
+
this is NOT 'no such row'
|
|
359
|
+
fix: resolve the subject to an active org before the read, or mark the
|
|
360
|
+
query .unscoped() if it is deliberately cross-tenant
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
If you see empty reads in development, this line tells you whether the
|
|
364
|
+
cause is the data or an unresolved org. `voltro serve` does not emit it —
|
|
365
|
+
it is a development diagnostic, not a production log.
|
|
366
|
+
|
|
367
|
+
`voltro dev` also warns ONCE, earlier, the first time an authenticated
|
|
368
|
+
subject resolves with no active org at all — "authenticated, but no active
|
|
369
|
+
org → all tenant-scoped reads will be empty" — catching the whole class at
|
|
370
|
+
the door before any specific read. Resolve the user to an active org during
|
|
371
|
+
auth, or route org-less users to an onboarding flow.
|
|
372
|
+
|
|
340
373
|
## Subdomain-based tenant resolution
|
|
341
374
|
|
|
342
375
|
For `tenant1.your-product.com` / `tenant2.your-product.com`, write a
|
|
@@ -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.11.
|
|
15
|
-
"@voltro/cli": "0.11.
|
|
16
|
-
"@voltro/database": "0.11.
|
|
17
|
-
"@voltro/env": "0.11.
|
|
18
|
-
"@voltro/protocol": "0.11.
|
|
19
|
-
"@voltro/runtime": "0.11.
|
|
14
|
+
"@voltro/ai": "0.11.1",
|
|
15
|
+
"@voltro/cli": "0.11.1",
|
|
16
|
+
"@voltro/database": "0.11.1",
|
|
17
|
+
"@voltro/env": "0.11.1",
|
|
18
|
+
"@voltro/protocol": "0.11.1",
|
|
19
|
+
"@voltro/runtime": "0.11.1",
|
|
20
20
|
"effect": "^3.21.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@voltro/testing": "0.11.
|
|
23
|
+
"@voltro/testing": "0.11.1",
|
|
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.11.
|
|
16
|
-
"@voltro/database": "0.11.
|
|
17
|
-
"@voltro/env": "0.11.
|
|
18
|
-
"@voltro/plugin-auth": "0.11.
|
|
19
|
-
"@voltro/protocol": "0.11.
|
|
20
|
-
"@voltro/runtime": "0.11.
|
|
21
|
-
"@voltro/sql-postgres": "0.11.
|
|
15
|
+
"@voltro/cli": "0.11.1",
|
|
16
|
+
"@voltro/database": "0.11.1",
|
|
17
|
+
"@voltro/env": "0.11.1",
|
|
18
|
+
"@voltro/plugin-auth": "0.11.1",
|
|
19
|
+
"@voltro/protocol": "0.11.1",
|
|
20
|
+
"@voltro/runtime": "0.11.1",
|
|
21
|
+
"@voltro/sql-postgres": "0.11.1",
|
|
22
22
|
"effect": "^3.21.2"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/testing": "0.11.
|
|
25
|
+
"@voltro/testing": "0.11.1",
|
|
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.11.
|
|
16
|
-
"@voltro/database": "0.11.
|
|
17
|
-
"@voltro/env": "0.11.
|
|
18
|
-
"@voltro/plugin-multitenancy": "0.11.
|
|
19
|
-
"@voltro/protocol": "0.11.
|
|
20
|
-
"@voltro/runtime": "0.11.
|
|
15
|
+
"@voltro/cli": "0.11.1",
|
|
16
|
+
"@voltro/database": "0.11.1",
|
|
17
|
+
"@voltro/env": "0.11.1",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.11.1",
|
|
19
|
+
"@voltro/protocol": "0.11.1",
|
|
20
|
+
"@voltro/runtime": "0.11.1",
|
|
21
21
|
"effect": "^3.21.2"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.11.
|
|
24
|
+
"@voltro/testing": "0.11.1",
|
|
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.11.
|
|
16
|
-
"@voltro/database": "0.11.
|
|
17
|
-
"@voltro/env": "0.11.
|
|
18
|
-
"@voltro/plugin-deactivation": "0.11.
|
|
19
|
-
"@voltro/protocol": "0.11.
|
|
20
|
-
"@voltro/runtime": "0.11.
|
|
15
|
+
"@voltro/cli": "0.11.1",
|
|
16
|
+
"@voltro/database": "0.11.1",
|
|
17
|
+
"@voltro/env": "0.11.1",
|
|
18
|
+
"@voltro/plugin-deactivation": "0.11.1",
|
|
19
|
+
"@voltro/protocol": "0.11.1",
|
|
20
|
+
"@voltro/runtime": "0.11.1",
|
|
21
21
|
"effect": "^3.21.2"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.11.
|
|
24
|
+
"@voltro/testing": "0.11.1",
|
|
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.11.
|
|
16
|
-
"@voltro/database": "0.11.
|
|
17
|
-
"@voltro/env": "0.11.
|
|
18
|
-
"@voltro/plugin-mail": "0.11.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.11.
|
|
20
|
-
"@voltro/protocol": "0.11.
|
|
21
|
-
"@voltro/runtime": "0.11.
|
|
15
|
+
"@voltro/cli": "0.11.1",
|
|
16
|
+
"@voltro/database": "0.11.1",
|
|
17
|
+
"@voltro/env": "0.11.1",
|
|
18
|
+
"@voltro/plugin-mail": "0.11.1",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.11.1",
|
|
20
|
+
"@voltro/protocol": "0.11.1",
|
|
21
|
+
"@voltro/runtime": "0.11.1",
|
|
22
22
|
"effect": "^3.21.2",
|
|
23
23
|
"react": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.11.
|
|
26
|
+
"@voltro/testing": "0.11.1",
|
|
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.11.
|
|
16
|
-
"@voltro/database": "0.11.
|
|
17
|
-
"@voltro/env": "0.11.
|
|
18
|
-
"@voltro/plugin-multitenancy": "0.11.
|
|
19
|
-
"@voltro/plugin-storage": "0.11.
|
|
20
|
-
"@voltro/protocol": "0.11.
|
|
21
|
-
"@voltro/runtime": "0.11.
|
|
22
|
-
"@voltro/sql-mysql": "0.11.
|
|
15
|
+
"@voltro/cli": "0.11.1",
|
|
16
|
+
"@voltro/database": "0.11.1",
|
|
17
|
+
"@voltro/env": "0.11.1",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.11.1",
|
|
19
|
+
"@voltro/plugin-storage": "0.11.1",
|
|
20
|
+
"@voltro/protocol": "0.11.1",
|
|
21
|
+
"@voltro/runtime": "0.11.1",
|
|
22
|
+
"@voltro/sql-mysql": "0.11.1",
|
|
23
23
|
"effect": "^3.21.2"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.11.
|
|
26
|
+
"@voltro/testing": "0.11.1",
|
|
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.11.
|
|
14
|
-
"@voltro/database": "0.11.
|
|
15
|
-
"@voltro/env": "0.11.
|
|
16
|
-
"@voltro/plugin-multitenancy": "0.11.
|
|
17
|
-
"@voltro/plugin-storage": "0.11.
|
|
18
|
-
"@voltro/protocol": "0.11.
|
|
19
|
-
"@voltro/runtime": "0.11.
|
|
13
|
+
"@voltro/cli": "0.11.1",
|
|
14
|
+
"@voltro/database": "0.11.1",
|
|
15
|
+
"@voltro/env": "0.11.1",
|
|
16
|
+
"@voltro/plugin-multitenancy": "0.11.1",
|
|
17
|
+
"@voltro/plugin-storage": "0.11.1",
|
|
18
|
+
"@voltro/protocol": "0.11.1",
|
|
19
|
+
"@voltro/runtime": "0.11.1",
|
|
20
20
|
"effect": "^3.21.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@voltro/testing": "0.11.
|
|
23
|
+
"@voltro/testing": "0.11.1",
|
|
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.11.
|
|
15
|
-
"@voltro/database": "0.11.
|
|
16
|
-
"@voltro/env": "0.11.
|
|
17
|
-
"@voltro/plugin-governance": "0.11.
|
|
18
|
-
"@voltro/plugin-multitenancy": "0.11.
|
|
19
|
-
"@voltro/protocol": "0.11.
|
|
20
|
-
"@voltro/runtime": "0.11.
|
|
14
|
+
"@voltro/cli": "0.11.1",
|
|
15
|
+
"@voltro/database": "0.11.1",
|
|
16
|
+
"@voltro/env": "0.11.1",
|
|
17
|
+
"@voltro/plugin-governance": "0.11.1",
|
|
18
|
+
"@voltro/plugin-multitenancy": "0.11.1",
|
|
19
|
+
"@voltro/protocol": "0.11.1",
|
|
20
|
+
"@voltro/runtime": "0.11.1",
|
|
21
21
|
"effect": "^3.21.2"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.11.
|
|
24
|
+
"@voltro/testing": "0.11.1",
|
|
25
25
|
"typescript": "^5.7.0",
|
|
26
26
|
"vitest": "^3.0.0"
|
|
27
27
|
}
|
|
@@ -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.11.
|
|
15
|
-
"@voltro/database": "0.11.
|
|
16
|
-
"@voltro/env": "0.11.
|
|
17
|
-
"@voltro/plugin-multitenancy": "0.11.
|
|
18
|
-
"@voltro/protocol": "0.11.
|
|
19
|
-
"@voltro/runtime": "0.11.
|
|
20
|
-
"@voltro/workflow": "0.11.
|
|
14
|
+
"@voltro/cli": "0.11.1",
|
|
15
|
+
"@voltro/database": "0.11.1",
|
|
16
|
+
"@voltro/env": "0.11.1",
|
|
17
|
+
"@voltro/plugin-multitenancy": "0.11.1",
|
|
18
|
+
"@voltro/protocol": "0.11.1",
|
|
19
|
+
"@voltro/runtime": "0.11.1",
|
|
20
|
+
"@voltro/workflow": "0.11.1",
|
|
21
21
|
"effect": "^3.21.2"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.11.
|
|
24
|
+
"@voltro/testing": "0.11.1",
|
|
25
25
|
"typescript": "^5.7.0",
|
|
26
26
|
"vitest": "^3.0.0"
|
|
27
27
|
}
|
|
@@ -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.11.
|
|
16
|
-
"@voltro/database": "0.11.
|
|
17
|
-
"@voltro/env": "0.11.
|
|
18
|
-
"@voltro/plugin-flags": "0.11.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.11.
|
|
20
|
-
"@voltro/protocol": "0.11.
|
|
21
|
-
"@voltro/runtime": "0.11.
|
|
22
|
-
"@voltro/sql-postgres": "0.11.
|
|
15
|
+
"@voltro/cli": "0.11.1",
|
|
16
|
+
"@voltro/database": "0.11.1",
|
|
17
|
+
"@voltro/env": "0.11.1",
|
|
18
|
+
"@voltro/plugin-flags": "0.11.1",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.11.1",
|
|
20
|
+
"@voltro/protocol": "0.11.1",
|
|
21
|
+
"@voltro/runtime": "0.11.1",
|
|
22
|
+
"@voltro/sql-postgres": "0.11.1",
|
|
23
23
|
"effect": "^3.21.2"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.11.
|
|
26
|
+
"@voltro/testing": "0.11.1",
|
|
27
27
|
"typescript": "^5.7.0",
|
|
28
28
|
"vitest": "^3.0.0"
|
|
29
29
|
}
|