@voltro/cli 0.23.0 → 0.25.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +537 -0
  2. package/THIRD-PARTY-NOTICES.md +2 -2
  3. package/bin/voltro.mjs +39 -4
  4. package/dist/{apiBuild-JQtIhZPy.js → apiBuild-BqhCSytw.js} +46 -24
  5. package/dist/apiBuild-N1R4V792.js +2 -0
  6. package/dist/bin.js +3 -3
  7. package/dist/{commands-Bsu9Buln.js → commands-7EmYJ9Xg.js} +2796 -2478
  8. package/dist/dbCommand-CIrdFLp9.js +2 -0
  9. package/dist/{dbCommand-GI7-BVZt.js → dbCommand-FUU12FvD.js} +285 -246
  10. package/dist/{dev-DRxF_qOB.js → dev-BvHT7WZa.js} +1 -1
  11. package/dist/{dev-jM07kq_D.js → dev-MacSQ1Ll.js} +2042 -1631
  12. package/dist/fileConventions-3bffWssN.js +30 -0
  13. package/dist/frameworkTableAssembly-BsnCKzQ6.js +2 -0
  14. package/dist/{frameworkTableAssembly-sg41s14F.js → frameworkTableAssembly-Cw5zJz6n.js} +2 -2
  15. package/dist/index.js +1 -1
  16. package/dist/inspect-C9gjHwBk.js +2 -0
  17. package/dist/{inspect-_ldwsAwH.js → inspect-DuLUrZp9.js} +14 -1
  18. package/dist/{inspectMetrics-dcC7gnxu.js → inspectMetrics-EQwH7BI4.js} +15 -15
  19. package/dist/manifestBuild-BVwS1Z_6.js +2 -0
  20. package/dist/{manifestBuild-Bs1Uw22_.js → manifestBuild-Dneq4_Jx.js} +1 -1
  21. package/dist/serveCommand-5ZFiNO1R.js +1241 -0
  22. package/dist/serveEntry.js +2 -2
  23. package/dist/{start-C_4VUDT8.js → start-C-ZWSDpg.js} +2 -2
  24. package/dist/startEntry.js +2 -2
  25. package/package.json +17 -17
  26. package/templates/AGENTS.core.md +12 -5
  27. package/templates/AGENTS.md +14 -7
  28. package/templates/agent-docs/_index.md +2 -2
  29. package/templates/agent-docs/_manifest.json +3 -3
  30. package/templates/agent-docs/authentication.md +21 -0
  31. package/templates/agent-docs/cli.md +32 -0
  32. package/templates/agent-docs/data.md +491 -167
  33. package/templates/agent-docs/database/migrations.md +132 -63
  34. package/templates/agent-docs/database/schema.md +80 -0
  35. package/templates/agent-docs/database/seedsdialects.md +12 -0
  36. package/templates/agent-docs/deployment.md +75 -1
  37. package/templates/agent-docs/plugins.md +1 -1
  38. package/templates/agent-docs/reference.md +2 -2
  39. package/templates/agent-docs/security.md +18 -0
  40. package/templates/agent-docs/templates/apibackends.md +5 -5
  41. package/templates/agent-docs/templates/overview.md +1 -1
  42. package/templates/agent-docs/whats-new.md +263 -169
  43. package/templates/agent-docs/workflows.md +74 -20
  44. package/templates/apps/api-ai/package.json +8 -7
  45. package/templates/apps/api-auth/package.json +9 -8
  46. package/templates/apps/api-backend/package.json +8 -7
  47. package/templates/apps/api-backend-deactivation/package.json +8 -7
  48. package/templates/apps/api-backend-mail/package.json +9 -8
  49. package/templates/apps/api-backend-mariadb/package.json +10 -9
  50. package/templates/apps/api-backend-storage/package.json +9 -8
  51. package/templates/apps/api-data-advanced/package.json +9 -8
  52. package/templates/apps/api-durable/README.md +2 -1
  53. package/templates/apps/api-durable/events/orders.event.ts +18 -0
  54. package/templates/apps/api-durable/mutations/orders.place.mutation.server.ts +17 -15
  55. package/templates/apps/api-durable/package.json +9 -8
  56. package/templates/apps/api-durable/triggers/order.placed.trigger.tsx +6 -4
  57. package/templates/apps/api-feature-flags/package.json +10 -9
  58. package/templates/apps/api-governance/package.json +9 -8
  59. package/templates/apps/api-kv/package.json +9 -8
  60. package/templates/apps/api-moderation/package.json +9 -8
  61. package/templates/apps/api-observability/package.json +9 -8
  62. package/templates/apps/api-ratelimit/package.json +9 -8
  63. package/templates/apps/api-rbac/package.json +9 -8
  64. package/templates/apps/api-rest/package.json +8 -7
  65. package/templates/apps/api-saas/package.json +12 -11
  66. package/templates/apps/api-search/package.json +9 -8
  67. package/templates/apps/api-versioning/package.json +9 -8
  68. package/templates/apps/api-webhooks/README.md +2 -2
  69. package/templates/apps/api-webhooks/events/orders.event.ts +35 -0
  70. package/templates/apps/api-webhooks/package.json +10 -9
  71. package/templates/apps/api-webhooks/template.json +1 -1
  72. package/templates/apps/changelog/package.json +7 -6
  73. package/templates/apps/edge-functions/package.json +3 -2
  74. package/templates/apps/frontend-admin/package.json +9 -8
  75. package/templates/apps/frontend-app/package.json +9 -8
  76. package/templates/apps/frontend-blank/package.json +8 -7
  77. package/templates/apps/frontend-contact/package.json +8 -7
  78. package/templates/apps/frontend-contact/src/globals.d.ts +6 -0
  79. package/templates/apps/frontend-dashboard/package.json +8 -7
  80. package/templates/apps/frontend-docs/package.json +8 -7
  81. package/templates/apps/frontend-docs/src/globals.d.ts +6 -0
  82. package/templates/apps/frontend-i18n/package.json +7 -6
  83. package/templates/apps/frontend-landing/package.json +8 -7
  84. package/templates/apps/frontend-landing/src/globals.d.ts +6 -0
  85. package/templates/apps/frontend-spa/package.json +8 -7
  86. package/templates/apps/frontend-spa/src/globals.d.ts +6 -0
  87. package/templates/apps/frontend-ssr/package.json +8 -7
  88. package/templates/apps/frontend-ssr/src/globals.d.ts +6 -0
  89. package/templates/apps/frontend-ssr-api/package.json +9 -8
  90. package/templates/apps/frontend-static-blog/package.json +7 -6
  91. package/templates/apps/frontend-static-blog/src/globals.d.ts +6 -0
  92. package/dist/apiBuild-CD-4JLLA.js +0 -2
  93. package/dist/dbCommand-CSguuUQn.js +0 -2
  94. package/dist/fileConventions-CBNPZVOV.js +0 -29
  95. package/dist/frameworkTableAssembly-DRJrAVr1.js +0 -2
  96. package/dist/inspect-BA67TF6v.js +0 -2
  97. package/dist/manifestBuild-i-fRHg_H.js +0 -2
  98. package/dist/serveCommand-Cl8sicD5.js +0 -1162
  99. package/templates/apps/api-webhooks/events/order.completed.webhook.tsx +0 -22
@@ -42,7 +42,7 @@ const { runId } = await run.run({})
42
42
  | External side effects that need retry/resume semantics | Workflow |
43
43
  | Human approval or webhook callback in the middle of work | Workflow + `awaitSignal` |
44
44
  | External command that must validate and return a result | Workflow + `awaitUpdate` |
45
- | Domain event that should fan out to durable work | `triggerWorkflow(...)` + `ctx.events.emit(...)` |
45
+ | Domain event that should fan out to durable work | `triggerWorkflow(...)` + `ctx.events.publish(...)` |
46
46
  | One-off durable delay inside an operation | Workflow + `sleep` |
47
47
  | Recurring job such as "daily at 3am" | [Schedule](/docs/scheduling/overview) (`*.cron.tsx`) |
48
48
  | Atomic database write | Mutation |
@@ -225,7 +225,7 @@ export default triggerWorkflow<{ userId: string; plan: string }, { userId: strin
225
225
  Then emit the event from a mutation, action, incoming webhook, schedule, or workflow:
226
226
 
227
227
  ```ts
228
- await ctx.events.emit('user.signup', { userId, plan })
228
+ yield* ctx.events.publish(userSignedUp, { userId }, { plan })
229
229
  ```
230
230
 
231
231
  Voltro records the event in `_voltro_workflow_events`, records each trigger delivery in `_voltro_workflow_event_deliveries`, and starts matching workflows with `source: event:<name>`. Emits inside mutations are post-commit safe: if the mutation rolls back, the workflow fan-out is not launched.
@@ -235,7 +235,7 @@ Use `event: '*'` for audit-style wildcard triggers that should see every emitted
235
235
  ## Runtime boundaries
236
236
 
237
237
  - The workflow executor receives an `AppContext` built for the run. Starts from RPC handlers, mutations, actions, schedules, verified incoming handlers, and inspect tooling record a `source` such as `workflow-rpc`, `app-context`, `schedule:<name>`, `incoming:<id>`, or `inspect`. Request starts also carry the starter trace; authenticated request starts carry the resolved subject into the run record and executor context.
238
- - `ctx.events.emit(...)` is available when the app declares workflow event triggers. Triggered workflow starts carry the starter subject/trace and a `source` of `event:<name>`.
238
+ - `ctx.events.publish(...)` is available when the app declares workflow event triggers. Triggered workflow starts carry the starter subject/trace and a `source` of `event:<name>`.
239
239
  - Put tenant/user ids that the workflow must enforce into the payload. That keeps business authorization deterministic across retries, resumes, and future cross-replica handoff.
240
240
  - Child workflows should be started with `ctx.workflows.child(...)`. Parent lineage and parent-close policy are persisted before the child start, so they survive cross-runner execution.
241
241
  - External incoming handlers created with `defineIncomingWebhook(...)` receive `context.workflows`, so a verified webhook can start, signal, or update a workflow after signature and idempotency checks.
@@ -474,6 +474,36 @@ Workflow starts persist the starter subject, trace id, source, parent execution
474
474
 
475
475
  Still include tenant/user ids that the business process must enforce in `payload`, validate them in the first step, and scope store reads/writes deliberately. Payload data is replay-safe and makes authorization decisions auditable across retries and deploys.
476
476
 
477
+ ## Calling an HTTP API from a step
478
+
479
+ The framework's `HttpClient` is available inside a workflow executor — the same one handlers `yield*`, with the same SSRF allowlist and the same automatic `traceparent` propagation. `yield*` it in a step:
480
+
481
+ ```ts
482
+ import { HttpClient, HttpClientRequest } from '@effect/platform'
483
+
484
+ export const executor = defineWorkflowExecutor(syncInvoice, (payload) =>
485
+ Effect.gen(function* () {
486
+ const remote = yield* step({ name: 'fetch-invoice' }, () =>
487
+ Effect.gen(function* () {
488
+ const client = yield* HttpClient.HttpClient
489
+ const res = yield* client.execute(
490
+ HttpClientRequest.get(`https://billing.example.com/invoices/${payload.invoiceId}`),
491
+ )
492
+ return yield* res.json
493
+ }),
494
+ )
495
+ yield* step({ name: 'persist' }, () => database.invoices.update(payload.invoiceId, remote))
496
+ }),
497
+ )
498
+ ```
499
+
500
+ Two things follow from where it sits:
501
+
502
+ - **Wrap the call in a `step`.** The result is then checkpointed, so a retry or a resume after a deploy replays the recorded response instead of calling the remote again. A bare `yield*` outside a step re-issues the request on every replay — which for a payment capture or an email send is the difference between once and several times.
503
+ - **It is the SSRF-guarded client.** Requests to internal targets are refused by the same policy handlers get; a workflow is not a way around it. Configure the allowlist once in `app.config.ts` under `http` — dev and serve read the same key, so the policy cannot differ between them.
504
+
505
+ Reaching for `fetch` instead loses both: no allowlist, no trace propagation, and nothing tying the call to the step that made it.
506
+
477
507
  ## Anti-patterns
478
508
 
479
509
  - **Using `input` in `workflow({...})`.** The current API is `payload`.
@@ -1152,7 +1182,7 @@ Voltro records workflow runs, step attempts, and lifecycle events into framework
1152
1182
  | `_voltro_workflow_runs` | One row per run: `id`, `tag`, `executionId`, `status`, `payload`, `workflowVersion`, `workflowPatches`, `output`, error fields, subject, start source, timing, trace id, parent execution id, parent-close policy. |
1153
1183
  | `_voltro_workflow_run_steps` | One row per step attempt: step name, attempt number, recorded input, retry metadata, output or error, duration. |
1154
1184
  | `_voltro_workflow_run_events` | Lifecycle events: `run-started`, `run-succeeded`, `run-failed`, `run-suspended`, `run-resumed`, `run-cancelled`, `timer-set`, `timer-fired`, `signal-awaited`, `signal-sent`, `signal-received`, `update-requested`, `update-received`, `update-completed`, `update-failed`. |
1155
- | `_voltro_workflow_events` | Domain events emitted through `ctx.events.emit(...)`: event id, name, payload, source, subject, trace id, occurred time. |
1185
+ | `_voltro_workflow_events` | Domain events emitted through `ctx.events.publish(...)`: event id, name, payload, source, subject, trace id, occurred time. |
1156
1186
  | `_voltro_workflow_event_deliveries` | One row per workflow trigger delivery: event id, trigger id, workflow name, execution id, idempotency key, status, error. |
1157
1187
 
1158
1188
  Run status is `running`, `succeeded`, `failed`, `cancelled`, or `suspended`.
@@ -1353,16 +1383,23 @@ Validation runs on the **decoded** value, so a `Schema.Date` payload accepts a
1353
1383
  <!-- source: en/workflows/event-triggers.md -->
1354
1384
  ## Event triggers
1355
1385
 
1356
- _defineEventTrigger — bind a domain event name to a workflow. ctx.events.emit(name, data) fans the event out to every matching trigger with filter, payload-mapping, and idempotency._
1386
+ _defineEventTrigger — bind a domain event name to a workflow. ctx.events.publish(descriptor, key, payload) fans the event out to every matching trigger with filter, payload-mapping, and idempotency._
1357
1387
 
1358
- A workflow is usually kicked off directly (`ctx.workflows.start(...)`). An **event trigger** decouples that: a `*.trigger.tsx` file binds a domain **event name** to a **workflow name**, and any handler that calls `ctx.events.emit('event.name', data)` fans the event out to every trigger listening for it. The emitter never names the workflow — "something happened" is separated from "run this workflow", so you add reactions without touching the code that emits.
1388
+ A workflow is usually kicked off directly (`ctx.workflows.start(...)`). An **event trigger** decouples that: a `*.trigger.tsx` file binds a **declared event** to a **workflow name**, and any handler that publishes that event fans it out to every trigger listening for it. The emitter never names the workflow — "something happened" is separated from "run this workflow", so you add reactions without touching the code that emits.
1359
1389
 
1360
1390
  ```tsx
1391
+ // events/orders.event.ts — the declaration both sides share
1392
+ export const orderPaid = defineEvent({
1393
+ name: 'order.paid',
1394
+ key: Schema.Struct({ orderId: Schema.String }),
1395
+ payload: Schema.Struct({ total: Schema.Number, tenantId: Schema.String }),
1396
+ })
1397
+
1361
1398
  // triggers/orderPaid.trigger.tsx
1362
1399
  import { defineEventTrigger } from '@voltro/runtime'
1363
1400
 
1364
1401
  export default defineEventTrigger({
1365
- event: 'order.paid', // the event name to listen for
1402
+ on: orderPaid, // the declared event to listen for
1366
1403
  workflow: 'fulfilment.run', // the workflow to start
1367
1404
  // Optional: skip when the predicate returns false.
1368
1405
  filter: (e) => e.data.total > 0,
@@ -1377,23 +1414,21 @@ Discovery walks every `*.trigger.tsx`; the `default` export must be a `defineEve
1377
1414
 
1378
1415
  ## Emitting an event
1379
1416
 
1380
- From any handler, emit through `ctx.events`:
1417
+ From any handler with a `ctx`, publish the declared event:
1381
1418
 
1382
1419
  ```ts
1383
- const execute = async (input: { orderId: string }, ctx: AppContext) => {
1420
+ const execute = (input: { orderId: string }, ctx: AppContext) => Effect.gen(function* () {
1384
1421
  // ... mark the order paid ...
1385
- const result = await ctx.events.emit('order.paid', {
1386
- orderId: input.orderId,
1387
- tenantId: ctx.request.subject.tenantId,
1422
+ yield* ctx.events.publish(orderPaid, { orderId: input.orderId }, {
1388
1423
  total: order.total,
1424
+ tenantId: ctx.request.subject.tenantId,
1389
1425
  })
1390
- // result.eventId — the persisted event id
1391
- // result.triggered — one entry per matched trigger:
1392
- // { triggerId, workflowName, status: 'started' | 'skipped' | 'failed', run?, reason? }
1393
- }
1426
+ })
1394
1427
  ```
1395
1428
 
1396
- `emit(name, data, options?)` accepts `{ id?, source? }` (override the event id / source label). It returns `{ eventId, triggered }` as soon as the workflows are kicked off the workflows themselves run durably in the background, exactly like `ctx.workflows.start`.
1429
+ `publish` reaches BOTH audiences from one call: the durable one (this page — the event log plus every matching trigger) and the ephemeral one ([connected clients](/docs/data/events)). Inside a mutation both fire on COMMIT and neither on rollback, so a client and a triggered workflow cannot disagree about whether the thing happened.
1430
+
1431
+ `on:` reads the event NAME off the descriptor, so renaming the event moves the trigger with it. The older string form (`event: 'order.paid'`) was removed in 0.25.0 — with a string, a rename left the trigger matching nothing and the workflow simply never ran again, without an error.
1397
1432
 
1398
1433
  The event envelope every `filter` / `payload` / `idempotencyKey` receives is:
1399
1434
 
@@ -1409,11 +1444,30 @@ The event envelope every `filter` / `payload` / `idempotencyKey` receives is:
1409
1444
  - **Idempotency.** Before starting, the runtime checks for an existing delivery with the same `idempotencyKey` (default `<eventId>:<triggerId>`) — a duplicate is recorded as `status: 'skipped', reason: 'duplicate'` and the workflow is not started twice.
1410
1445
  - **Every emit is audited.** The event itself lands in `_voltro_workflow_events`; one row per trigger lands in `_voltro_workflow_event_deliveries` with `status` (`starting → started`, or `skipped` / `failed`), the started `executionId`, and any error message. Both tables are reactive — the dashboard's Workflows view surfaces deliveries live.
1411
1446
 
1447
+ ## Retention — and why the delivery TTL is not just housekeeping
1448
+
1449
+ Both audit tables are append-only, so both are swept on a **30-day** default by the
1450
+ boot retention GC (postgres): `VOLTRO_WORKFLOW_EVENTS_TTL_HOURS` and
1451
+ `VOLTRO_WORKFLOW_EVENT_DELIVERIES_TTL_HOURS`. The delivery log grows faster — one
1452
+ row per *trigger*, so three triggers on one event write four rows per emit.
1453
+
1454
+ **The delivery TTL is the deduplication window.** The idempotency check above looks
1455
+ for an existing delivery row, so once a row is swept its key is no longer
1456
+ deduplicated. With the default key (`<eventId>:<triggerId>`, and `eventId` is fresh
1457
+ per emit) a duplicate cannot occur and this costs nothing. It matters only when you
1458
+ supply your own `idempotencyKey`: if your app can re-emit the *same* stable key
1459
+ (`fulfil:order-123`) more than 30 days apart and must still be deduplicated, raise
1460
+ `VOLTRO_WORKFLOW_EVENT_DELIVERIES_TTL_HOURS` past that horizon.
1461
+
1462
+ Unlike `_voltro_outbox`, the delivery log is **not** status-filtered — a stale
1463
+ `starting` row has no requeue path and no reader, so keeping it would leave the
1464
+ table unbounded for exactly the rows a crash produces.
1465
+
1412
1466
  ## When to use it
1413
1467
 
1414
1468
  Reach for an event trigger when ONE thing happening should fan out to several independent reactions, or when you want the emitter to stay ignorant of the consumers:
1415
1469
 
1416
- - `order.paid` → start fulfilment AND a receipt-email workflow AND an analytics rollup — three triggers, one `emit`.
1417
- - A mutation emits `user.signedUp`; an onboarding workflow trigger starts the drip sequence. The signup mutation never imports the onboarding workflow.
1470
+ - `order.paid` → start fulfilment AND a receipt-email workflow AND an analytics rollup — three triggers, one `publish`.
1471
+ - A mutation publishes `user.signedUp`; an onboarding workflow trigger starts the drip sequence. The signup mutation never imports the onboarding workflow.
1418
1472
 
1419
- If the handler already knows exactly which workflow to run and there's only one, call [`ctx.workflows.start(...)`](/docs/workflows/definition) directly — the trigger indirection only pays off when you're decoupling emit from react, or fanning one event out to many.
1473
+ If the handler already knows exactly which workflow to run and there's only one, call [`ctx.workflows.start(...)`](/docs/workflows/definition) directly — the trigger indirection only pays off when you're decoupling publish from react, or fanning one event out to many.
@@ -5,22 +5,23 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "voltro dev .",
8
+ "lint": "voltro doctor .",
8
9
  "test": "voltro test",
9
10
  "typecheck": "tsc --noEmit"
10
11
  },
11
12
  "dependencies": {
12
13
  "@effect/platform": "^0.97.0",
13
14
  "@effect/rpc": "^0.76.0",
14
- "@voltro/ai": "0.23.0",
15
- "@voltro/cli": "0.23.0",
16
- "@voltro/database": "0.23.0",
17
- "@voltro/env": "0.23.0",
18
- "@voltro/protocol": "0.23.0",
19
- "@voltro/runtime": "0.23.0",
15
+ "@voltro/ai": "0.25.0",
16
+ "@voltro/cli": "0.25.0",
17
+ "@voltro/database": "0.25.0",
18
+ "@voltro/env": "0.25.0",
19
+ "@voltro/protocol": "0.25.0",
20
+ "@voltro/runtime": "0.25.0",
20
21
  "effect": "^3.22.0"
21
22
  },
22
23
  "devDependencies": {
23
- "@voltro/testing": "0.23.0",
24
+ "@voltro/testing": "0.25.0",
24
25
  "typescript": "^6.0.3",
25
26
  "vitest": "^4.1.10"
26
27
  }
@@ -5,6 +5,7 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "voltro dev .",
8
+ "lint": "voltro doctor .",
8
9
  "migrate": "voltro migrate",
9
10
  "test": "voltro test",
10
11
  "typecheck": "tsc --noEmit"
@@ -12,17 +13,17 @@
12
13
  "dependencies": {
13
14
  "@effect/platform": "^0.97.0",
14
15
  "@effect/rpc": "^0.76.0",
15
- "@voltro/cli": "0.23.0",
16
- "@voltro/database": "0.23.0",
17
- "@voltro/env": "0.23.0",
18
- "@voltro/plugin-auth": "0.23.0",
19
- "@voltro/protocol": "0.23.0",
20
- "@voltro/runtime": "0.23.0",
21
- "@voltro/sql-postgres": "0.23.0",
16
+ "@voltro/cli": "0.25.0",
17
+ "@voltro/database": "0.25.0",
18
+ "@voltro/env": "0.25.0",
19
+ "@voltro/plugin-auth": "0.25.0",
20
+ "@voltro/protocol": "0.25.0",
21
+ "@voltro/runtime": "0.25.0",
22
+ "@voltro/sql-postgres": "0.25.0",
22
23
  "effect": "^3.22.0"
23
24
  },
24
25
  "devDependencies": {
25
- "@voltro/testing": "0.23.0",
26
+ "@voltro/testing": "0.25.0",
26
27
  "typescript": "^6.0.3",
27
28
  "vitest": "^4.1.10"
28
29
  }
@@ -5,6 +5,7 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "voltro dev .",
8
+ "lint": "voltro doctor .",
8
9
  "migrate": "voltro migrate",
9
10
  "test": "voltro test",
10
11
  "typecheck": "tsc --noEmit"
@@ -12,16 +13,16 @@
12
13
  "dependencies": {
13
14
  "@effect/platform": "^0.97.0",
14
15
  "@effect/rpc": "^0.76.0",
15
- "@voltro/cli": "0.23.0",
16
- "@voltro/database": "0.23.0",
17
- "@voltro/env": "0.23.0",
18
- "@voltro/plugin-multitenancy": "0.23.0",
19
- "@voltro/protocol": "0.23.0",
20
- "@voltro/runtime": "0.23.0",
16
+ "@voltro/cli": "0.25.0",
17
+ "@voltro/database": "0.25.0",
18
+ "@voltro/env": "0.25.0",
19
+ "@voltro/plugin-multitenancy": "0.25.0",
20
+ "@voltro/protocol": "0.25.0",
21
+ "@voltro/runtime": "0.25.0",
21
22
  "effect": "^3.22.0"
22
23
  },
23
24
  "devDependencies": {
24
- "@voltro/testing": "0.23.0",
25
+ "@voltro/testing": "0.25.0",
25
26
  "typescript": "^6.0.3",
26
27
  "vitest": "^4.1.10"
27
28
  }
@@ -5,6 +5,7 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "voltro dev .",
8
+ "lint": "voltro doctor .",
8
9
  "migrate": "voltro migrate",
9
10
  "test": "voltro test",
10
11
  "typecheck": "tsc --noEmit"
@@ -12,16 +13,16 @@
12
13
  "dependencies": {
13
14
  "@effect/platform": "^0.97.0",
14
15
  "@effect/rpc": "^0.76.0",
15
- "@voltro/cli": "0.23.0",
16
- "@voltro/database": "0.23.0",
17
- "@voltro/env": "0.23.0",
18
- "@voltro/plugin-deactivation": "0.23.0",
19
- "@voltro/protocol": "0.23.0",
20
- "@voltro/runtime": "0.23.0",
16
+ "@voltro/cli": "0.25.0",
17
+ "@voltro/database": "0.25.0",
18
+ "@voltro/env": "0.25.0",
19
+ "@voltro/plugin-deactivation": "0.25.0",
20
+ "@voltro/protocol": "0.25.0",
21
+ "@voltro/runtime": "0.25.0",
21
22
  "effect": "^3.22.0"
22
23
  },
23
24
  "devDependencies": {
24
- "@voltro/testing": "0.23.0",
25
+ "@voltro/testing": "0.25.0",
25
26
  "typescript": "^6.0.3",
26
27
  "vitest": "^4.1.10"
27
28
  }
@@ -5,6 +5,7 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "voltro dev .",
8
+ "lint": "voltro doctor .",
8
9
  "migrate": "voltro migrate",
9
10
  "typecheck": "tsc --noEmit",
10
11
  "test": "voltro test"
@@ -12,18 +13,18 @@
12
13
  "dependencies": {
13
14
  "@react-email/components": "^1.0.12",
14
15
  "@react-email/render": "^1.4.0",
15
- "@voltro/cli": "0.23.0",
16
- "@voltro/database": "0.23.0",
17
- "@voltro/env": "0.23.0",
18
- "@voltro/plugin-mail": "0.23.0",
19
- "@voltro/plugin-multitenancy": "0.23.0",
20
- "@voltro/protocol": "0.23.0",
21
- "@voltro/runtime": "0.23.0",
16
+ "@voltro/cli": "0.25.0",
17
+ "@voltro/database": "0.25.0",
18
+ "@voltro/env": "0.25.0",
19
+ "@voltro/plugin-mail": "0.25.0",
20
+ "@voltro/plugin-multitenancy": "0.25.0",
21
+ "@voltro/protocol": "0.25.0",
22
+ "@voltro/runtime": "0.25.0",
22
23
  "effect": "^3.22.0",
23
24
  "react": "^19.0.0"
24
25
  },
25
26
  "devDependencies": {
26
- "@voltro/testing": "0.23.0",
27
+ "@voltro/testing": "0.25.0",
27
28
  "typescript": "^6.0.3",
28
29
  "vitest": "^4.1.10"
29
30
  }
@@ -5,6 +5,7 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "voltro dev .",
8
+ "lint": "voltro doctor .",
8
9
  "migrate": "voltro migrate",
9
10
  "test": "voltro test",
10
11
  "typecheck": "tsc --noEmit"
@@ -12,18 +13,18 @@
12
13
  "dependencies": {
13
14
  "@effect/platform": "^0.97.0",
14
15
  "@effect/rpc": "^0.76.0",
15
- "@voltro/cli": "0.23.0",
16
- "@voltro/database": "0.23.0",
17
- "@voltro/env": "0.23.0",
18
- "@voltro/plugin-multitenancy": "0.23.0",
19
- "@voltro/plugin-storage": "0.23.0",
20
- "@voltro/protocol": "0.23.0",
21
- "@voltro/runtime": "0.23.0",
22
- "@voltro/sql-mysql": "0.23.0",
16
+ "@voltro/cli": "0.25.0",
17
+ "@voltro/database": "0.25.0",
18
+ "@voltro/env": "0.25.0",
19
+ "@voltro/plugin-multitenancy": "0.25.0",
20
+ "@voltro/plugin-storage": "0.25.0",
21
+ "@voltro/protocol": "0.25.0",
22
+ "@voltro/runtime": "0.25.0",
23
+ "@voltro/sql-mysql": "0.25.0",
23
24
  "effect": "^3.22.0"
24
25
  },
25
26
  "devDependencies": {
26
- "@voltro/testing": "0.23.0",
27
+ "@voltro/testing": "0.25.0",
27
28
  "typescript": "^6.0.3",
28
29
  "vitest": "^4.1.10"
29
30
  }
@@ -5,22 +5,23 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "voltro dev .",
8
+ "lint": "voltro doctor .",
8
9
  "migrate": "voltro migrate",
9
10
  "typecheck": "tsc --noEmit",
10
11
  "test": "voltro test"
11
12
  },
12
13
  "dependencies": {
13
- "@voltro/cli": "0.23.0",
14
- "@voltro/database": "0.23.0",
15
- "@voltro/env": "0.23.0",
16
- "@voltro/plugin-multitenancy": "0.23.0",
17
- "@voltro/plugin-storage": "0.23.0",
18
- "@voltro/protocol": "0.23.0",
19
- "@voltro/runtime": "0.23.0",
14
+ "@voltro/cli": "0.25.0",
15
+ "@voltro/database": "0.25.0",
16
+ "@voltro/env": "0.25.0",
17
+ "@voltro/plugin-multitenancy": "0.25.0",
18
+ "@voltro/plugin-storage": "0.25.0",
19
+ "@voltro/protocol": "0.25.0",
20
+ "@voltro/runtime": "0.25.0",
20
21
  "effect": "^3.22.0"
21
22
  },
22
23
  "devDependencies": {
23
- "@voltro/testing": "0.23.0",
24
+ "@voltro/testing": "0.25.0",
24
25
  "typescript": "^6.0.3",
25
26
  "vitest": "^4.1.10"
26
27
  }
@@ -5,23 +5,24 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "voltro dev .",
8
+ "lint": "voltro doctor .",
8
9
  "test": "voltro test",
9
10
  "typecheck": "tsc --noEmit"
10
11
  },
11
12
  "dependencies": {
12
13
  "@effect/platform": "^0.97.0",
13
14
  "@effect/rpc": "^0.76.0",
14
- "@voltro/cli": "0.23.0",
15
- "@voltro/database": "0.23.0",
16
- "@voltro/env": "0.23.0",
17
- "@voltro/plugin-governance": "0.23.0",
18
- "@voltro/plugin-multitenancy": "0.23.0",
19
- "@voltro/protocol": "0.23.0",
20
- "@voltro/runtime": "0.23.0",
15
+ "@voltro/cli": "0.25.0",
16
+ "@voltro/database": "0.25.0",
17
+ "@voltro/env": "0.25.0",
18
+ "@voltro/plugin-governance": "0.25.0",
19
+ "@voltro/plugin-multitenancy": "0.25.0",
20
+ "@voltro/protocol": "0.25.0",
21
+ "@voltro/runtime": "0.25.0",
21
22
  "effect": "^3.22.0"
22
23
  },
23
24
  "devDependencies": {
24
- "@voltro/testing": "0.23.0",
25
+ "@voltro/testing": "0.25.0",
25
26
  "typescript": "^6.0.3",
26
27
  "vitest": "^4.1.10"
27
28
  }
@@ -28,7 +28,8 @@ state + multi-instance cluster workflow runners.
28
28
  |---|---|---|
29
29
  | `app.config.ts` | app config | `store: 'memory'`, zero-infra boot. |
30
30
  | `database/schema.ts` | schema | core `actors`/`tenants` + an `orders` table (`tenant()` + `.reactive()`). |
31
- | `mutations/orders.place.mutation.*` | mutation | insert an order, then **emit `order.placed`** post-commit via `ctx.events.emit`. Tenant write-guard. |
31
+ | `mutations/orders.place.mutation.*` | mutation | insert an order and **publish `order.placed`** on commit via `ctx.events.publish`. Tenant write-guard. |
32
+ | `events/orders.event.ts` | event | the declared `order.placed` — one value shared by the publisher, the trigger and any client. |
32
33
  | `triggers/order.placed.trigger.tsx` | event trigger | maps event `order.placed` → **start** the `orders.fulfill` workflow. |
33
34
  | `workflows/order.fulfill.workflow.*` | durable workflow | `step('reserve-stock')` (with **`withCompensation`** saga rollback) → `sleep` → **`awaitSignal('approval')`** human gate → `step('ship')` / mark rejected. |
34
35
  | `mutations/orders.approve.mutation.*` | mutation | inject the `approval` signal into the parked run via `ctx.workflows.signal(...)`. |
@@ -0,0 +1,18 @@
1
+ // The declared domain event. Browser-safe: the SAME value is imported by the
2
+ // mutation that publishes it, the trigger that reacts to it, and any client that
3
+ // wants to watch it live — which is what makes the payload type identical
4
+ // everywhere and a rename impossible to half-apply.
5
+
6
+ import { defineEvent } from '@voltro/protocol'
7
+ import { Schema } from 'effect'
8
+
9
+ export const orderPlaced = defineEvent({
10
+ name: 'order.placed',
11
+ // The ROUTING key: which subscribers receive this. Only what decides
12
+ // delivery belongs here — the tenant comes from the subject, never the key.
13
+ key: Schema.Struct({ orderId: Schema.String }),
14
+ payload: Schema.Struct({
15
+ orderId: Schema.String,
16
+ tenantId: Schema.String,
17
+ }),
18
+ })
@@ -1,34 +1,36 @@
1
1
  // `orders.place` — EXECUTOR (server-only, default export).
2
2
  //
3
- // Inserts the order in the mutation transaction, then emits a domain
4
- // event AFTER commit. `ctx.events.emit(...)` is post-commit safe: the
5
- // event is recorded + the `order.placed` trigger fans out only once the
6
- // row is durably written, so a rollback never starts a fulfillment
7
- // workflow for an order that doesn't exist.
3
+ // Inserts the order and publishes a declared domain event in the SAME
4
+ // transaction. `ctx.events.publish(...)` fires on COMMIT and not at all on
5
+ // rollback, so a rolled-back order can neither start a fulfillment workflow nor
6
+ // tell a connected client that it exists you do not have to sequence the two
7
+ // by hand.
8
8
 
9
9
  import { assertOwnTenant } from '@voltro/plugin-multitenancy/guard'
10
+ import { Effect } from 'effect'
10
11
  import type { AppContext } from '@voltro/runtime'
12
+ import { orderPlaced } from '../events/orders.event'
11
13
 
12
- const execute = async (
14
+ const execute = (
13
15
  input: { tenantId: string; customerName: string; amountCents: number },
14
16
  ctx: AppContext,
15
- ) => {
17
+ ) => Effect.gen(function* () {
16
18
  // Cross-tenant write guard — reject a caller spoofing input.tenantId.
17
19
  assertOwnTenant(input.tenantId, ctx.request.subject)
18
20
 
19
21
  // Framework auto-injects an `order_…` id + the tenant/audit columns.
20
- const row = await ctx.store.insert('orders', {
22
+ const row = yield* Effect.promise(() => ctx.store.insert('orders', {
21
23
  status: 'placed',
22
24
  customerName: input.customerName,
23
25
  amountCents: input.amountCents,
24
26
  tenantId: input.tenantId,
25
- })
27
+ }))
26
28
 
27
- // Post-commit fan-out the `order.placed` trigger starts the
28
- // `orders.fulfill` workflow. `ctx.events` is only present when the
29
- // runtime wired the events facade (it always is under `voltro dev`);
30
- // guard so a unit-test ctx without it doesn't throw.
31
- await ctx.events?.emit('order.placed', {
29
+ // One publish, both audiences: the `order.placed` trigger starts the
30
+ // `orders.fulfill` workflow, and any client watching this order sees it live.
31
+ // Both on the commit boundary they cannot disagree about whether it
32
+ // happened.
33
+ yield* ctx.events!.publish(orderPlaced, { orderId: row['id'] as string }, {
32
34
  orderId: row['id'] as string,
33
35
  tenantId: input.tenantId,
34
36
  })
@@ -40,6 +42,6 @@ const execute = async (
40
42
  amountCents: row['amountCents'] as number,
41
43
  tenantId: row['tenantId'] as string,
42
44
  }
43
- }
45
+ })
44
46
 
45
47
  export default execute
@@ -5,23 +5,24 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "voltro dev .",
8
+ "lint": "voltro doctor .",
8
9
  "test": "voltro test",
9
10
  "typecheck": "tsc --noEmit"
10
11
  },
11
12
  "dependencies": {
12
13
  "@effect/platform": "^0.97.0",
13
14
  "@effect/rpc": "^0.76.0",
14
- "@voltro/cli": "0.23.0",
15
- "@voltro/database": "0.23.0",
16
- "@voltro/env": "0.23.0",
17
- "@voltro/plugin-multitenancy": "0.23.0",
18
- "@voltro/protocol": "0.23.0",
19
- "@voltro/runtime": "0.23.0",
20
- "@voltro/workflow": "0.23.0",
15
+ "@voltro/cli": "0.25.0",
16
+ "@voltro/database": "0.25.0",
17
+ "@voltro/env": "0.25.0",
18
+ "@voltro/plugin-multitenancy": "0.25.0",
19
+ "@voltro/protocol": "0.25.0",
20
+ "@voltro/runtime": "0.25.0",
21
+ "@voltro/workflow": "0.25.0",
21
22
  "effect": "^3.22.0"
22
23
  },
23
24
  "devDependencies": {
24
- "@voltro/testing": "0.23.0",
25
+ "@voltro/testing": "0.25.0",
25
26
  "typescript": "^6.0.3",
26
27
  "vitest": "^4.1.10"
27
28
  }
@@ -1,9 +1,10 @@
1
1
  // Event → workflow trigger. Auto-discovered (`*.trigger.tsx`); no
2
2
  // app.config wiring needed.
3
3
  //
4
- // When any handler emits `order.placed` (the `orders.place` mutation
5
- // does, post-commit), this trigger starts the `orders.fulfill`
6
- // workflow. The event payload IS the workflow payload here (both are
4
+ // When any handler publishes `order.placed` (the `orders.place` mutation does,
5
+ // on commit), this trigger starts the `orders.fulfill` workflow. It binds the
6
+ // DECLARED event rather than its name, so renaming the event moves this file
7
+ // with it instead of silently matching nothing. The event payload IS the workflow payload here (both are
7
8
  // `{ orderId, tenantId }`), so no `payload` mapper is needed — pass one
8
9
  // when the shapes differ.
9
10
  //
@@ -11,11 +12,12 @@
11
12
  // (crash + replay), the matching key short-circuits the second start.
12
13
 
13
14
  import { defineEventTrigger } from '@voltro/runtime'
15
+ import { orderPlaced } from '../events/orders.event'
14
16
 
15
17
  export default defineEventTrigger<
16
18
  { orderId: string; tenantId: string }
17
19
  >({
18
- event: 'order.placed',
20
+ on: orderPlaced,
19
21
  workflow: 'orders.fulfill',
20
22
  idempotencyKey: (event) => `order.placed:${event.data.orderId}`,
21
23
  })
@@ -5,6 +5,7 @@
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "voltro dev .",
8
+ "lint": "voltro doctor .",
8
9
  "migrate": "voltro migrate",
9
10
  "test": "voltro test",
10
11
  "typecheck": "tsc --noEmit"
@@ -12,18 +13,18 @@
12
13
  "dependencies": {
13
14
  "@effect/platform": "^0.97.0",
14
15
  "@effect/rpc": "^0.76.0",
15
- "@voltro/cli": "0.23.0",
16
- "@voltro/database": "0.23.0",
17
- "@voltro/env": "0.23.0",
18
- "@voltro/plugin-flags": "0.23.0",
19
- "@voltro/plugin-multitenancy": "0.23.0",
20
- "@voltro/protocol": "0.23.0",
21
- "@voltro/runtime": "0.23.0",
22
- "@voltro/sql-postgres": "0.23.0",
16
+ "@voltro/cli": "0.25.0",
17
+ "@voltro/database": "0.25.0",
18
+ "@voltro/env": "0.25.0",
19
+ "@voltro/plugin-flags": "0.25.0",
20
+ "@voltro/plugin-multitenancy": "0.25.0",
21
+ "@voltro/protocol": "0.25.0",
22
+ "@voltro/runtime": "0.25.0",
23
+ "@voltro/sql-postgres": "0.25.0",
23
24
  "effect": "^3.22.0"
24
25
  },
25
26
  "devDependencies": {
26
- "@voltro/testing": "0.23.0",
27
+ "@voltro/testing": "0.25.0",
27
28
  "typescript": "^6.0.3",
28
29
  "vitest": "^4.1.10"
29
30
  }