@voltro/cli 0.24.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.
- package/CHANGELOG.md +426 -0
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/dist/{apiBuild-B2m4XK_8.js → apiBuild-BqhCSytw.js} +15 -15
- package/dist/apiBuild-N1R4V792.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/{commands-CaIhTsC2.js → commands-7EmYJ9Xg.js} +1774 -1577
- package/dist/dbCommand-CIrdFLp9.js +2 -0
- package/dist/{dbCommand-CC61CsAc.js → dbCommand-FUU12FvD.js} +265 -256
- package/dist/{dev-DPkQVUTP.js → dev-BvHT7WZa.js} +1 -1
- package/dist/{dev-NCuEhNxs.js → dev-MacSQ1Ll.js} +2019 -1626
- package/dist/fileConventions-3bffWssN.js +30 -0
- package/dist/frameworkTableAssembly-BsnCKzQ6.js +2 -0
- package/dist/{frameworkTableAssembly-sg41s14F.js → frameworkTableAssembly-Cw5zJz6n.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/inspect-C9gjHwBk.js +2 -0
- package/dist/{inspect-_ldwsAwH.js → inspect-DuLUrZp9.js} +14 -1
- package/dist/{inspectMetrics-dcC7gnxu.js → inspectMetrics-EQwH7BI4.js} +15 -15
- package/dist/manifestBuild-BVwS1Z_6.js +2 -0
- package/dist/{manifestBuild-Bs1Uw22_.js → manifestBuild-Dneq4_Jx.js} +1 -1
- package/dist/serveCommand-5ZFiNO1R.js +1241 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-C_4VUDT8.js → start-C-ZWSDpg.js} +2 -2
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.core.md +12 -5
- package/templates/AGENTS.md +14 -7
- package/templates/agent-docs/_index.md +2 -2
- package/templates/agent-docs/_manifest.json +3 -3
- package/templates/agent-docs/cli.md +15 -0
- package/templates/agent-docs/data.md +306 -2
- package/templates/agent-docs/database/migrations.md +40 -0
- package/templates/agent-docs/database/schema.md +80 -0
- package/templates/agent-docs/deployment.md +9 -0
- package/templates/agent-docs/plugins.md +1 -1
- package/templates/agent-docs/reference.md +2 -2
- package/templates/agent-docs/security.md +18 -0
- package/templates/agent-docs/templates/apibackends.md +5 -5
- package/templates/agent-docs/templates/overview.md +1 -1
- package/templates/agent-docs/whats-new.md +263 -169
- package/templates/agent-docs/workflows.md +44 -20
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/README.md +2 -1
- package/templates/apps/api-durable/events/orders.event.ts +18 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.server.ts +17 -15
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-durable/triggers/order.placed.trigger.tsx +6 -4
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/README.md +2 -2
- package/templates/apps/api-webhooks/events/orders.event.ts +35 -0
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/api-webhooks/template.json +1 -1
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/dist/apiBuild-UEM3QBke.js +0 -2
- package/dist/dbCommand-Cr__4ATv.js +0 -2
- package/dist/fileConventions-CBNPZVOV.js +0 -29
- package/dist/frameworkTableAssembly-DRJrAVr1.js +0 -2
- package/dist/inspect-BA67TF6v.js +0 -2
- package/dist/manifestBuild-i-fRHg_H.js +0 -2
- package/dist/serveCommand-C4gzDZzc.js +0 -1175
- package/templates/apps/api-webhooks/events/order.completed.webhook.tsx +0 -22
|
@@ -629,6 +629,86 @@ A mixin can also declare `requires: [otherMixin()]` (resolved depth-first) and `
|
|
|
629
629
|
|
|
630
630
|
|
|
631
631
|
|
|
632
|
+
---
|
|
633
|
+
|
|
634
|
+
<!-- source: en/database/concurrency-and-expiry.md -->
|
|
635
|
+
## Concurrency + expiry
|
|
636
|
+
|
|
637
|
+
_.version() for optimistic locking and expires() for time-limited rows — what each guarantees, on which dialect._
|
|
638
|
+
|
|
639
|
+
Two column-level tools for questions a schema cannot otherwise answer: **which write is newest**, and **when does this row stop counting**.
|
|
640
|
+
|
|
641
|
+
## `.version()` — optimistic locking
|
|
642
|
+
|
|
643
|
+
Two clients read the same row and both write it. Without a version the second silently wins, and the first user's change is gone with no trace. That is the shape of every *"my edit disappeared"* report.
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
export const documents = table('documents', {
|
|
647
|
+
id: id(),
|
|
648
|
+
title: text(),
|
|
649
|
+
version: integer().version(),
|
|
650
|
+
})
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
From then on the store **increments** `version` on every update, and an update that carries an expectation fails when the row has moved on:
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
// The client sends the version it read.
|
|
657
|
+
yield* ctx.store.update('documents', input.id, { title: input.title, version: input.version })
|
|
658
|
+
// → VersionConflict { expected: 3, actual: 7 } when four writes landed in between
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
`VersionConflict` is a typed error, so it reaches the client typed and a UI can offer *reload and re-apply* instead of showing a crash. It carries **both** numbers, because "someone else changed it" is not actionable while "you had 3, it is now 7" is.
|
|
662
|
+
|
|
663
|
+
**Why not `updatedAt`.** A timestamp cannot do this job. Two writes in the same millisecond are indistinguishable, and across replicas the clocks disagree — a comparison that looks correct in a test loses rows under load. An integer the database owns is totally ordered and needs no clock. (`.version()` therefore rejects a `text()` or `timestamp()` column at declaration.)
|
|
664
|
+
|
|
665
|
+
**What it does not do.** It is not a history — it records *that* a row changed, not what to; use [`plugin-versioning`](/docs/plugins/versioning) for that. It is not a lock: a conflict is **reported**, never queued or merged, because merging two intents is a decision only your application can make. And it is not a retry — "re-apply my change on top of theirs" is correct for some changes and wrong for others, so you write it.
|
|
666
|
+
|
|
667
|
+
**Three details worth knowing:**
|
|
668
|
+
|
|
669
|
+
- The version a caller sends is an **expectation, never a write**. It is stripped from the patch, so a client cannot pin its own version and win every race.
|
|
670
|
+
- An update with **no** expectation is still last-write-wins — the default does not change — but the version **still advances**. A version that moved only for careful writers would be worse than none: it would sit still while a careless write changed the row.
|
|
671
|
+
- A row **deleted** underneath you is a conflict too, with `actual: null`. That is how you tell "deleted" from "changed".
|
|
672
|
+
|
|
673
|
+
## `expires()` — a row with an end date
|
|
674
|
+
|
|
675
|
+
```ts
|
|
676
|
+
export const inviteLinks = table('inviteLinks', {
|
|
677
|
+
id: id(),
|
|
678
|
+
email: text(),
|
|
679
|
+
}).with(expires())
|
|
680
|
+
|
|
681
|
+
await ctx.store.insert('inviteLinks', {
|
|
682
|
+
email,
|
|
683
|
+
expiresAt: new Date(Date.now() + 24 * 3_600_000),
|
|
684
|
+
})
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
After that instant the row is **not returned by reads**. `expiresAt` is nullable and null means *never*, so adding the mixin to an existing table does not make its rows vanish.
|
|
688
|
+
|
|
689
|
+
Opt out for a deliberate read — an admin view, a grace-period check:
|
|
690
|
+
|
|
691
|
+
```ts
|
|
692
|
+
ctx.store.select('inviteLinks').includeExpired()
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### Read this before you rely on it
|
|
696
|
+
|
|
697
|
+
**Visibility and storage are two different guarantees, and only one of them holds everywhere.**
|
|
698
|
+
|
|
699
|
+
| | where | when |
|
|
700
|
+
| --- | --- | --- |
|
|
701
|
+
| **Invisible to reads** | every dialect | immediately, the instant it passes |
|
|
702
|
+
| **Physically deleted** | postgres only | eventually, by the retention sweep |
|
|
703
|
+
|
|
704
|
+
So an expired row is **invisible everywhere and still present in the database on four of five dialects**. That is the right trade — making visibility depend on the sweep would mean a row that vanished on postgres and kept serving on MariaDB — but it is a surprising one, and it matters: *do not treat an expired row as unreachable*. If the value must actually be gone, delete it, or do not store it in a row at all.
|
|
705
|
+
|
|
706
|
+
## See also
|
|
707
|
+
|
|
708
|
+
[Column types](/docs/database/columns) · [Mutations](/docs/data/mutations) · [Soft delete](/docs/plugins/soft-delete)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
|
|
632
712
|
---
|
|
633
713
|
|
|
634
714
|
<!-- source: en/database/ids/index.md -->
|
|
@@ -956,6 +956,15 @@ Renaming `orders.total` → `orders.amount` under zero downtime is: add `amount`
|
|
|
956
956
|
(expand) → backfill + dual-write → cut reads over → drop `total` (contract) — three
|
|
957
957
|
releases, never one, so no in-flight pod ever references a column that isn't there.
|
|
958
958
|
|
|
959
|
+
**Make it a hard gate if you always rolling-deploy — `VOLTRO_ROLLING_DEPLOY=1`.**
|
|
960
|
+
The `db plan` ⚠ is advisory by default, because a maintenance-window / scale-to-zero
|
|
961
|
+
deploy has no overlap window and the framework can't tell which you run. If your
|
|
962
|
+
pipeline is *always* a rolling update, set `VOLTRO_ROLLING_DEPLOY=1` in the migration
|
|
963
|
+
Job's env: `voltro db apply` then **refuses** (exit 2) a plan containing a
|
|
964
|
+
rolling-unsafe operation instead of warning, so an un-split breaking change fails the
|
|
965
|
+
deploy rather than breaking pods at runtime. Override a specific apply with `--force`.
|
|
966
|
+
Unset (the default) leaves today's advisory behaviour untouched.
|
|
967
|
+
|
|
959
968
|
### If you DO run `voltro dev` in a cluster (dev / staging only)
|
|
960
969
|
|
|
961
970
|
`voltro dev` binds a small **boot-health surface** on its own port so a probe can
|
|
@@ -70,7 +70,7 @@ Status legend: ✓ shipped · ◐ partial · — planned.
|
|
|
70
70
|
| `@voltro/plugin-storage` | ✓ | File storage — public (CDN-direct) + private (access policy + per-object grants), S3 / R2 / GCS / MinIO / filesystem providers, presigned URLs, `listRefs` browse/search, HTTP Range (206) serving, dashboard browser |
|
|
71
71
|
| `@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) |
|
|
72
72
|
| `@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) |
|
|
73
|
-
| `@voltro/plugin-webhooks` | ✓ | Incoming + outgoing webhooks — `defineIncomingWebhook` (signature verify + idempotency, Stripe/GitHub/Slack presets) and `
|
|
73
|
+
| `@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) |
|
|
74
74
|
| `@voltro/plugin-auth-{workos,kinde,clerk,auth0,supabase,oidc}` | ✓ | Six IdP adapters over the shared `jwtBearerStrategy` — JWKS verify + claims→tenant mapping; WorkOS additionally ships hosted-login OAuth primitives (`workosAuthorizationUrl` / `workosAuthenticateWithCode`) for a redirect-based SSO login flow. [→ details](/docs/authentication/external-idp) |
|
|
75
75
|
| `@voltro/plugin-analytics-postgres` | ✓ | First-party lite — events on the main DataStore, cross-dialect (postgres / mysql / mariadb / mssql / sqlite / turso). [→ details](/docs/plugins/analytics#voltroplugin-analytics-postgres) |
|
|
76
76
|
| `@voltro/plugin-duckdb` | ✓ | Embedded DuckDB sidecar — real OLAP performance, no external service. [→ details](/docs/plugins/analytics#voltroplugin-duckdb) |
|
|
@@ -361,7 +361,7 @@ Events include lifecycle changes, timers, signals, and updates recorded by the w
|
|
|
361
361
|
|
|
362
362
|
## `useWorkflowDomainEvents(apiName, filters?, options?)`
|
|
363
363
|
|
|
364
|
-
Subscribes to the **domain events** an app emitted through `ctx.events.
|
|
364
|
+
Subscribes to the **domain events** an app emitted through `ctx.events.publish(...)` — the business-event log behind [event triggers](/docs/workflows/event-triggers), not one run's internal timeline.
|
|
365
365
|
|
|
366
366
|
```tsx
|
|
367
367
|
const { events } = useWorkflowDomainEvents('app', { name: 'order.paid', limit: 50 })
|
|
@@ -1131,7 +1131,7 @@ Templates come in **three kinds**, matching the three things you deploy:
|
|
|
1131
1131
|
| [`api-rest`](/docs/templates/api-rest) | Public REST API — `defineRestRoute` (query/path/body, scope guards, Idempotency-Key) + `@voltro/plugin-openapi` (OpenAPI 3.1 spec + Swagger UI at `/docs`). Zero-infra boot. |
|
|
1132
1132
|
| [`api-saas`](/docs/templates/api-saas) | The SaaS plugin bundle — billing entitlements + notifications + analytics + presence, wired turnkey; one `projects.create` exercises three together. Zero-infra boot. |
|
|
1133
1133
|
| [`api-observability`](/docs/templates/api-observability) | Production-readiness — Prometheus `/metrics` + a custom counter, Sentry (inert without a DSN), tracing, and a `@voltro/testing` unit test (`voltro test`). Zero-infra boot. |
|
|
1134
|
-
| [`api-webhooks`](/docs/templates/api-webhooks) | First-class webhooks both ways — a signature-verified incoming `*.webhook.tsx` receiver + an outgoing `
|
|
1134
|
+
| [`api-webhooks`](/docs/templates/api-webhooks) | First-class webhooks both ways — a signature-verified incoming `*.webhook.tsx` receiver + an outgoing `defineEvent` emitted via a durable signed delivery workflow. Zero-infra boot. |
|
|
1135
1135
|
|
|
1136
1136
|
## Web frontends (`kind: web`)
|
|
1137
1137
|
|
|
@@ -39,6 +39,24 @@ This matters because a caller-supplied URL is ordinary product surface — a
|
|
|
39
39
|
scraper, a webhook-registration form, an importer, a "test this connection"
|
|
40
40
|
button. Those reach `HttpClient` with whatever the user typed.
|
|
41
41
|
|
|
42
|
+
### Handling a refusal
|
|
43
|
+
|
|
44
|
+
A blocked request fails on the **error channel**, as an `HttpClientError.RequestError` whose `cause` is the `SsrfBlockedError`. So the handler that supplied the URL can decide what it means:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
yield* enrich(userSuppliedUrl).pipe(
|
|
48
|
+
Effect.catchTag('RequestError', (e) =>
|
|
49
|
+
e.cause instanceof SsrfBlockedError
|
|
50
|
+
? Effect.succeed(none) // an optional enrichment: skip it
|
|
51
|
+
: Effect.fail(e)), // a real transport failure: keep failing
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Two things worth knowing if you are upgrading from a version before this was a failure:
|
|
56
|
+
|
|
57
|
+
- **It used to be a defect** (`Effect.die`), so code that handled it did so with `Effect.catchAllDefect`. That arm no longer fires. If you also have an `Effect.mapError` above it, the rejection now reaches `mapError` FIRST — and a delivery that classified an SSRF block as *terminal* in the defect arm will be reclassified as whatever `mapError` returns. A consumer hit exactly this: a permanent condition became retryable and burned the full backoff budget re-attempting a request that can never succeed. Nothing in the type system can see that, because the error channel already carried `HttpClientError`.
|
|
58
|
+
- **`description` names the policy** (`SSRF policy: blocked private/reserved address …`), which is what you want in a log line; the `cause` is what you want in a branch.
|
|
59
|
+
|
|
42
60
|
### Allowing a target on purpose
|
|
43
61
|
|
|
44
62
|
```ts
|
|
@@ -663,7 +663,7 @@ const execute = async (
|
|
|
663
663
|
export default execute
|
|
664
664
|
```
|
|
665
665
|
|
|
666
|
-
`TenantMismatch` is imported from the browser-safe `@voltro/plugin-multitenancy/guard` subpath, NOT the package root — the root re-exports the schema mixin, which would drag `@voltro/database` into the client rpcGroup bundle. `ctx.events.
|
|
666
|
+
`TenantMismatch` is imported from the browser-safe `@voltro/plugin-multitenancy/guard` subpath, NOT the package root — the root re-exports the schema mixin, which would drag `@voltro/database` into the client rpcGroup bundle. `ctx.events.publish(...)` is post-commit safe: the event is recorded and fans out only once the row is durably written.
|
|
667
667
|
|
|
668
668
|
## Event trigger — start a workflow from a domain event
|
|
669
669
|
|
|
@@ -1019,7 +1019,7 @@ Watch the run in the dashboard's **Workflows** tab (the `awaitSignal` gate also
|
|
|
1019
1019
|
|
|
1020
1020
|
## Anti-patterns
|
|
1021
1021
|
|
|
1022
|
-
- **
|
|
1022
|
+
- **Worrying about publishing before the write commits.** You no longer have to: inside a mutation `ctx.events.publish` fires on COMMIT and not at all on rollback, so a rolled-back transaction cannot start a workflow for an order that does not exist — and cannot tell a client it does either. Publish wherever it reads best.
|
|
1023
1023
|
- **Doing external I/O inside a mutation.** The place mutation only inserts + emits. HTTP calls, payments, and other side effects belong in a workflow `step()` (journaled + replayed) or an action — a mutation runs in a transaction and can't roll back an HTTP side effect.
|
|
1024
1024
|
- **Tracking the workflow run handle to send a signal.** `orders.approve` re-derives the executionId from the payload via the deterministic `idempotencyKey` instead. Storing the handle from the place mutation is unnecessary and breaks across restarts.
|
|
1025
1025
|
- **Dropping the cross-tenant write guard.** Tables with `tenant()` get automatic SUBSCRIPTION scoping, but a mutation that writes raw rows still needs `assertOwnTenant(input.tenantId, ctx.request.subject)` — and `TenantMismatch` must be imported from `@voltro/plugin-multitenancy/guard` in the descriptor, never the package root (the root leaks `@voltro/database` into the browser bundle).
|
|
@@ -2212,7 +2212,7 @@ The package also ships `MockClock` / `MockEmail` / `mockAi` / `makeWorkflowRunne
|
|
|
2212
2212
|
<!-- source: en/templates/api-webhooks.md -->
|
|
2213
2213
|
## API · Webhooks
|
|
2214
2214
|
|
|
2215
|
-
_First-class webhooks both ways — a signature-verified incoming *.webhook.tsx receiver (genericProvider HMAC) that rejects forged traffic before your handler runs, plus an outgoing
|
|
2215
|
+
_First-class webhooks both ways — a signature-verified incoming *.webhook.tsx receiver (genericProvider HMAC) that rejects forged traffic before your handler runs, plus an outgoing defineEvent a mutation emits to subscribed targets via a durable signed delivery workflow. Zero-infra boot._
|
|
2216
2216
|
|
|
2217
2217
|
Webhooks **both ways**, wired turnkey: a **signature-verified incoming receiver** that rejects forged traffic before your code runs, and an **outgoing event** a mutation emits to subscribed targets through a durable, signed, retried delivery workflow. Webhooks are **file-convention** (drop a `*.webhook.tsx`, it's auto-discovered) — not a `plugins:[]` entry. Boots **zero-infra** (`store: 'memory'`). Template id: **`api-webhooks`**.
|
|
2218
2218
|
|
|
@@ -2231,7 +2231,7 @@ apps/acme/api/
|
|
|
2231
2231
|
├── .env # VOLTRO_WEBHOOK_SECRET_ORDERS (dev signing secret)
|
|
2232
2232
|
├── database/schema.ts # orders + webhookTables() bookkeeping
|
|
2233
2233
|
├── webhooks/orders.webhook.tsx # INCOMING — signature-verified receiver
|
|
2234
|
-
├── events/order.completed.webhook.tsx # OUTGOING —
|
|
2234
|
+
├── events/order.completed.webhook.tsx # OUTGOING — defineEvent
|
|
2235
2235
|
├── mutations/orders.fulfill.mutation.ts # create order + emit (+ .server.ts)
|
|
2236
2236
|
└── queries/orders.list.query.ts # reactive list (+ .server.ts)
|
|
2237
2237
|
```
|
|
@@ -2281,7 +2281,7 @@ curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:4000/webhooks/
|
|
|
2281
2281
|
|
|
2282
2282
|
```ts
|
|
2283
2283
|
// events/order.completed.webhook.tsx
|
|
2284
|
-
export default
|
|
2284
|
+
export default defineEvent({ id: 'order.completed', payload: Schema.Struct({ /* … */ }), version: 1 })
|
|
2285
2285
|
|
|
2286
2286
|
// mutations/orders.fulfill.mutation.server.ts — after the row commits:
|
|
2287
2287
|
const { eventId, deliveries } = await useWebhooks(ctx).emit('order.completed', { orderId, tenantId, /* … */ })
|
|
@@ -43,7 +43,7 @@ The four `api-backend*` shapes share the same minimal `notes` domain and differ
|
|
|
43
43
|
| [`api-rest`](/docs/templates/api-rest) | **Public REST + OpenAPI** — `defineRestRoute` endpoints (query/path/body, scope guards, `Idempotency-Key`) + `@voltro/plugin-openapi` (Swagger UI at `/docs`, generated from the descriptors). Zero-infra boot. |
|
|
44
44
|
| [`api-saas`](/docs/templates/api-saas) | **The SaaS plugin bundle** — billing entitlements (a quota gate), in-app + console notifications, analytics events, and live presence, wired turnkey. One `projects.create` exercises billing + analytics + notifications together. |
|
|
45
45
|
| [`api-observability`](/docs/templates/api-observability) | **Metrics + errors + tests** — `@voltro/plugin-prometheus` (`GET /metrics` + a custom counter), `@voltro/plugin-sentry` (inert without a DSN), tracing, and a `@voltro/testing` unit test. Zero-infra boot. |
|
|
46
|
-
| [`api-webhooks`](/docs/templates/api-webhooks) | **First-class webhooks** — a signature-verified incoming `*.webhook.tsx` receiver (rejects forged traffic before your handler) + an outgoing `
|
|
46
|
+
| [`api-webhooks`](/docs/templates/api-webhooks) | **First-class webhooks** — a signature-verified incoming `*.webhook.tsx` receiver (rejects forged traffic before your handler) + an outgoing `defineEvent` a mutation emits via a durable signed delivery workflow. Zero-infra boot. |
|
|
47
47
|
| [`api-search`](/docs/templates/api-search) | **Full-text search in sync** — `@voltro/plugin-search` mirrors every table write into an index via the post-commit change tap; the synthesized `search.query` rpc returns tenant-scoped hits. Memory backend (zero infra); one line to Typesense/Meilisearch/Algolia. |
|
|
48
48
|
| [`api-feature-flags`](/docs/templates/api-feature-flags) | **Feature flags as code** — `@voltro/plugin-flags`: kill-switch / %-rollout / targeting, a declarative `gatedBy` map + an in-handler `requireFlag` guard (both fail typed `FlagDisabled`), `useFlag()` on the web. Memory store. |
|
|
49
49
|
| [`api-ratelimit`](/docs/templates/api-ratelimit) | **Per-endpoint rate limiting** — `@voltro/plugin-ratelimit` intercepts rpc: a default + rules (sliding-window / token-bucket, by subject/tenant/…), typed `RateLimited` auto-merged into the error union. Memory store. |
|