@voltro/cli 0.51.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +127 -0
- package/dist/{agentsMd-D6yD7IQv.js → agentsMd-0l980yhL.js} +4 -1
- package/dist/agentsMd-SDDSkyl4.js +2 -0
- package/dist/apiBuild-BYBpL7Pz.js +2 -0
- package/dist/{apiBuild-CPDHXF72.js → apiBuild-CSFI8QGq.js} +3 -3
- package/dist/bin.js +1 -1
- package/dist/build-CPgcMQug.js +793 -0
- package/dist/checkCommand-2SbqzukH.js +2 -0
- package/dist/{checkCommand-DNuPiWMc.js → checkCommand-COmqc2cB.js} +92 -46
- package/dist/{cloudCmd-F4YJeqM3.js → cloudCmd-QUXh-b5w.js} +1 -1
- package/dist/codegen-CctkDO-1.js +2 -0
- package/dist/{codegen-CrMXs4hb.js → codegen-VF479Cnb.js} +1 -1
- package/dist/{codegenCommand-BNBHcNNj.js → codegenCommand-DCdG2JN-.js} +12 -12
- package/dist/{codemodRunner-BDVixlSw.js → codemodRunner-r7J9lIa7.js} +588 -540
- package/dist/{commands-B1OiS9bX.js → commands-Cc_nV8WI.js} +35 -35
- package/dist/{dashboardCommand-C-vvPY1B.js → dashboardCommand-C-gKvwqh.js} +5 -5
- package/dist/{dataCommand-C1GxXW5q.js → dataCommand-BgpBHnlB.js} +27 -27
- package/dist/dbCommand-DNb6yeOG.js +2 -0
- package/dist/{dbCommand-If4Y1xQ-.js → dbCommand-sHedr-NJ.js} +2 -2
- package/dist/dev--A3nsxA3.js +3 -0
- package/dist/{dev-kdAg9Q7l.js → dev-CRHoCEiy.js} +2142 -2103
- package/dist/doctorCommand-CqoWA2p5.js +2 -0
- package/dist/{doctorCommand-nKmeW78u.js → doctorCommand-DtfJ3FA6.js} +314 -234
- package/dist/{dormancyCommand-CY3wa_SW.js → dormancyCommand-Drn7o0No.js} +1 -1
- package/dist/{embeddingsCommand-BDLIgje_.js → embeddingsCommand-Z-jO1fWN.js} +1 -1
- package/dist/{envCommand-C6V_xVlT.js → envCommand-D4gCrrTZ.js} +8 -8
- package/dist/{evolveCommand-D3c4DSfN.js → evolveCommand-CMROeKeA.js} +2 -2
- package/dist/fileConventions-DOqD3lPS.js +34 -0
- package/dist/{fileTaxonomy-CJfgOllU.js → fileTaxonomy-DvDUV9wq.js} +1 -1
- package/dist/frameworkTableAssembly-C6ETawPR.js +2 -0
- package/dist/{frameworkTableAssembly-BwJVEKLr.js → frameworkTableAssembly-w-XnLa3q.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/{infoCommand-BnRFEF1o.js → infoCommand-DXM868o_.js} +1 -1
- package/dist/inspectMetrics-CGF94puw.js +143 -0
- package/dist/{metaCommands-CfRLra0s.js → metaCommands-C6RFmF1r.js} +2 -2
- package/dist/{migrate-DehuBakM.js → migrate-D0F-eTlK.js} +2 -2
- package/dist/{pageConvention-cEiRxdab.js → pageConvention-CzUiSbtU.js} +1 -1
- package/dist/{privacyCommand-XejDMvmu.js → privacyCommand-DGdopOI6.js} +1 -1
- package/dist/{projectScaffold-mIX_DpSe.js → projectScaffold-B4dmTlwT.js} +1 -1
- package/dist/{projectScaffold-BIl97_E6.js → projectScaffold-EzlErR4E.js} +1 -1
- package/dist/{renderModeScan-D7J1B7Kw.js → renderModeScan-CcH2X1_D.js} +25 -10
- package/dist/{renderProfile-1OWWAAtx.js → renderProfile-Ck32Fzxr.js} +2 -2
- package/dist/{runtimeTrace-ZsBU7Tkx.js → runtimeTrace-BPQyCmC5.js} +1 -1
- package/dist/{sdkgen-O4XqWOjM.js → sdkgen-Se88ifTd.js} +1 -1
- package/dist/serveCommand-DLc-BznW.js +2 -0
- package/dist/{serveCommand-DdiYNBBu.js → serveCommand-DkP3OT0W.js} +885 -868
- package/dist/serveEntry.js +1 -1
- package/dist/start-DfL3fOiN.js +3 -0
- package/dist/start-jw89Xbqy.js +1339 -0
- package/dist/startEntry.js +1 -1
- package/dist/{staticCommand-Dr2M6tpU.js → staticCommand-BwNEDlSU.js} +1 -1
- package/dist/{test-rXFq4S76.js → test-f3amja6a.js} +1 -1
- package/dist/updateCommand-5gFVfK5q.js +2 -0
- package/dist/{updateCommand-Bs322Q78.js → updateCommand-BMk2e4ky.js} +1 -1
- package/dist/{webDev-B-ubQEMX.js → webDev-BgWL9gKV.js} +1156 -835
- package/dist/webDev-CZbTsDcH.js +2 -0
- package/dist/{webhooksCommand-FLYY9IXh.js → webhooksCommand-CoIO3jbj.js} +1 -1
- package/package.json +30 -17
- package/templates/AGENTS.core.md +11 -0
- package/templates/AGENTS.md +15 -4
- package/templates/agent-docs/_index.md +4 -4
- package/templates/agent-docs/_manifest.json +11 -11
- package/templates/agent-docs/cli.md +96 -14
- package/templates/agent-docs/data.md +210 -7
- package/templates/agent-docs/database/schema.md +1 -1
- package/templates/agent-docs/database/seedsdialects.md +1 -1
- package/templates/agent-docs/deployment.md +22 -3
- package/templates/agent-docs/introduction.md +46 -0
- package/templates/agent-docs/local-first-mobile.md +34 -7
- package/templates/agent-docs/plugins/ai-flows.md +1 -1
- package/templates/agent-docs/plugins/audit.md +5 -5
- package/templates/agent-docs/plugins/cdc-out.md +2 -2
- package/templates/agent-docs/plugins/{versioning.md → row-history.md} +22 -22
- package/templates/agent-docs/plugins/storage.md +2 -2
- package/templates/agent-docs/plugins.md +29 -7
- package/templates/agent-docs/reference.md +39 -2
- package/templates/agent-docs/routing.md +341 -47
- package/templates/agent-docs/schema-driven-ui.md +78 -2
- package/templates/agent-docs/security.md +125 -8
- package/templates/agent-docs/templates/apibackends.md +14 -14
- package/templates/agent-docs/templates/overview.md +1 -1
- package/templates/agent-docs/whats-new.md +76 -53
- package/templates/apps/api-ai/package.json +6 -7
- package/templates/apps/api-ai/tests/summarize.test.ts +1 -1
- 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 +9 -10
- package/templates/apps/api-collab/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-versioning → api-row-history}/README.md +3 -3
- package/templates/apps/{api-versioning → api-row-history}/actions/documents.asOf.action.server.ts +1 -1
- package/templates/apps/{api-versioning → api-row-history}/actions/documents.history.action.server.ts +1 -1
- package/templates/apps/{api-versioning → api-row-history}/app.config.ts +3 -3
- package/templates/apps/{api-versioning → api-row-history}/database/schema.ts +1 -1
- package/templates/apps/{api-versioning → api-row-history}/mutations/documents.create.mutation.server.ts +1 -1
- package/templates/apps/{api-versioning → api-row-history}/mutations/documents.create.mutation.ts +1 -1
- package/templates/apps/{api-versioning → api-row-history}/mutations/documents.update.mutation.ts +1 -1
- package/templates/apps/{api-versioning → api-row-history}/package.json +8 -8
- package/templates/apps/api-row-history/template.json +6 -0
- package/templates/apps/{api-versioning → api-row-history}/tests/documents.create.test.ts +1 -1
- package/templates/apps/api-saas/app.config.ts +1 -0
- package/templates/apps/api-saas/package.json +10 -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-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/package.json +8 -8
- package/templates/apps/frontend-app/package.json +9 -9
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-collab/package.json +10 -10
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +10 -9
- package/templates/apps/frontend-collab/src/pages/page.tsx +42 -65
- 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 +6 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +6 -7
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-saas/package.json +8 -8
- 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/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +4 -4
- package/dist/agentsMd-Bu_XQgVf.js +0 -2
- package/dist/apiBuild-GDKuGOMV.js +0 -2
- package/dist/build-DETLZAFt.js +0 -752
- package/dist/checkCommand-CWcnDArJ.js +0 -2
- package/dist/codegen-DiMn2KkZ.js +0 -2
- package/dist/dbCommand-C27HIsGE.js +0 -2
- package/dist/dev-CK522MV5.js +0 -3
- package/dist/doctorCommand-BK4l18eG.js +0 -2
- package/dist/fileConventions-Cof68_BL.js +0 -33
- package/dist/frameworkTableAssembly-CVDB2hCq.js +0 -2
- package/dist/inspectMetrics-CfdKLh6t.js +0 -72
- package/dist/serveCommand-BRnPCxVd.js +0 -2
- package/dist/start-BLNmWkLa.js +0 -1154
- package/dist/start-Dzicuyw8.js +0 -3
- package/dist/updateCommand-eXB35SEv.js +0 -2
- package/dist/webDev-DposiF3j.js +0 -2
- package/templates/apps/api-versioning/template.json +0 -6
- /package/templates/apps/{api-versioning → api-row-history}/actions/documents.asOf.action.ts +0 -0
- /package/templates/apps/{api-versioning → api-row-history}/actions/documents.history.action.ts +0 -0
- /package/templates/apps/{api-versioning → api-row-history}/mutations/documents.update.mutation.server.ts +0 -0
- /package/templates/apps/{api-versioning → api-row-history}/tsconfig.json +0 -0
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
# Row
|
|
1
|
+
# Row history
|
|
2
2
|
|
|
3
|
-
> Full row history + time-travel. audit() records who/when;
|
|
3
|
+
> Full row history + time-travel. audit() records who/when; row-history records what-changed-to-what — a value snapshot of every row on every write, with as-of queries.
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
<!-- source: en/plugins/
|
|
10
|
-
## Row
|
|
9
|
+
<!-- source: en/plugins/row-history.md -->
|
|
10
|
+
## Row history
|
|
11
11
|
|
|
12
|
-
_Full row history + time-travel. audit() records who/when;
|
|
12
|
+
_Full row history + time-travel. audit() records who/when; row-history records what-changed-to-what — a value snapshot of every row on every write, with as-of queries._
|
|
13
13
|
|
|
14
|
-
`@voltro/plugin-
|
|
14
|
+
`@voltro/plugin-row-history` keeps a complete value history of selected tables. Where [`audit()`](/docs/plugins/audit) records *who* changed a row and *when*, row-history records *what* — a full snapshot of the row on every insert / update / delete — and lets you read any row **as of** a past instant. It rides the framework's post-commit ChangeEvent tap, so it captures every write that goes through the store with no per-handler wiring.
|
|
15
15
|
|
|
16
16
|
## Wiring
|
|
17
17
|
|
|
18
18
|
```ts
|
|
19
19
|
// app.config.ts
|
|
20
|
-
import {
|
|
20
|
+
import { rowHistoryPlugin } from '@voltro/plugin-row-history'
|
|
21
21
|
|
|
22
22
|
export default {
|
|
23
23
|
type: 'api' as const, name: 'api',
|
|
24
|
-
plugins: [
|
|
24
|
+
plugins: [rowHistoryPlugin({})],
|
|
25
25
|
}
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -30,14 +30,14 @@ Every committed change to a listed table appends a row to `_voltro_row_history`
|
|
|
30
30
|
The history row's own `id` is **derived** from `(tableName, rowId, version)` and has a fixed width — it is a surrogate, and every part of it is already a column beside it, so do not parse or construct it. That width is the point: an `id()` column is `VARCHAR(64)` on mysql/mariadb and `NVARCHAR(64)` on mssql, so a key built by concatenating those parts grew with your **table name** and stopped fitting past 22 characters — which failed every write to that table, not merely an import.
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
### What gets
|
|
33
|
+
### What gets recorded — opt OUT, not in
|
|
34
34
|
|
|
35
|
-
`
|
|
35
|
+
`rowHistoryPlugin({})` covers **every table your app declares**. There is no list to write and none to maintain.
|
|
36
36
|
|
|
37
37
|
```ts
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
rowHistoryPlugin({}) // every app table
|
|
39
|
+
rowHistoryPlugin({ exclude: [domainEvents] }) // opt one out
|
|
40
|
+
rowHistoryPlugin({ include: [aiFlowsTable] }) // add a PLUGIN's table
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
Both take table **values**, not names — a misspelling is a compile error at the call site, exactly as with `reference(() => table)`.
|
|
@@ -51,18 +51,18 @@ A table named in both `include` and `exclude` throws at construction — only yo
|
|
|
51
51
|
**Check the boot line once after upgrading.** It prints the RESOLVED count, not the configured one:
|
|
52
52
|
|
|
53
53
|
```txt
|
|
54
|
-
|
|
54
|
+
row-history active · tables: 41 · historyTable: _voltro_row_history · retentionDays: 365
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
If 41 surprises you, `exclude` is the knob. The retention sweep (`VOLTRO_ROW_HISTORY_TTL_HOURS`) still bounds age.
|
|
58
58
|
|
|
59
59
|
## What this is NOT — the grain
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
Row history records **row changes, not domain events**. One entry per row per write, named by *table*. If your product has a user-facing audit feature whose entries are named after an aggregate root — one `Team` event for a call that writes `teams` + `roles` + `userTeams` + `userTeamRoles` — this is the layer **underneath** that, not a replacement for it.
|
|
62
62
|
|
|
63
63
|
The distinction is worth reading before you plan a migration onto it. A migration off hundreds of hand-written audit calls onto this tap runs into the same wall a few hours in: the grain is different. A table-keyed tap does not produce an aggregate-keyed trail with better coverage, it produces a *different artifact*. The two compose:
|
|
64
64
|
|
|
65
|
-
- **
|
|
65
|
+
- **row history** answers "what did row R look like before, and after" — for every write, whether or not anyone remembered to record it;
|
|
66
66
|
- an **aggregate trail** (the [audit sink](/docs/plugins/audit), one row per mutation invocation) answers "what business operation happened, to which entity, and did it succeed";
|
|
67
67
|
- `traceId` joins them, so one request reads as one story.
|
|
68
68
|
|
|
@@ -95,10 +95,10 @@ migration — with the same meaning as an absent `traceId`.
|
|
|
95
95
|
|
|
96
96
|
## The correlation bridge — joining *what changed* to *who called*
|
|
97
97
|
|
|
98
|
-
`ChangeEvent` carries the calling `traceId` and `subjectId`, so a history row can be joined to the [audit sink](/docs/plugins/audit) row for the **same call**. Before this, both trails existed and shipped and nothing connected them:
|
|
98
|
+
`ChangeEvent` carries the calling `traceId` and `subjectId`, so a history row can be joined to the [audit sink](/docs/plugins/audit) row for the **same call**. Before this, both trails existed and shipped and nothing connected them: row-history knew what changed, the audit sink knew who called and whether they were refused, and no key spanned the two.
|
|
99
99
|
|
|
100
100
|
```ts
|
|
101
|
-
import { historyByTrace, historyBySubject } from '@voltro/plugin-
|
|
101
|
+
import { historyByTrace, historyBySubject } from '@voltro/plugin-row-history'
|
|
102
102
|
|
|
103
103
|
// What did this call change? (`byTrace`)
|
|
104
104
|
const touched = await historyByTrace(ctx.store, traceId, ctx.request.subject.tenantId)
|
|
@@ -118,7 +118,7 @@ Both questions were previously unanswerable at any speed — `byRow` is the only
|
|
|
118
118
|
## `timing` — when the history row is written
|
|
119
119
|
|
|
120
120
|
```ts
|
|
121
|
-
|
|
121
|
+
rowHistoryPlugin({ timing: 'in-transaction' })
|
|
122
122
|
```
|
|
123
123
|
|
|
124
124
|
| | `'post-commit'` (default) | `'in-transaction'` |
|
|
@@ -175,7 +175,7 @@ That is the correct order, not a race to engineer around: the change is durable,
|
|
|
175
175
|
{ "id": "sess_1", "secret": "enc:v1:a56iziEV9THLhzmJ:Vk0ux+0bECleTLBJkCa0Rg==:3AtMwP" }
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
-
So
|
|
178
|
+
So keeping history for a table with encrypted columns **does not widen exposure** — the history is exactly as readable as the row it came from. This is worth stating because "full row snapshot" reads alarming next to `.encrypted()`, and the cautious reader excludes the table. One did, and only found out by measuring.
|
|
179
179
|
|
|
180
180
|
**`.serverOnly()` columns ARE withheld**, and for a sharper reason than "a second copy": `crud.*` strips those columns from every row it returns, and a snapshot would smuggle the value back past that stripping inside a `json()` blob, where no column-level rule applies. A marker meaning *never serialize this to a client* cannot survive being re-exported through a different column's contents.
|
|
181
181
|
|
|
@@ -189,7 +189,7 @@ The withheld names are listed under `data._omitted`, so a reader can tell *"this
|
|
|
189
189
|
## Querying the timeline
|
|
190
190
|
|
|
191
191
|
```ts
|
|
192
|
-
import { rowHistory, rowAsOf } from '@voltro/plugin-
|
|
192
|
+
import { rowHistory, rowAsOf } from '@voltro/plugin-row-history'
|
|
193
193
|
|
|
194
194
|
// Every version of a row, oldest → newest — TENANT-SCOPED to the caller:
|
|
195
195
|
const history = await rowHistory(ctx.store, 'posts', postId, ctx.request.subject.tenantId)
|
|
@@ -204,7 +204,7 @@ Pass the caller's `tenantId` — reads are **tenant-scoped**: a row's value time
|
|
|
204
204
|
## Restore & diff
|
|
205
205
|
|
|
206
206
|
```ts
|
|
207
|
-
import { restoreAsOf, diffVersions } from '@voltro/plugin-
|
|
207
|
+
import { restoreAsOf, diffVersions } from '@voltro/plugin-row-history'
|
|
208
208
|
|
|
209
209
|
// Roll the LIVE row back to its state at a past instant (tenant-scoped like
|
|
210
210
|
// rowAsOf — no visible state then ⇒ null, nothing written). The restore goes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Storage
|
|
2
2
|
|
|
3
|
-
> File storage behind one StorageService — public objects served direct from the bucket/CDN, private objects gated by an access policy + per-object grants. S3 / R2
|
|
3
|
+
> File storage behind one StorageService — public objects served direct from the bucket/CDN, private objects gated by an access policy + per-object grants. S3 / MinIO (R2 and GCS via their S3 interop) / Azure / database / filesystem / memory providers, presigned URLs, a dashboard browser.
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<!-- source: en/plugins/storage.md -->
|
|
10
10
|
## Storage
|
|
11
11
|
|
|
12
|
-
_File storage behind one StorageService — public objects served direct from the bucket/CDN, private objects gated by an access policy + per-object grants. S3 / R2
|
|
12
|
+
_File storage behind one StorageService — public objects served direct from the bucket/CDN, private objects gated by an access policy + per-object grants. S3 / MinIO (R2 and GCS via their S3 interop) / Azure / database / filesystem / memory providers, presigned URLs, a dashboard browser._
|
|
13
13
|
|
|
14
14
|
`@voltro/plugin-storage` is file storage behind one `StorageService`. Wire a
|
|
15
15
|
provider in `app.config.ts`; consume it in handlers and actions via
|
|
@@ -27,7 +27,7 @@ The framework ships some plugins; you write your own; the contract is small enou
|
|
|
27
27
|
- [plugin-billing](/docs/plugins/billing) — subscriptions, plans, entitlements + usage metering (Stripe + mock provider); seat-based billing on Stripe's own proration, retries, tax and checkout; money as integer minor units
|
|
28
28
|
- [plugin-licensing](/docs/plugins/licensing) — offline-verified EdDSA license keys + cloud-issued entitlement snapshots that feed plugin-billing; plan entitlements + pricing decided server-side, never baked into a published version
|
|
29
29
|
- [plugin-mail](/docs/plugins/mail) — transactional email (Resend / Postmark / SendGrid / SES / Mailgun / SMTP, templates, suppression, scheduling, batch, idempotency)
|
|
30
|
-
- [plugin-storage](/docs/plugins/storage) — file storage: public (CDN-direct) + private (access policy + per-object grants), S3 / R2
|
|
30
|
+
- [plugin-storage](/docs/plugins/storage) — file storage: public (CDN-direct) + private (access policy + per-object grants), S3 / MinIO (R2 and GCS via their S3 interop) / Azure / database / filesystem
|
|
31
31
|
- [plugin-ai-flows](/docs/plugins/ai-flows) — durable multi-step AI pipelines (deterministic + agentic) with human-in-the-loop, chaining, and cadence; code-first `defineFlow` or data-driven rows
|
|
32
32
|
- [plugin-postgis](/docs/plugins/postgis) — postgres-native `geography` / `geometry` columns + spatial operators
|
|
33
33
|
- [plugin-broadcast](/docs/plugins/broadcast) — cross-replica reactivity over a pub/sub bus (Redis / NATS) for non-postgres dialects
|
|
@@ -42,10 +42,10 @@ The framework ships some plugins; you write your own; the contract is small enou
|
|
|
42
42
|
- [plugin-logship](/docs/plugins/logship) — ship structured logs to Better Stack / Axiom / Loki / any HTTP sink; batched, redacted, fail-soft
|
|
43
43
|
- [plugin-moderation](/docs/plugins/moderation) — moderate user content before commit: keyword or AI provider, block / flag via interceptor + in-handler redact
|
|
44
44
|
- [plugin-search](/docs/plugins/search) — keep an external index (Typesense / Meilisearch / Algolia) in sync via the ChangeEvent tap; tenant-scoped `search.query` + hook
|
|
45
|
-
- [plugin-cdc-out](/docs/plugins/cdc-out) — declarative reverse-ETL: mirror table changes outward to a webhook
|
|
45
|
+
- [plugin-cdc-out](/docs/plugins/cdc-out) — declarative reverse-ETL: mirror table changes outward to a webhook sink (or any custom `CdcSink`) through a durable outbox; ordered per pipe, at-least-once from enqueue, dead-lettered
|
|
46
46
|
- [plugin-governance](/docs/plugins/governance) — data governance: retention TTL sweep, GDPR export + erasure, consent ledger, field encryption
|
|
47
47
|
- [plugin-openapi](/docs/plugins/openapi) — OpenAPI 3.1 spec + Swagger-UI docs generated from your `defineRestRoute` descriptors and (opt-in) rpc procedures
|
|
48
|
-
- [plugin-
|
|
48
|
+
- [plugin-row-history](/docs/plugins/row-history) — full row history + time-travel (`rowHistory` / `rowAsOf` / `restoreAsOf` / `diffVersions`); what-changed-to-what on every write
|
|
49
49
|
- [plugin-presence](/docs/plugins/presence) — ephemeral realtime presence: heartbeat roster per channel + `usePresence` / `useTyping` hooks, held in memory; cross-instance with [plugin-broadcast](/docs/plugins/broadcast)
|
|
50
50
|
- [plugin-auth-social](/docs/plugins/auth-social) — first-party Sign in with Google / GitHub / Apple: mandatory PKCE + state, JWKS-verified ID tokens, a deliberate account-linking policy, sessions issued through plugin-auth
|
|
51
51
|
- [plugin-scim](/docs/plugins/scim) — SCIM 2.0 provisioning (Users + Groups at `/scim/v2`) so an enterprise IdP can create/deactivate users
|
|
@@ -70,7 +70,7 @@ Status legend: ✓ shipped · ◐ partial · — planned.
|
|
|
70
70
|
| `@voltro/plugin-licensing` | ✓ | Offline-verified EdDSA license keys + cloud-issued entitlement snapshots that feed plugin-billing; plan entitlements + pricing decided server-side, never baked into a published version. [→ details](/docs/plugins/licensing) |
|
|
71
71
|
| `@voltro/plugin-ai-flows` | ✓ | Durable multi-step AI pipelines — deterministic or agentic, with human-in-the-loop, chaining and cadence; author flows in code (`defineFlow`) or as data (visual-editor rows), one engine runs both. [→ details](/docs/plugins/ai-flows) |
|
|
72
72
|
| `@voltro/plugin-mail` | ✓ | Transactional email — Resend / Postmark / SendGrid / SES / Mailgun / SMTP, *.email.tsx templates, per-tenant suppression, send-time scheduling, bulk/batch send, per-send idempotency, durable via workflows |
|
|
73
|
-
| `@voltro/plugin-storage` | ✓ | File storage — public (CDN-direct) + private (access policy + per-object grants), S3 / R2
|
|
73
|
+
| `@voltro/plugin-storage` | ✓ | File storage — public (CDN-direct) + private (access policy + per-object grants), S3 / MinIO (R2 and GCS via their S3 interop) / Azure / database / filesystem providers, presigned URLs, `listRefs` browse/search, HTTP Range (206) serving, dashboard browser |
|
|
74
74
|
| `@voltro/plugin-postgis` | ✓ | Postgres-native `geography` / `geometry` columns + spatial predicates (`ST_DWithin`, `ST_Contains`, `ST_Intersects`); GiST indexes via `.expressionIndex(..., { kind: 'gist' })`. No `ST_Distance` projection yet. Postgres-only by design (fails loud elsewhere). [→ details](/docs/plugins/postgis) |
|
|
75
75
|
| `@voltro/plugin-broadcast` | ✓ | Cross-replica reactivity — fans out app-mutation change events to every replica over a pub/sub bus (Redis / NATS). Closes the single-instance gap for every non-postgres dialect. [→ details](/docs/plugins/broadcast) |
|
|
76
76
|
| `@voltro/plugin-webhooks` | ✓ | Incoming + outgoing webhooks — `defineIncomingWebhook` (signature verify + idempotency, Stripe/GitHub/Slack presets) and `defineEvent` (durable delivery workflow, HMAC signing, retries, filters). [→ details](/docs/plugins/webhooks) |
|
|
@@ -91,10 +91,10 @@ Status legend: ✓ shipped · ◐ partial · — planned.
|
|
|
91
91
|
| `@voltro/plugin-logship` | ✓ | Ship structured logs to Better Stack / Axiom / Loki / any HTTP sink — rides the log-sink hook, batched + redacted + fail-soft, trace-correlated. [→ details](/docs/plugins/logship) |
|
|
92
92
|
| `@voltro/plugin-moderation` | ✓ | Content moderation — keyword denylist or AI provider (fails open), block (typed `ContentRejected`) / flag via rpc interceptor + in-handler `moderate()` redact helper. [→ details](/docs/plugins/moderation) |
|
|
93
93
|
| `@voltro/plugin-search` | ✓ | External search index sync — rides the ChangeEvent tap to mirror tables into Typesense / Meilisearch / Algolia (memory default), tenant-scoped `search.query` action (facets · highlighting · fuzziness · range/negation filters · engine-param passthrough) + `useSearch` hook + `backfillIndex` + durable cross-replica sync stats. [→ details](/docs/plugins/search) |
|
|
94
|
-
| `@voltro/plugin-cdc-out` | ◐ | Declarative reverse-ETL — mirror table changes outward to external sinks (webhook
|
|
94
|
+
| `@voltro/plugin-cdc-out` | ◐ | Declarative reverse-ETL — mirror table changes outward to external sinks (webhook, plus a `CdcSink` interface for custom sinks) through a durable outbox; ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered. Engine + memory/webhook sinks shipped; anything else implements the `CdcSink` interface. |
|
|
95
95
|
| `@voltro/plugin-governance` | ✓ | Data governance — retention TTL sweep (delete / anonymise), GDPR subject export + erasure (admin-gated routes + `GovernanceService`), consent ledger, field encryption. [→ details](/docs/plugins/governance) |
|
|
96
96
|
| `@voltro/plugin-openapi` | ✓ | OpenAPI 3.1 spec (`GET /openapi.json`) + Swagger-UI (`GET /docs`) generated from `defineRestRoute` descriptors AND (opt-in) rpc procedures (queries/mutations/actions/streams → `POST /rpc/<name>`) — input/output/error Schemas via `JSONSchema.make`. [→ details](/docs/plugins/openapi) |
|
|
97
|
-
| `@voltro/plugin-
|
|
97
|
+
| `@voltro/plugin-row-history` | ✓ | Full row history + time-travel — value snapshot of every insert/update/delete (every table by default; narrow with include/exclude) into `_voltro_row_history` (rides the ChangeEvent tap); `rowHistory` / `rowAsOf` queries + `restoreAsOf` / `diffVersions`; TTL + per-row cap retention. [→ details](/docs/plugins/row-history) |
|
|
98
98
|
| `@voltro/plugin-presence` | ✓ | Ephemeral realtime presence — heartbeat roster per channel (`presence.heartbeat`/`list`/`leave` + `usePresence`), a `useTyping` typing indicator. Held **in memory**, owner-partitioned — no table is written; cross-instance requires [`@voltro/plugin-broadcast`](/docs/plugins/broadcast), and without a broker each replica sees only its own clients. [→ details](/docs/plugins/presence) |
|
|
99
99
|
| `@voltro/plugin-scim` | ✓ | SCIM 2.0 provisioning — Users + Groups REST at `/scim/v2` (bearer-gated) incl. group-membership PATCH/PUT + the RFC 7644 discovery trio (ServiceProviderConfig/Schemas/ResourceTypes), `userName`/`externalId`/`displayName eq` filters, pagination, unique `userName`, `active:false` deactivation; `_voltro_scim_users`/`_voltro_scim_groups`. [→ details](/docs/plugins/scim) |
|
|
100
100
|
| `@voltro/plugin-sso-saml` | ✓ | Enterprise SAML 2.0 SSO — SP-initiated login + Single Logout (both directions) + ACS + SP metadata under `/saml`; IdP-metadata-URL auto cert rotation, encrypted assertions, clock-skew, SP request signing. Signature verify via `@node-saml/node-saml` (optional+lazy), mints a framework session. [→ details](/docs/plugins/sso-saml) |
|
|
@@ -746,7 +746,7 @@ A package can ship BOTH a schema mixin AND a runtime plugin (`@voltro/plugin-aud
|
|
|
746
746
|
|
|
747
747
|
## More extension surfaces
|
|
748
748
|
|
|
749
|
-
The plugin contract carries
|
|
749
|
+
The plugin contract carries the additional surfaces below, beyond the
|
|
750
750
|
interceptors + lifecycle + manifest fields covered above:
|
|
751
751
|
|
|
752
752
|
### `inspectEndpoints: PluginInspectEndpoint[]` — plugin-mounted HTTP endpoints
|
|
@@ -874,6 +874,17 @@ plugin-provided template is scaffolded exactly like a built-in one —
|
|
|
874
874
|
`scaffoldFromTemplate` copies the declared tree and substitutes the
|
|
875
875
|
`{{…}}` tokens in both file content AND file/directory names.
|
|
876
876
|
|
|
877
|
+
### `httpRoutes: PluginHttpRoute[]` — raw HTTP endpoints on the framework listener
|
|
878
|
+
|
|
879
|
+
A plugin can mount plain HTTP routes beside the rpc surface (`@voltro/plugin-storage`'s upload/download routes, an IdP callback). The request/response shape is transport-honest, and four properties are worth pinning:
|
|
880
|
+
|
|
881
|
+
- **The full method union is first-class.** `method` is `'*' | 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'`. **HEAD is admitted wherever GET is** (RFC 9110) — the GET handler runs and the transport drops the body; you never mount a second route for it. A wrong method stays a precise `405` with an `Allow:` header, including when several routes share one path.
|
|
882
|
+
- **The body read is capped** — 8 MiB by default, the same cap as every other surface (`http.maxBodyBytes` in `app.config.ts`, env `VOLTRO_MAX_BODY_BYTES`), and the read is binary-clean. A route that takes more declares its own `maxBodyBytes`; routes **sharing a path share one body read**, so the widest override in the group applies to the group. Oversize answers `413` for both `Content-Length` and chunked requests.
|
|
883
|
+
- **Binary streaming responses** return `byteStream` on the `PluginHttpRouteResult` — a `ReadableStream<Uint8Array>` (or lazy thunk) with optional `contentLength` / `contentDisposition`, piped without buffering and never compressed. It is the plugin-route spelling of the REST surface's [`bytes()`](/docs/data/rest-routes#binary-downloads--bytes).
|
|
884
|
+
- **Buffered responses are compression-negotiated** (brotli/gzip, compressible types only) by the listener — nothing to declare; see [Security → compression](/docs/security/overview).
|
|
885
|
+
|
|
886
|
+
A state-changing plugin route is origin-checked unless it declares `originGuard: 'exempt'`, and `req.remoteAddr` is the trusted-proxy-resolved client address — both covered with examples in [Security](/docs/security/overview#routes-that-a-third-party-legitimately-posts-to).
|
|
887
|
+
|
|
877
888
|
### `onHttpRequest: HttpRequestInterceptor` — pre-auth HTTP-pipeline hook
|
|
878
889
|
|
|
879
890
|
Fires at the **very top** of every HTTP request — BEFORE auth resolution,
|
|
@@ -937,6 +948,17 @@ is deliberate: `GET /internal/liveness` and `GET /internal/readiness` are
|
|
|
937
948
|
answered before the interceptor, so a rate-limit or geo-block plugin cannot 503
|
|
938
949
|
a Kubernetes probe and take the replica out of rotation.
|
|
939
950
|
|
|
951
|
+
**The chain is FAIL-CLOSED.** An interceptor that throws is a `500` plus a log
|
|
952
|
+
line — the request does NOT continue. It used to: the failure was swallowed and
|
|
953
|
+
the request flowed on, which meant a crashed security gate was an open one.
|
|
954
|
+
That polarity puts a decision on every interceptor author: if your hook is a
|
|
955
|
+
GATE (geo-block, bot detection), let a failure propagate — refusing is the
|
|
956
|
+
correct degraded behaviour. If it is protection with a DEPENDENCY (a rate-limit
|
|
957
|
+
counter in Redis), catch your own failure inside the hook and **degrade
|
|
958
|
+
loudly** — `@voltro/plugin-ratelimit`'s `httpShield` does exactly that: a Redis
|
|
959
|
+
outage means unlimited-with-a-warning, never a self-inflicted API outage.
|
|
960
|
+
What no interceptor gets to do anymore is fail silently and stay in the chain.
|
|
961
|
+
|
|
940
962
|
### `extendSchema: { tables, migrations }` — contribute schema + migrations
|
|
941
963
|
|
|
942
964
|
A plugin contributes BOTH declarative table descriptors AND custom SQL
|
|
@@ -581,6 +581,7 @@ Returns a function `(to: string, options?) => void`.
|
|
|
581
581
|
|---|---|
|
|
582
582
|
| `replace: true` | Replace the history entry (no back-button entry). |
|
|
583
583
|
| `scroll: false` | Don't scroll to top after navigation. |
|
|
584
|
+
| `transition: true / false` | Run (or suppress) this navigation's swap through `document.startViewTransition`, overriding the app-wide `router.viewTransitions` default. `<Link transition>` is the declarative mirror. See [View transitions](/docs/routing/navigation#view-transitions). |
|
|
584
585
|
|
|
585
586
|
`navigate` takes a path string only — there is no numeric history overload. For back / forward, reach for the browser API:
|
|
586
587
|
|
|
@@ -607,12 +608,22 @@ Pass `true`/`false` or a predicate `({ to, opts }) => boolean` (to allow some de
|
|
|
607
608
|
|
|
608
609
|
## `useSearchParams()` + `useSetSearchParams()`
|
|
609
610
|
|
|
610
|
-
Read the query string
|
|
611
|
+
Read the query string. Two overloads, both SSR-aware (the request URL on the server, `window.location.search` on the client):
|
|
612
|
+
|
|
613
|
+
- `useSearchParams()` — the raw `URLSearchParams`, for routes without a schema.
|
|
614
|
+
- `useSearchParams(searchParams)` — pass the page's own `searchParams` schema export to get the decoded, typed shape. Defaults applied; an invalid query falls back to the defaults instead of crashing.
|
|
611
615
|
|
|
612
616
|
```tsx
|
|
617
|
+
import { Schema } from 'effect'
|
|
613
618
|
import { useSearchParams } from '@voltro/web'
|
|
614
619
|
|
|
615
|
-
const
|
|
620
|
+
export const searchParams = Schema.Struct({
|
|
621
|
+
tab: Schema.optionalWith(Schema.String, { default: () => 'overview' }),
|
|
622
|
+
page: Schema.optionalWith(Schema.NumberFromString, { default: () => 1 }),
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
const { tab, page } = useSearchParams(searchParams) // tab: string · page: number
|
|
626
|
+
const raw = useSearchParams() // URLSearchParams (schema-less routes)
|
|
616
627
|
```
|
|
617
628
|
|
|
618
629
|
Write it with `useSetSearchParams()` — the setter updates the query via `navigate`, so readers re-render immediately:
|
|
@@ -628,6 +639,28 @@ setParams({ page: '2' }, { push: true }) // distinct history entry
|
|
|
628
639
|
|
|
629
640
|
Writes default to a history replace; pass `{ push: true }` for a Back entry or `{ scroll: false }` to keep scroll. See [Navigation](/docs/routing/navigation#reading--writing-search-params).
|
|
630
641
|
|
|
642
|
+
`useSetSearchParams(searchParams)` — pass the schema to get the **typed** setter. Object form replaces the query (a left-out field decodes to its default on the next read); the updater form receives the current **decoded** params, so a merge is an explicit spread:
|
|
643
|
+
|
|
644
|
+
```tsx
|
|
645
|
+
const setTyped = useSetSearchParams(searchParams)
|
|
646
|
+
setTyped({ page: 2 }) // replaces → ?page=2
|
|
647
|
+
setTyped((p) => ({ ...p, page: p.page + 1 })) // keeps every other param — typed merge
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
## Typed `withQuery()`
|
|
651
|
+
|
|
652
|
+
Not a hook, but the link-side half of the same contract: for a route whose page exports a `searchParams` schema, the generated `routes` builder brands the URL with the schema's shape (through a type-only import — no page code enters the routes module), and `withQuery` type-checks the params against it — a misspelt key or a wrong value type is a compile error:
|
|
653
|
+
|
|
654
|
+
```tsx
|
|
655
|
+
import { withQuery } from '@voltro/web'
|
|
656
|
+
import { routes } from './.framework/routes'
|
|
657
|
+
|
|
658
|
+
withQuery(routes['/notes'](), { page: 2 }) // OK — typed against the schema
|
|
659
|
+
// withQuery(routes['/notes'](), { pgae: 2 }) // compile error (unknown key)
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
The encode is canonical: strings pass through, numbers/booleans via `String()`, arrays as repeated keys, `undefined` omitted; a `Date` (or any object) is refused loudly — declare the field as a string/number transform in the schema instead. See [Navigation → typed `withQuery`](/docs/routing/navigation#typed-withquery).
|
|
663
|
+
|
|
631
664
|
## `usePrefetch()`
|
|
632
665
|
|
|
633
666
|
Trigger loader-data prefetch on hover / focus. Wired automatically by `<Link prefetch />`; export only useful for custom triggers.
|
|
@@ -821,6 +854,10 @@ export default function SearchPage() {
|
|
|
821
854
|
that must react to router-pushed query changes without a reload re-render through the router
|
|
822
855
|
(`useNavigate`/`useLocation`); the hook then re-resolves on that render.
|
|
823
856
|
|
|
857
|
+
Prefer the typed form where the page declares a `searchParams` schema export —
|
|
858
|
+
`useSearchParams(searchParams)` returns the decoded shape instead of a raw
|
|
859
|
+
`URLSearchParams`. See [Routing hooks](/docs/reference/hooks-routing#usesearchparams--usesetsearchparams).
|
|
860
|
+
|
|
824
861
|
## Reading cookies
|
|
825
862
|
|
|
826
863
|
```ts
|