@voltro/cli 0.34.0 → 0.35.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 +167 -0
- package/dist/apiBuild-B8aoJvuw.js +2 -0
- package/dist/{apiBuild-DZF_f0_I.js → apiBuild-DYD_ONLD.js} +2 -2
- package/dist/{appGraph-gQ_6GkQQ.js → appGraph-KGDPTuTy.js} +1 -1
- package/dist/appGraph-zuMGKVYX.js +2 -0
- package/dist/bin.js +1 -1
- package/dist/{build-CE7Ca9gm.js → build-CD8K4XOr.js} +7 -7
- package/dist/{checkCommand-Dv8r9tGO.js → checkCommand-Ct9xkTrS.js} +8 -7
- package/dist/{checkCommand-YZDmkAGd.js → checkCommand-DKpDLlqu.js} +1 -1
- package/dist/{clusterCmd-CNXKlcvD.js → clusterCmd-D5wsCmA_.js} +1 -1
- package/dist/{codegenCommand-DC6w2tNZ.js → codegenCommand-C4YoQIc2.js} +3 -3
- package/dist/{codemodRunner-DRRqXR74.js → codemodRunner-BnFq3Fgu.js} +175 -34
- package/dist/{commands-gutsz-Ac.js → commands-BE8E7zF3.js} +37 -29
- package/dist/{dashboardCommand-3YG8p-UA.js → dashboardCommand-D7SgZGaN.js} +3 -3
- package/dist/{dataCommand-qL0r7fPO.js → dataCommand-BhYwDgg-.js} +56 -54
- package/dist/dbCommand-DS4b97Is.js +2 -0
- package/dist/{dbCommand-B6X0FZbq.js → dbCommand-O8HA63s2.js} +6 -6
- package/dist/{dev-CerMd0mW.js → dev-C7sFZq3m.js} +2696 -2188
- package/dist/{dev-CoG-ZPx8.js → dev-D2BikO7a.js} +1 -1
- package/dist/doctorCommand-CM4Ch9C7.js +2 -0
- package/dist/{doctorCommand-CnDXQxfa.js → doctorCommand-DnimF5IM.js} +76 -73
- package/dist/{dormancyCommand-Dszo57d6.js → dormancyCommand-QewYug_s.js} +1 -1
- package/dist/{embeddingsCommand-C0sKVRo1.js → embeddingsCommand-BfiLS_QI.js} +1 -1
- package/dist/{envCommand-DPgdV1Bq.js → envCommand-CCGPRQY1.js} +2 -2
- package/dist/{evolveCommand-DHpkgjgH.js → evolveCommand-CHsLCtDf.js} +2 -2
- package/dist/frameworkTableAssembly-YyVe32Cb.js +2 -0
- package/dist/frameworkTableAssembly-oMPBKqlE.js +511 -0
- package/dist/{generateCommand-oibemh97.js → generateCommand-DbgcUpGw.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/{infoCommand-BJw9nLUR.js → infoCommand-DwOgK1t6.js} +1 -1
- package/dist/{inspect-CBqFtAKA.js → inspect-CjTYzAs_.js} +32 -2
- package/dist/inspect-P4pxoMaV.js +2 -0
- package/dist/{inspectCmd-Bppy-GGw.js → inspectCmd-EHFZ9yYu.js} +1 -1
- package/dist/{inspectFetch-Cm8_wVvp.js → inspectFetch-EMuhTG_9.js} +1 -1
- package/dist/{logsCmd-CCca3KRZ.js → logsCmd-D36xK7Zu.js} +1 -1
- package/dist/manifestBuild-COkJoyAr.js +2 -0
- package/dist/{manifestBuild-sxpwdKY1.js → manifestBuild-hpPLaGxV.js} +1 -1
- package/dist/{migrate-CBwOt_iV.js → migrate-BV7I-ZHZ.js} +3 -3
- package/dist/probeCommand-CZfaaUOZ.js +122 -0
- package/dist/{runtimeTrace-CRxalXTs.js → runtimeTrace-CH3eUiMw.js} +1 -1
- package/dist/scheduleCmd-DQRu6BZC.js +149 -0
- package/dist/{sdkgen-CYJscZC7.js → sdkgen-BLkvGRfX.js} +1 -1
- package/dist/{seedRunner-TFHHiToI.js → seedRunner-ZmLSqNe2.js} +74 -70
- package/dist/serveCommand-CbDHU6l-.js +2 -0
- package/dist/serveCommand-iwlUBNS1.js +1766 -0
- package/dist/serveEntry.js +1 -1
- package/dist/{start-B-9Nsp-S.js → start-BgN62boB.js} +1 -1
- package/dist/{start-9LiUOfES.js → start-T4VesWiM.js} +2 -2
- package/dist/startEntry.js +1 -1
- package/dist/{tracesCmd-CkEZQrtt.js → tracesCmd-DStmCJPi.js} +1 -1
- package/dist/updateCommand-BlMZhWgO.js +2 -0
- package/dist/{updateCommand-Bkptutss.js → updateCommand-x0pI_x-B.js} +1 -1
- package/dist/webDev-BcykISYQ2.js +2 -0
- package/dist/{webDev-CBYvPqQr.js → webDev-Dybxew86.js} +14 -2
- package/dist/{webhooksCommand-CID96Rga.js → webhooksCommand-DlAgS2Iw.js} +1 -1
- package/dist/{workflowsCmd-D1VTmLMY.js → workflowsCmd-BGF-mRZ5.js} +1 -1
- package/package.json +47 -17
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/_manifest.json +1 -1
- package/templates/agent-docs/authentication.md +55 -7
- package/templates/agent-docs/cli.md +59 -0
- package/templates/agent-docs/data.md +59 -2
- package/templates/agent-docs/database/migrations.md +50 -1
- package/templates/agent-docs/deployment.md +214 -21
- package/templates/agent-docs/introduction.md +1 -1
- package/templates/agent-docs/multi-tenancy.md +93 -22
- package/templates/agent-docs/plugins/ai-flows.md +86 -0
- package/templates/agent-docs/plugins/billing.md +1 -1
- package/templates/agent-docs/plugins/clickhouse.md +31 -0
- package/templates/agent-docs/plugins/search.md +6 -3
- package/templates/agent-docs/plugins/storage.md +11 -0
- package/templates/agent-docs/plugins.md +3 -1
- package/templates/agent-docs/scheduling.md +17 -0
- package/templates/agent-docs/security.md +2 -2
- package/templates/agent-docs/whats-new.md +95 -1729
- package/templates/agent-docs/workflows.md +101 -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/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-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/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-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-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 +3 -3
- package/dist/apiBuild-BICVGnEC.js +0 -2
- package/dist/appGraph-CvQCte0z.js +0 -2
- package/dist/dbCommand-CpdKLeQq.js +0 -2
- package/dist/doctorCommand-BvqGBwNG.js +0 -2
- package/dist/frameworkTableAssembly-BGHmck-x.js +0 -2
- package/dist/frameworkTableAssembly-DkkP6BgC.js +0 -674
- package/dist/inspect-C_T_WGvl.js +0 -2
- package/dist/manifestBuild-ChsKAhmn.js +0 -2
- package/dist/scheduleCmd--jksTrf6.js +0 -69
- package/dist/serveCommand-B_isw7q4.js +0 -1647
- package/dist/serveCommand-DOvbgRnQ.js +0 -2
- package/dist/updateCommand-us1_hdIC.js +0 -2
- package/dist/webDev-Cg-fFiyd2.js +0 -2
package/dist/serveEntry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as e } from "./loadEnv-D9nEOClM.js";
|
|
2
2
|
import { r as t } from "./appModuleLoader-C9r9mxZt.js";
|
|
3
3
|
import { i as n } from "./dialectDriver-CgXnDfec.js";
|
|
4
|
-
import { t as r } from "./serveCommand-
|
|
4
|
+
import { t as r } from "./serveCommand-iwlUBNS1.js";
|
|
5
5
|
export { e as loadDotEnv, t as registerAppModules, n as registerDriver, r as runServe };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { i as e, t } from "./nodeEnvironment-cGFAj1J8.js";
|
|
2
2
|
import { r as n } from "./appPort-B_HpJ_ck.js";
|
|
3
|
-
import { A as r, B as i, C as a, D as o, E as s, H as c, I as l, J as u, N as d, O as f, P as p, S as m, T as ee, U as h, V as g, a as _, b as v, d as y, f as b, i as x, j as S, k as te, n as ne, q as C, s as re, st as ie, ut as w, w as T, x as E, y as ae, z as D } from "./webDev-
|
|
3
|
+
import { A as r, B as i, C as a, D as o, E as s, H as c, I as l, J as u, N as d, O as f, P as p, S as m, T as ee, U as h, V as g, a as _, b as v, d as y, f as b, i as x, j as S, k as te, n as ne, q as C, s as re, st as ie, ut as w, w as T, x as E, y as ae, z as D } from "./webDev-Dybxew86.js";
|
|
4
4
|
import { o as O, t as k } from "./connectionConfig-UFlIEiys.js";
|
|
5
|
-
import { A, D as j, a as M, h as oe, j as N, k as se } from "./inspect-
|
|
5
|
+
import { A, D as j, a as M, h as oe, j as N, k as se } from "./inspect-CjTYzAs_.js";
|
|
6
6
|
import { a as ce, c as P, d as le, f as ue, l as F, p as I, u as de } from "./renderModeScan-D7J1B7Kw.js";
|
|
7
7
|
import { i as fe, n as pe, r as me, t as he } from "./runtimeRegistry-DMeKfTHP.js";
|
|
8
8
|
import { n as ge } from "./logFileSink-C_D2wRN1.js";
|
package/dist/startEntry.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as e } from "./runtimeRegistry-DMeKfTHP.js";
|
|
2
2
|
import { n as t } from "./cliOutput-D1tSBoRM.js";
|
|
3
|
-
import { a as n, i as r } from "./inspectFetch-
|
|
3
|
+
import { a as n, i as r } from "./inspectFetch-EMuhTG_9.js";
|
|
4
4
|
import { ansi as i } from "@voltro/logger";
|
|
5
5
|
//#region src/tracesCmd.ts
|
|
6
6
|
var a = (e) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as e, r as t, t as n } from "./codemodRunner-
|
|
1
|
+
import { n as e, r as t, t as n } from "./codemodRunner-BnFq3Fgu.js";
|
|
2
2
|
import { basename as r, dirname as i, join as a, relative as o, resolve as s } from "node:path";
|
|
3
3
|
import { existsSync as c, readFileSync as l, readdirSync as u, statSync as d, writeFileSync as f } from "node:fs";
|
|
4
4
|
import { totalmem as p } from "node:os";
|
|
@@ -2,7 +2,7 @@ import { t as e } from "./nodeEnvironment-cGFAj1J8.js";
|
|
|
2
2
|
import { n as t } from "./loadEnv-D9nEOClM.js";
|
|
3
3
|
import { r as n } from "./appPort-B_HpJ_ck.js";
|
|
4
4
|
import { h as r, m as i } from "./fileConventions-Cof68_BL.js";
|
|
5
|
-
import { A as a, D as o, a as s, h as c, j as l, k as u } from "./inspect-
|
|
5
|
+
import { A as a, D as o, a as s, h as c, j as l, k as u } from "./inspect-CjTYzAs_.js";
|
|
6
6
|
import { n as d } from "./agentsMd-mhQMF1bx.js";
|
|
7
7
|
import { c as f, p, t as m } from "./renderModeScan-D7J1B7Kw.js";
|
|
8
8
|
import { t as h } from "./pageConvention-cEiRxdab.js";
|
|
@@ -1854,7 +1854,19 @@ import {
|
|
|
1854
1854
|
if (!o.ok) throw Error(`[loader.query] ${r}: HTTP ${o.status} ${await o.text()}`);
|
|
1855
1855
|
let s = await o.json();
|
|
1856
1856
|
if (!Array.isArray(s)) throw Error(`[loader.query] ${r}: unexpected non-array rpc response`);
|
|
1857
|
-
let c = (e) =>
|
|
1857
|
+
let c = (e) => {
|
|
1858
|
+
if (!e || typeof e != "object" || !("_tag" in e)) return e;
|
|
1859
|
+
let t = e;
|
|
1860
|
+
switch (t._tag) {
|
|
1861
|
+
case "snapshot": return t.data;
|
|
1862
|
+
case "error": {
|
|
1863
|
+
let e = t.error;
|
|
1864
|
+
throw Object.assign(Error(`[loader.query] ${r}: ${e?._tag ?? "error"}${e?.message ? ` — ${e.message}` : ""}`, { cause: t.error }), { voltroError: t.error });
|
|
1865
|
+
}
|
|
1866
|
+
case "delta": throw Error(`[loader.query] ${r}: received a 'delta' as the first event, which should be impossible — a subscription always opens with a snapshot. This is a framework bug.`);
|
|
1867
|
+
default: return e;
|
|
1868
|
+
}
|
|
1869
|
+
}, l, u = !1;
|
|
1858
1870
|
for (let e of s) {
|
|
1859
1871
|
if (e._tag === "ClientProtocolError") throw Error(`[loader.query] ${r}: protocol error ${JSON.stringify(e.error)}`);
|
|
1860
1872
|
if (!(e.requestId !== void 0 && String(e.requestId) !== "0")) {
|
|
@@ -222,7 +222,7 @@ createVerifier({ secret: [process.env.WEBHOOK_SECRET, process.env.WEBHOOK_SECRET
|
|
|
222
222
|
...t === void 0 ? {} : { payload: t }
|
|
223
223
|
};
|
|
224
224
|
}, S = u({ scope: "voltro:webhooks" }), C = ["--out", "--name"], w = async (e) => {
|
|
225
|
-
let { walk: t, loadDiscovered: n } = await import("./dev-
|
|
225
|
+
let { walk: t, loadDiscovered: n } = await import("./dev-D2BikO7a.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-CrGAfhIG.js");
|
|
226
226
|
return r((await n(await t(e))).events.map((e) => ({
|
|
227
227
|
file: e.file,
|
|
228
228
|
descriptor: e.descriptor
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as e } from "./cliOutput-D1tSBoRM.js";
|
|
2
|
-
import { n as t, o as n, r, t as i } from "./inspectFetch-
|
|
2
|
+
import { n as t, o as n, r, t as i } from "./inspectFetch-EMuhTG_9.js";
|
|
3
3
|
import { ansi as a } from "@voltro/logger";
|
|
4
4
|
//#region src/workflowsCmd.ts
|
|
5
5
|
var o = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -538,6 +538,36 @@
|
|
|
538
538
|
"title": "`routeHttp` returns a Promise",
|
|
539
539
|
"kind": "manual"
|
|
540
540
|
},
|
|
541
|
+
{
|
|
542
|
+
"version": "0.35.0",
|
|
543
|
+
"id": "0.35.0/01_event-access-decision",
|
|
544
|
+
"title": "Every declared event decides who may listen (`guards:` or `openAccess:`)",
|
|
545
|
+
"kind": "manual"
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
"version": "0.35.0",
|
|
549
|
+
"id": "0.35.0/02_plugin-procedures-carry-access-decisions",
|
|
550
|
+
"title": "First-party plugin procedures carry access decisions; twelve now require a scope",
|
|
551
|
+
"kind": "manual"
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
"version": "0.35.0",
|
|
555
|
+
"id": "0.35.0/03_analytics-mirror-max-version",
|
|
556
|
+
"title": "Custom analytics mirror sinks answer `maxVersion` (fleet-scope seed read)",
|
|
557
|
+
"kind": "manual"
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
"version": "0.35.0",
|
|
561
|
+
"id": "0.35.0/04_flow-run-step-skipped",
|
|
562
|
+
"title": "A flow run step can now be 'skipped' — handle it where you render a run timeline",
|
|
563
|
+
"kind": "manual"
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
"version": "0.35.0",
|
|
567
|
+
"id": "0.35.0/05_declared-schema-drops-runtime-flags",
|
|
568
|
+
"title": "The declared schema no longer reads `CDC` / `VOLTRO_UNDO` / `VOLTRO_TRACING_PERSIST`",
|
|
569
|
+
"kind": "manual"
|
|
570
|
+
},
|
|
541
571
|
{
|
|
542
572
|
"version": "0.4.0",
|
|
543
573
|
"id": "0.4.0/01_rbac-forbidden-to-scopeerror",
|
|
@@ -637,22 +667,22 @@
|
|
|
637
667
|
"@effect/platform-node": "^0.108.0",
|
|
638
668
|
"@effect/sql": "^0.52.0",
|
|
639
669
|
"@effect/workflow": "^0.19.0",
|
|
640
|
-
"@voltro/ai": "0.
|
|
641
|
-
"@voltro/cache": "0.
|
|
642
|
-
"@voltro/data-transfer": "0.
|
|
643
|
-
"@voltro/database": "0.
|
|
644
|
-
"@voltro/env": "0.
|
|
645
|
-
"@voltro/kv": "0.
|
|
646
|
-
"@voltro/logger": "0.
|
|
647
|
-
"@voltro/plugin-auth": "0.
|
|
648
|
-
"@voltro/plugin-broadcast": "0.
|
|
649
|
-
"@voltro/plugin-mail": "0.
|
|
650
|
-
"@voltro/plugin-storage": "0.
|
|
651
|
-
"@voltro/plugin-webhooks": "0.
|
|
652
|
-
"@voltro/protocol": "0.
|
|
653
|
-
"@voltro/runtime": "0.
|
|
654
|
-
"@voltro/serverless": "0.
|
|
655
|
-
"@voltro/workflow": "0.
|
|
670
|
+
"@voltro/ai": "0.35.0",
|
|
671
|
+
"@voltro/cache": "0.35.0",
|
|
672
|
+
"@voltro/data-transfer": "0.35.0",
|
|
673
|
+
"@voltro/database": "0.35.0",
|
|
674
|
+
"@voltro/env": "0.35.0",
|
|
675
|
+
"@voltro/kv": "0.35.0",
|
|
676
|
+
"@voltro/logger": "0.35.0",
|
|
677
|
+
"@voltro/plugin-auth": "0.35.0",
|
|
678
|
+
"@voltro/plugin-broadcast": "0.35.0",
|
|
679
|
+
"@voltro/plugin-mail": "0.35.0",
|
|
680
|
+
"@voltro/plugin-storage": "0.35.0",
|
|
681
|
+
"@voltro/plugin-webhooks": "0.35.0",
|
|
682
|
+
"@voltro/protocol": "0.35.0",
|
|
683
|
+
"@voltro/runtime": "0.35.0",
|
|
684
|
+
"@voltro/serverless": "0.35.0",
|
|
685
|
+
"@voltro/workflow": "0.35.0",
|
|
656
686
|
"chokidar": "^5.0.0",
|
|
657
687
|
"ioredis": "^5.11.1",
|
|
658
688
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.md
CHANGED
|
@@ -707,7 +707,7 @@ each plugin's own README.
|
|
|
707
707
|
|
|
708
708
|
| Topic | Open | Summary |
|
|
709
709
|
|---|---|---|
|
|
710
|
-
| **What's new in 0.
|
|
710
|
+
| **What's new in 0.35.0** | `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. |
|
|
711
711
|
| 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. |
|
|
712
712
|
| 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. |
|
|
713
713
|
| 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. |
|
|
@@ -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.35.0** | `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. |
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
"group": null,
|
|
162
162
|
"description": "Voltro Cloud (coming soon) — the managed runtime for your Voltro project. Today the Free control plane registers + observes your self-hosted apps.",
|
|
163
163
|
"path": "agent-docs/deployment.md",
|
|
164
|
-
"files":
|
|
164
|
+
"files": 8
|
|
165
165
|
},
|
|
166
166
|
{
|
|
167
167
|
"id": "internationalization",
|
|
@@ -2167,6 +2167,11 @@ default to "allowed", so a discovered `*.query.ts` / `*.mutation.ts` /
|
|
|
2167
2167
|
`*.action.ts` / `*.stream.ts` with no guard was callable by **any authenticated
|
|
2168
2168
|
session** — the door defaulted open, and nothing said so.
|
|
2169
2169
|
|
|
2170
|
+
The same rule covers **events**: a `*.event.ts` declaration is a wire surface
|
|
2171
|
+
too, and one that declares neither `guards:` nor `openAccess:` was silently
|
|
2172
|
+
subscribable by anyone who could open the socket. `defineEvent` takes the same
|
|
2173
|
+
two answers — see [Events](/docs/data/events).
|
|
2174
|
+
|
|
2170
2175
|
There are exactly two answers, and they are not the same claim:
|
|
2171
2176
|
|
|
2172
2177
|
```ts
|
|
@@ -2204,8 +2209,8 @@ refusal names **every** offending procedure with its file, because the fix is on
|
|
|
2204
2209
|
pass over the whole list:
|
|
2205
2210
|
|
|
2206
2211
|
```
|
|
2207
|
-
[access] 3 wire-exposed procedures declare no access decision, and
|
|
2208
|
-
with `security.defaultDeny`:
|
|
2212
|
+
[access] 3 wire-exposed procedures or events declare no access decision, and
|
|
2213
|
+
this app runs with `security.defaultDeny`:
|
|
2209
2214
|
|
|
2210
2215
|
invoices.list (query)
|
|
2211
2216
|
src/api/invoices.query.ts
|
|
@@ -2229,11 +2234,24 @@ anyone reaches for is off, and an env var is how a security default becomes
|
|
|
2229
2234
|
permanently off in one CI job with no diff to review. `voltro doctor` keeps
|
|
2230
2235
|
listing the undecided procedures while it is off, marked advisory.
|
|
2231
2236
|
|
|
2232
|
-
### What it does NOT cover
|
|
2233
|
-
|
|
2234
|
-
The gate reads **your app's own** discovered procedures. The
|
|
2235
|
-
declares are the plugin author's decision and are not
|
|
2236
|
-
this does not turn into a bug report against a plugin you
|
|
2237
|
+
### What it does NOT cover — and what covers the rest
|
|
2238
|
+
|
|
2239
|
+
The **boot** gate reads **your app's own** discovered procedures and events. The
|
|
2240
|
+
procedures a plugin declares are the plugin author's decision and are not judged
|
|
2241
|
+
at boot — adopting this does not turn into a bug report against a plugin you
|
|
2242
|
+
installed.
|
|
2243
|
+
|
|
2244
|
+
They are not unpoliced, though: the same `security.defaultDeny` is also enforced
|
|
2245
|
+
**per request in the dispatch spine**, as defense in depth. A descriptor that
|
|
2246
|
+
reaches the wire with no access decision — a plugin route, a hand-bound
|
|
2247
|
+
descriptor — is refused with the same typed `ScopeError` before the transaction
|
|
2248
|
+
opens or any external I/O runs. Every first-party plugin route declares its own
|
|
2249
|
+
decision (a scope where a real authority exists — e.g. `billing:manage`,
|
|
2250
|
+
`storage:browse` — or `openAccess` with the reason on the routes that are
|
|
2251
|
+
self-scoped or anonymous-capable by design; each plugin's page lists them). A
|
|
2252
|
+
third-party plugin that declares neither on a route will see that route refused
|
|
2253
|
+
per-request under default-deny — the fix is one field on the route, exactly as
|
|
2254
|
+
for your own procedures.
|
|
2237
2255
|
|
|
2238
2256
|
## Decide — `can` / `assertCan`
|
|
2239
2257
|
|
|
@@ -2486,6 +2504,36 @@ error frame, rather than delivering an empty snapshot — an empty snapshot on a
|
|
|
2486
2504
|
live subscription reads to a client as "every row you could see was just
|
|
2487
2505
|
deleted". Make sure your subscription error handling surfaces it.
|
|
2488
2506
|
|
|
2507
|
+
## A path that cannot apply the filter refuses, rather than serving rows
|
|
2508
|
+
|
|
2509
|
+
If a filter is registered and a scoped store is built without a resolved scope,
|
|
2510
|
+
the store **throws**. It does not fall back to unfiltered reads.
|
|
2511
|
+
|
|
2512
|
+
That fallback used to exist, and it is the reason this section does. A team
|
|
2513
|
+
measured four read paths returning every row of the tenant to every employee,
|
|
2514
|
+
on both transports, with `row filter registered` in the boot log and a green
|
|
2515
|
+
test suite. The registration lived in a module-local variable, so an app's
|
|
2516
|
+
`*.startup.tsx` and the framework's request pipeline could hold two different
|
|
2517
|
+
copies of it — the serve bundle inlines the framework while app modules stay
|
|
2518
|
+
external, and a strict pnpm tree can resolve one version into two directories.
|
|
2519
|
+
The pipeline read "no filter registered", which was indistinguishable from an
|
|
2520
|
+
app that has none, and served everything.
|
|
2521
|
+
|
|
2522
|
+
The registration is process-global for real now (`globalThis`, so every copy
|
|
2523
|
+
shares one cell), and the ambiguity that made the failure silent is gone: those
|
|
2524
|
+
two readings are different claims and only one of them is a decision.
|
|
2525
|
+
|
|
2526
|
+
If a code path is deliberately unfiltered — a system sweep, a migration, a
|
|
2527
|
+
seeding helper in a test — say so:
|
|
2528
|
+
|
|
2529
|
+
```ts
|
|
2530
|
+
wrapStoreWithMixinBehaviour(store, { subject, schemaRegistry, rowFilter: NO_ROW_FILTER })
|
|
2531
|
+
```
|
|
2532
|
+
|
|
2533
|
+
`runAsSystem`, change-stream subscribers and the webhook trigger context already
|
|
2534
|
+
do this; a system subject bypasses row filters by design, and it is now written
|
|
2535
|
+
down rather than inferred from an absence.
|
|
2536
|
+
|
|
2489
2537
|
## What does *not* bypass it
|
|
2490
2538
|
|
|
2491
2539
|
| | Bypasses the row filter? |
|
|
@@ -2107,6 +2107,45 @@ voltro check --url https://api.example.com
|
|
|
2107
2107
|
|
|
2108
2108
|
`--token` (or `VOLTRO_INSPECT_TOKEN`) supplies the bearer; `VOLTRO_INSPECT_URL` sets a default target so you can drop the flag. Works for `inspect`, `logs`, `traces`, `workflows`, `cluster` and `check`.
|
|
2109
2109
|
|
|
2110
|
+
## `voltro probe access` — is a declared guard actually enforced?
|
|
2111
|
+
|
|
2112
|
+
`voltro check` reads an app's manifest and reports a procedure with **no access
|
|
2113
|
+
decision**. It cannot tell you whether the decisions that ARE declared are
|
|
2114
|
+
enforced. `voltro probe access` asks the running app:
|
|
2115
|
+
|
|
2116
|
+
```bash
|
|
2117
|
+
voltro probe access # every live api
|
|
2118
|
+
voltro probe access --url https://api.example.com --strict
|
|
2119
|
+
```
|
|
2120
|
+
|
|
2121
|
+
It calls every procedure that declares a guard with **no credentials at all**
|
|
2122
|
+
and reports any that answer anyway:
|
|
2123
|
+
|
|
2124
|
+
```
|
|
2125
|
+
api: probed 14 guarded procedure(s) with NO credentials
|
|
2126
|
+
✗ orders.export — ANSWERED an unauthenticated call
|
|
2127
|
+
? billing.invoice — answered 'ParseError' — not an access refusal, so the guard was not reached
|
|
2128
|
+
✓ 12 refused · 1 inconclusive · 1 admitted
|
|
2129
|
+
```
|
|
2130
|
+
|
|
2131
|
+
Three verdicts, and the third is what keeps the command honest:
|
|
2132
|
+
|
|
2133
|
+
| Verdict | Meaning |
|
|
2134
|
+
|---|---|
|
|
2135
|
+
| `refused` | The call came back as an access refusal. The declaration is enforced. |
|
|
2136
|
+
| `admitted` | The call SUCCEEDED without credentials. **This is the finding.** |
|
|
2137
|
+
| `inconclusive` | The call failed for a reason that is not an access refusal — usually payload validation running before the guard. **Not a pass.** |
|
|
2138
|
+
|
|
2139
|
+
Exit code is non-zero on any `admitted`. `--strict` also fails on
|
|
2140
|
+
`inconclusive`, which is what you want in CI: "I could not tell" should block.
|
|
2141
|
+
|
|
2142
|
+
**What it deliberately does not do.** It probes ANONYMOUSLY, so it cannot tell
|
|
2143
|
+
`orders:read` from `orders:write` — it answers exactly one question, and the
|
|
2144
|
+
alternative (minting a subject per guard) would put credential minting into a
|
|
2145
|
+
command that can be pointed at production. Procedures declared `openAccess:` are
|
|
2146
|
+
skipped; probing them would report every deliberately-public route as a finding
|
|
2147
|
+
and bury the real ones.
|
|
2148
|
+
|
|
2110
2149
|
## Securing the local surface
|
|
2111
2150
|
|
|
2112
2151
|
`voltro dev` mints a per-project `VOLTRO_INSPECT_TOKEN` into `.env.local`, so the inspect surface is authenticated from the first boot — the dev server listens on every interface, and without a token anyone on the same network could read your rows, schema and logs. You don't have to wire it anywhere: the CLI picks the token up from the runtime registry (so the commands work from any directory), and the dashboard's server-side proxy supplies it for same-machine targets. Setting `VOLTRO_INSPECT_TOKEN` yourself always wins.
|
|
@@ -2564,6 +2603,26 @@ voltro data import ./out --force # import despite schema drift AND c
|
|
|
2564
2603
|
|
|
2565
2604
|
Import is **integrity-checked** (each table's checksum + row count verified as it decodes; each asset re-hashed against its content address), applies tables in **FK-parent-first order**, and a resumed run skips already-applied tables via the ledger.
|
|
2566
2605
|
|
|
2606
|
+
### Postgres targets bulk-load via COPY
|
|
2607
|
+
|
|
2608
|
+
On a **postgres** target, the direct import switches to `COPY … FROM STDIN`
|
|
2609
|
+
wherever plain-INSERT semantics provably hold: `--mode replace` (the tables were
|
|
2610
|
+
just truncated) and the default `upsert` into a table that is **empty** at
|
|
2611
|
+
import time — the fresh-target shape every cross-dialect migration
|
|
2612
|
+
(mysql → postgres, sqlite → postgres, …) lands in. Measured on a 7-column table
|
|
2613
|
+
(text/int/bool/jsonb/timestamptz) with 50 000 rows against a local postgres:
|
|
2614
|
+
row-by-row **12.8 s (~3.9 k rows/s)** vs COPY **0.59 s (~84.6 k rows/s)** —
|
|
2615
|
+
**21.7× faster**. Your factor depends on row width and network latency;
|
|
2616
|
+
COPY's advantage grows with per-row round-trip cost.
|
|
2617
|
+
|
|
2618
|
+
Everything else keeps the per-row `DataStore` writes: `upsert` into a non-empty
|
|
2619
|
+
table (COPY cannot upsert), `append` (per-row conflict handling), `--atomic`
|
|
2620
|
+
(the COPY connection would sit outside the transaction), and every other
|
|
2621
|
+
dialect. A refused COPY batch (an FK the deferred pass repairs later, a value
|
|
2622
|
+
COPY text can't carry) is atomic — nothing landed — so the importer replays
|
|
2623
|
+
exactly that batch through the per-row path and continues; semantics are
|
|
2624
|
+
identical, only the speed differs.
|
|
2625
|
+
|
|
2567
2626
|
### Schema-drift pre-flight
|
|
2568
2627
|
|
|
2569
2628
|
Every bundle records the **schema fingerprint** of the source it was exported from (a stable hash of the schema shape — the same fingerprint prod boot uses to detect drift). Before touching the target, `import` **recomputes the target app's fingerprint the same way** and compares it to the bundle's. On a mismatch it **refuses, fail-closed, before any row lands** — a drifted target (a missing column, a renamed table) would otherwise fail mid-load with a raw database error after some rows already committed:
|
|
@@ -1606,6 +1606,37 @@ Names are lowercase kebab segments separated by dots — `presence`, `job-queue`
|
|
|
1606
1606
|
uppercase letter (a key that differs only by case reads as one channel and
|
|
1607
1607
|
routes as two). Declaring the same name twice returns the same channel.
|
|
1608
1608
|
|
|
1609
|
+
### A publish is LOCAL news, and says so
|
|
1610
|
+
|
|
1611
|
+
The change event a publish emits carries `origin: 'inline'` — the same stamp a
|
|
1612
|
+
write made in this process carries. An event that arrives from another replica
|
|
1613
|
+
carries `origin: 'injected'`. So a listener can tell the two apart:
|
|
1614
|
+
|
|
1615
|
+
```ts
|
|
1616
|
+
store.onChange((event) => {
|
|
1617
|
+
if (event.table !== jobQueue.key) return
|
|
1618
|
+
if (event.origin === 'injected') {
|
|
1619
|
+
// a PEER published — mirror it into local state, then publish onward if
|
|
1620
|
+
// this node has anything of its own to add
|
|
1621
|
+
}
|
|
1622
|
+
})
|
|
1623
|
+
```
|
|
1624
|
+
|
|
1625
|
+
That distinction is what makes a fan-out pattern writable. Without it, a
|
|
1626
|
+
replica's own publish and a peer's are indistinguishable, and the only way to
|
|
1627
|
+
avoid an echo is a boolean per channel.
|
|
1628
|
+
|
|
1629
|
+
Two consequences worth knowing:
|
|
1630
|
+
|
|
1631
|
+
- **Re-publishing from inside a change listener works, synchronously.** A
|
|
1632
|
+
channel published while handling a peer's event reaches the other replicas
|
|
1633
|
+
like any other publish. (This is worth stating because it did not use to:
|
|
1634
|
+
`plugin-broadcast` suppressed every emission made while it was injecting, so
|
|
1635
|
+
such a publish woke the local node and silently never left it.)
|
|
1636
|
+
- **`origin` never crosses the wire.** It describes how an event reached *this*
|
|
1637
|
+
process, so the receiving replica always stamps its own. A publish that says
|
|
1638
|
+
`'inline'` here arrives as `'injected'` there, which is the truth on both ends.
|
|
1639
|
+
|
|
1609
1640
|
### What a channel is not
|
|
1610
1641
|
|
|
1611
1642
|
- **Not an event.** [`defineEvent`](/docs/data/subscriptions#streams-are-different)
|
|
@@ -1881,7 +1912,16 @@ So deliveries run **concurrently, up to a bound**. The default is 8 in flight.
|
|
|
1881
1912
|
Measured with 50 subscribers behind a 5 ms guard: 517 ms to serve all of them
|
|
1882
1913
|
serially, 72 ms at 8 lanes.
|
|
1883
1914
|
|
|
1884
|
-
|
|
1915
|
+
Declare it in `app.config.ts`, or override per deployment with the env var —
|
|
1916
|
+
the env wins, because an operator acting on a running system outranks what the
|
|
1917
|
+
project declared:
|
|
1918
|
+
|
|
1919
|
+
```ts
|
|
1920
|
+
export default {
|
|
1921
|
+
type: 'api' as const, name: 'api',
|
|
1922
|
+
reactive: { deliveryConcurrency: 16, rawReadTrackingLimit: 64 },
|
|
1923
|
+
}
|
|
1924
|
+
```
|
|
1885
1925
|
|
|
1886
1926
|
```bash
|
|
1887
1927
|
VOLTRO_REACTIVE_DELIVERY_CONCURRENCY=16 voltro serve
|
|
@@ -2120,6 +2160,17 @@ A `*.event.ts` file is **browser-safe** and may hold several declarations — a
|
|
|
2120
2160
|
|
|
2121
2161
|
**`guards` decide who may listen**, in the same vocabulary a query uses, and they are checked *before* the subscription is registered — a refused client never holds one. The routing key is the guard input, so a resource-scoped guard (`{ scope: 'arena:read', from: 'arenaId' }`) sees which arena was asked for. A refusal reaches the client as the `ScopeError` the rpc declares.
|
|
2122
2162
|
|
|
2163
|
+
**`openAccess: '<reason>'` is the other answer to the same question**, exactly as on a procedure. Under `security.defaultDeny` (the default) an event that declares *neither* `guards:` nor `openAccess:` refuses the boot, by name — an access decision nobody made is a hole, not a default. For an event that really is open — a public scoreboard, a status pulse — declare it and say why; the reason is what a reviewer reads and what `voltro doctor` prints beside the tag. Do not reach for a scope every caller already holds just to satisfy the gate: that guard reads as protection and enforces nothing.
|
|
2164
|
+
|
|
2165
|
+
```ts
|
|
2166
|
+
export const scoreboardTick = defineEvent({
|
|
2167
|
+
name: 'scoreboard.tick',
|
|
2168
|
+
key: Schema.Struct({ arenaId: Schema.String }),
|
|
2169
|
+
payload: Schema.Struct({ score: Schema.Number }),
|
|
2170
|
+
openAccess: 'public scoreboard — carries no caller data',
|
|
2171
|
+
})
|
|
2172
|
+
```
|
|
2173
|
+
|
|
2123
2174
|
**The tenant is not part of the key and must never be added.** It comes from the subject on both sides, so a cross-tenant delivery is impossible by construction rather than by remembering to filter.
|
|
2124
2175
|
|
|
2125
2176
|
The name shares the rpc tag space. Two declarations answering to one name fail the **boot**, not the first delivery.
|
|
@@ -2548,6 +2599,8 @@ Migrating is mechanical: declare the event, replace the insert with `ctx.events.
|
|
|
2548
2599
|
```ts
|
|
2549
2600
|
defineEvent({ name: 'orders paid', … }) // ✗ whitespace — see below
|
|
2550
2601
|
defineEvent({ name: 'orders.paid', guards: [], … }) // ✗ enforces nothing
|
|
2602
|
+
defineEvent({ name: 'x', openAccess: '', … }) // ✗ the reason is the point
|
|
2603
|
+
defineEvent({ name: 'x', openAccess: 'why', guards: [{ scope: 's' }], … }) // ✗ two decisions
|
|
2551
2604
|
defineEvent({ name: 'x', webhook: { rateLimit: { perMinute: 0 } } }) // ✗ never delivers
|
|
2552
2605
|
```
|
|
2553
2606
|
|
|
@@ -2559,7 +2612,11 @@ production only. Use a dot to namespace: `orders.paid`.
|
|
|
2559
2612
|
|
|
2560
2613
|
**`guards: []` is refused** because it reads at the call site as if the event
|
|
2561
2614
|
were protected and enforces nothing — the empty list never reaches the check.
|
|
2562
|
-
|
|
2615
|
+
An event with no decision at all does not boot under `security.defaultDeny`;
|
|
2616
|
+
declare `openAccess: '<why>'` for a deliberately open one. **An empty
|
|
2617
|
+
`openAccess` reason is refused** — the reason is the reviewable half of the
|
|
2618
|
+
decision — and **`openAccess` + `guards` together are refused**: two decisions
|
|
2619
|
+
say the event is protected AND open.
|
|
2563
2620
|
|
|
2564
2621
|
**`rateLimit: { perMinute: 0 }`** defers every delivery forever. There is no
|
|
2565
2622
|
"unlimited" spelling — omit `rateLimit` for no ceiling. **`version: 0`** would
|
|
@@ -887,6 +887,10 @@ A multi-step plan runs inside one `BEGIN ... COMMIT`. Mid-flight failure rolls E
|
|
|
887
887
|
|
|
888
888
|
The advisory-lock variants serialise concurrent applies — two operators running `voltro db apply` against the same DB at the same time go through serially.
|
|
889
889
|
|
|
890
|
+
**The lock is scoped to your configured schema.** With `DB_SCHEMA` set, the lock key (Postgres) / lock name (MySQL, MSSQL) is derived from the schema, so two apps sharing one database in different schemas do not serialise — or defer — each other's migrations and trigger repairs. Without `DB_SCHEMA` (or with `DB_SCHEMA=public`) every instance takes one stable framework-wide key, which is what makes a rolling deploy safe: old and new replicas contend on the same lock. On MySQL/MariaDB `GET_LOCK` is server-wide; setting `DB_SCHEMA` to your database name un-shares the lock between two apps on one server.
|
|
891
|
+
|
|
892
|
+
**Transient DDL failures are retried, per the dialect's own predicate.** A `CREATE TABLE IF NOT EXISTS` that meets SQLite/Turso's schema lock (`database is locked` / `SQLITE_BUSY`), a MySQL lock-wait timeout, or a Postgres/MSSQL deadlock victim during the boot auto-migrate is retried with bounded attempts and exponential backoff instead of failing the boot on the first attempt — only statements that are safe to re-run, and on Postgres/MSSQL as a fresh transaction (their deadlock classes roll the whole transaction back). `VOLTRO_MIGRATION_DDL_RETRIES` moves the retry count (default 4; `0` disables).
|
|
893
|
+
|
|
890
894
|
## MySQL / MariaDB / SQLite / Turso — the resume ledger
|
|
891
895
|
|
|
892
896
|
Every DDL statement implicitly commits. A 5-op plan on MySQL is effectively 5 separate "atomic statements" with the prior ones already committed when a later one fails. If op 5 of 5 fails, ops 1–4 stay applied:
|
|
@@ -3154,6 +3158,48 @@ you would review is the one you just wrote.
|
|
|
3154
3158
|
|
|
3155
3159
|
If the cause is drift (someone DDL'd prod manually), see [drift.md](./drift.md) for reconciliation paths.
|
|
3156
3160
|
|
|
3161
|
+
### If the two processes are the same image: compare their `env:` blocks
|
|
3162
|
+
|
|
3163
|
+
A fingerprint mismatch does not always mean the schema changed. The declared
|
|
3164
|
+
table set is what gets hashed, and until 0.35.0 three RUNTIME flags could move
|
|
3165
|
+
it — so a pre-deploy migrate Job and the pods it feeds could disagree while
|
|
3166
|
+
running identical code against one database.
|
|
3167
|
+
|
|
3168
|
+
That was reported as a green migrate job followed by every pod in
|
|
3169
|
+
CrashLoopBackOff. The chart gave the Job its own `env:` list (`NODE_ENV`,
|
|
3170
|
+
`DB_*`) while `CDC: "0"` lived in the pods' block, because change data capture
|
|
3171
|
+
reads as a runtime concern. Measured on mariadb at `NODE_ENV=production`, each
|
|
3172
|
+
flag flipped alone:
|
|
3173
|
+
|
|
3174
|
+
| flag | effect on the DECLARED set |
|
|
3175
|
+
| --- | --- |
|
|
3176
|
+
| `CDC=0` | removes `_voltro_cdc_offsets` |
|
|
3177
|
+
| `VOLTRO_UNDO=on` | adds `_voltro_undo_log` |
|
|
3178
|
+
| `VOLTRO_TRACING_PERSIST=all` | adds `_voltro_traces` |
|
|
3179
|
+
|
|
3180
|
+
**From 0.35.0 none of them does.** `_voltro_cdc_offsets` follows the DIALECT, so
|
|
3181
|
+
a mariadb/mssql app declares it either way; the other two are declared in
|
|
3182
|
+
`app.config.ts` and the env vars only choose what a process captures:
|
|
3183
|
+
|
|
3184
|
+
```ts
|
|
3185
|
+
export default {
|
|
3186
|
+
// Both default to on outside production. Declare them when you want the
|
|
3187
|
+
// table in a production schema — `VOLTRO_UNDO` / `VOLTRO_TRACING_PERSIST`
|
|
3188
|
+
// alone is no longer enough, because your migrate job does not carry them.
|
|
3189
|
+
schema: { undo: true, traces: true },
|
|
3190
|
+
}
|
|
3191
|
+
```
|
|
3192
|
+
|
|
3193
|
+
Setting a capture flag ON without the declaration now refuses the boot and names
|
|
3194
|
+
the field, rather than writing to a table nobody created.
|
|
3195
|
+
|
|
3196
|
+
The refusal also prints which of the three tables this process decided and from
|
|
3197
|
+
which input, so the comparison is a glance rather than a hash diff. Do compare
|
|
3198
|
+
the two `env:` blocks anyway if you use plugins: a plugin's `extendSchema.tables`
|
|
3199
|
+
is your code and may read anything, which is the one part the framework cannot
|
|
3200
|
+
guarantee for you.
|
|
3201
|
+
|
|
3202
|
+
|
|
3157
3203
|
## "duplicate column name in plan" (logically invalid plan)
|
|
3158
3204
|
|
|
3159
3205
|
```
|
|
@@ -3493,7 +3539,10 @@ Three things about it are worth knowing before you deploy a fleet:
|
|
|
3493
3539
|
`pg_try_advisory_lock` and SKIPS if anything holds it, so N replicas booting
|
|
3494
3540
|
together produce one repairing and N-1 logging `another instance … is
|
|
3495
3541
|
converging it`. A concurrent `voltro db apply` holds the same lock, so the two
|
|
3496
|
-
can never run each other's DDL.
|
|
3542
|
+
can never run each other's DDL. The lock is scoped to your configured
|
|
3543
|
+
`DB_SCHEMA`, so `another instance` really means an instance of YOUR
|
|
3544
|
+
deployment — a second app sharing the database in a different schema takes a
|
|
3545
|
+
different key and neither defers the other.
|
|
3497
3546
|
- **It never fails a boot.** A check that cannot run warns and the process
|
|
3498
3547
|
continues; reactivity may be degraded, and that is still better than a
|
|
3499
3548
|
diagnostic taking the app down.
|