@voltro/cli 0.30.1 → 0.31.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 (124) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/dist/apiBuild-BOqL1dlg.js +2 -0
  3. package/dist/{apiBuild-DmkzRyv7.js → apiBuild-CLPt-pX6.js} +2 -2
  4. package/dist/appGraph-CvQCte0z.js +2 -0
  5. package/dist/appGraph-gQ_6GkQQ.js +110 -0
  6. package/dist/bin.js +3 -3
  7. package/dist/checkCommand-ClRvH_SN.js +2 -0
  8. package/dist/{checkCommand-C4konUy9.js → checkCommand-D7r6_pk7.js} +439 -530
  9. package/dist/{commands-BkJft9K2.js → commands-CNVVv03h.js} +1941 -1880
  10. package/dist/{dbCommand-qn__kLEt.js → dbCommand-BH8GAYuf.js} +76 -19
  11. package/dist/dbCommand-BwQ_Jl98.js +2 -0
  12. package/dist/{dev-DXhhZ7-f.js → dev-DOZVMVfi.js} +1 -1
  13. package/dist/{dev-C_NPcLDp.js → dev-DdQEn2Ia.js} +1986 -1967
  14. package/dist/{frameworkTableAssembly-Cpgn2e88.js → frameworkTableAssembly-BwIrO5nv.js} +122 -103
  15. package/dist/frameworkTableAssembly-D-EebUQX.js +2 -0
  16. package/dist/index.js +1 -1
  17. package/dist/{inspectMetrics-CkB2J0Zn.js → inspectMetrics-BeEAnjqw.js} +774 -527
  18. package/dist/{sdkgen-wsWLrq4l.js → sdkgen-v9WV8zRi.js} +1 -1
  19. package/dist/{serveCommand-HtI4X5kj.js → serveCommand-KsKXXWn4.js} +294 -293
  20. package/dist/serveEntry.js +2 -2
  21. package/dist/{start-egkbpugc.js → start-CI2QdKy9.js} +351 -346
  22. package/dist/startEntry.js +2 -2
  23. package/package.json +17 -17
  24. package/templates/AGENTS.md +43 -43
  25. package/templates/agent-docs/_index.md +43 -43
  26. package/templates/agent-docs/_manifest.json +84 -42
  27. package/templates/agent-docs/cli.md +4 -2
  28. package/templates/agent-docs/database/migrations.md +14 -1
  29. package/templates/agent-docs/internationalization.md +54 -3
  30. package/templates/agent-docs/plugins/ai-flows.md +148 -0
  31. package/templates/agent-docs/plugins/analytics-postgres.md +90 -0
  32. package/templates/agent-docs/plugins/atlassian.md +378 -0
  33. package/templates/agent-docs/plugins/audit.md +324 -0
  34. package/templates/agent-docs/plugins/auth-auth0.md +92 -0
  35. package/templates/agent-docs/plugins/auth-clerk.md +90 -0
  36. package/templates/agent-docs/plugins/auth-kinde.md +88 -0
  37. package/templates/agent-docs/plugins/auth-oidc.md +95 -0
  38. package/templates/agent-docs/plugins/auth-supabase.md +103 -0
  39. package/templates/agent-docs/plugins/auth-workos.md +105 -0
  40. package/templates/agent-docs/plugins/auth.md +227 -0
  41. package/templates/agent-docs/plugins/billing.md +324 -0
  42. package/templates/agent-docs/plugins/broadcast.md +144 -0
  43. package/templates/agent-docs/plugins/cdc-out.md +116 -0
  44. package/templates/agent-docs/plugins/clickhouse.md +91 -0
  45. package/templates/agent-docs/plugins/datadog.md +105 -0
  46. package/templates/agent-docs/plugins/deactivation.md +114 -0
  47. package/templates/agent-docs/plugins/duckdb.md +82 -0
  48. package/templates/agent-docs/plugins/flags.md +146 -0
  49. package/templates/agent-docs/plugins/governance.md +143 -0
  50. package/templates/agent-docs/plugins/licensing.md +74 -0
  51. package/templates/agent-docs/plugins/logship.md +56 -0
  52. package/templates/agent-docs/plugins/mail.md +334 -0
  53. package/templates/agent-docs/plugins/moderation.md +65 -0
  54. package/templates/agent-docs/plugins/multitenancy.md +99 -0
  55. package/templates/agent-docs/plugins/notifications.md +252 -0
  56. package/templates/agent-docs/plugins/openapi.md +94 -0
  57. package/templates/agent-docs/plugins/postgis.md +140 -0
  58. package/templates/agent-docs/plugins/posthog.md +91 -0
  59. package/templates/agent-docs/plugins/presence.md +115 -0
  60. package/templates/agent-docs/plugins/prometheus.md +124 -0
  61. package/templates/agent-docs/plugins/ratelimit.md +274 -0
  62. package/templates/agent-docs/plugins/rbac.md +270 -0
  63. package/templates/agent-docs/plugins/scim.md +63 -0
  64. package/templates/agent-docs/plugins/search.md +116 -0
  65. package/templates/agent-docs/plugins/sentry.md +192 -0
  66. package/templates/agent-docs/plugins/soft-delete.md +72 -0
  67. package/templates/agent-docs/plugins/sso-saml.md +132 -0
  68. package/templates/agent-docs/plugins/storage.md +709 -0
  69. package/templates/agent-docs/plugins/tinybird.md +92 -0
  70. package/templates/agent-docs/plugins/versioning.md +226 -0
  71. package/templates/agent-docs/plugins/webhooks.md +585 -0
  72. package/templates/agent-docs/security.md +12 -1
  73. package/templates/agent-docs/whats-new.md +81 -37
  74. package/templates/apps/api-ai/package.json +7 -7
  75. package/templates/apps/api-auth/package.json +8 -8
  76. package/templates/apps/api-backend/package.json +7 -7
  77. package/templates/apps/api-backend-deactivation/package.json +7 -7
  78. package/templates/apps/api-backend-mail/package.json +8 -8
  79. package/templates/apps/api-backend-mariadb/package.json +9 -9
  80. package/templates/apps/api-backend-sqlite/package.json +8 -8
  81. package/templates/apps/api-backend-storage/package.json +8 -8
  82. package/templates/apps/api-cms/package.json +10 -10
  83. package/templates/apps/api-collab/package.json +8 -8
  84. package/templates/apps/api-data-advanced/package.json +8 -8
  85. package/templates/apps/api-durable/package.json +8 -8
  86. package/templates/apps/api-feature-flags/package.json +9 -9
  87. package/templates/apps/api-governance/package.json +8 -8
  88. package/templates/apps/api-kv/package.json +8 -8
  89. package/templates/apps/api-moderation/package.json +8 -8
  90. package/templates/apps/api-observability/package.json +8 -8
  91. package/templates/apps/api-ratelimit/package.json +8 -8
  92. package/templates/apps/api-rbac/package.json +8 -8
  93. package/templates/apps/api-rest/package.json +7 -7
  94. package/templates/apps/api-saas/package.json +11 -11
  95. package/templates/apps/api-saas-starter/package.json +10 -10
  96. package/templates/apps/api-search/package.json +8 -8
  97. package/templates/apps/api-status/package.json +8 -8
  98. package/templates/apps/api-versioning/package.json +8 -8
  99. package/templates/apps/api-webhooks/package.json +9 -9
  100. package/templates/apps/changelog/package.json +6 -6
  101. package/templates/apps/edge-functions/package.json +2 -2
  102. package/templates/apps/frontend-admin/package.json +8 -8
  103. package/templates/apps/frontend-app/package.json +8 -8
  104. package/templates/apps/frontend-auth/package.json +8 -8
  105. package/templates/apps/frontend-blank/package.json +7 -7
  106. package/templates/apps/frontend-cms/package.json +9 -9
  107. package/templates/apps/frontend-collab/package.json +9 -9
  108. package/templates/apps/frontend-contact/package.json +7 -7
  109. package/templates/apps/frontend-dashboard/package.json +7 -7
  110. package/templates/apps/frontend-docs/package.json +7 -7
  111. package/templates/apps/frontend-i18n/package.json +6 -6
  112. package/templates/apps/frontend-landing/package.json +7 -7
  113. package/templates/apps/frontend-portal/package.json +8 -8
  114. package/templates/apps/frontend-saas/package.json +8 -8
  115. package/templates/apps/frontend-spa/package.json +7 -7
  116. package/templates/apps/frontend-ssr/package.json +7 -7
  117. package/templates/apps/frontend-ssr-api/package.json +8 -8
  118. package/templates/apps/frontend-static-blog/package.json +6 -6
  119. package/templates/apps/frontend-status/package.json +8 -8
  120. package/templates/apps/mobile-app/package.json +2 -2
  121. package/dist/apiBuild-Ca6Sq5jy.js +0 -2
  122. package/dist/checkCommand-DHD0aBeR.js +0 -2
  123. package/dist/dbCommand-wfGi3FgG.js +0 -2
  124. package/dist/frameworkTableAssembly-Bn_c4Mnl.js +0 -2
@@ -0,0 +1,91 @@
1
+ # ClickHouse
2
+
3
+ > Production-grade OLAP AnalyticsSink over ClickHouse — self-hosted or ClickHouse Cloud — for billions of events with millisecond aggregates.
4
+
5
+
6
+
7
+ ---
8
+
9
+ <!-- source: en/plugins/clickhouse.md -->
10
+ ## ClickHouse
11
+
12
+ _Production-grade OLAP AnalyticsSink over ClickHouse — self-hosted or ClickHouse Cloud — for billions of events with millisecond aggregates._
13
+
14
+ `@voltro/plugin-clickhouse` is the production `AnalyticsSink` for analytics at
15
+ scale — native HTTP ingest into a ClickHouse cluster (self-hosted **or**
16
+ ClickHouse Cloud, both via the official `@clickhouse/client`). The sink owns an
17
+ `events` table inside the configured database (a MergeTree engine created on
18
+ first boot) and implements all four contract methods. Reach for it when
19
+ [postgres-lite](/docs/plugins/analytics-postgres) hits its ~10M events/day
20
+ ceiling and you need millisecond aggregate queries over billions of rows. For
21
+ the shared `AnalyticsSink` API and the `useAnalytics()` read methods, see
22
+ [Analytics & warehouse sinks](/docs/plugins/analytics).
23
+
24
+ ## Install
25
+
26
+ ```
27
+ pnpm add @voltro/plugin-clickhouse
28
+ ```
29
+
30
+ ## Wiring
31
+
32
+ ```ts
33
+ // app.config.ts
34
+ import { clickhouseAnalytics } from '@voltro/plugin-clickhouse'
35
+
36
+ export default {
37
+ type: 'api' as const,
38
+ name: 'myApi',
39
+ store: 'postgres' as const,
40
+ analytics: clickhouseAnalytics({
41
+ url: process.env.CLICKHOUSE_URL!, // https://my-cluster.clickhouse.cloud
42
+ username: process.env.CLICKHOUSE_USER,
43
+ password: process.env.CLICKHOUSE_PASSWORD,
44
+ database: 'voltro_events', // optional, default 'default'
45
+ }),
46
+ }
47
+ ```
48
+
49
+ At boot the plugin opens the client, pings the cluster (fail-fast on bad
50
+ config), and creates the events table + any mirror tables. The client is closed
51
+ on graceful shutdown.
52
+
53
+ ## Options
54
+
55
+ `clickhouseAnalytics(options)`:
56
+
57
+ | Option | Type | Default | Notes |
58
+ |---|---|---|---|
59
+ | `url` | `string` | — (**required**) | HTTP URL of the ClickHouse server, incl. protocol + port. |
60
+ | `username` | `string` | — | |
61
+ | `password` | `string` | — | |
62
+ | `database` | `string` | `'default'` | The plugin creates the events table inside it on first boot. |
63
+ | `table` | `string` | `'events'` | Override the events-table name. |
64
+ | `mirrorTables` | `ReadonlyArray<string>` | `[]` (events-only) | Reactive tables to CDC-mirror into `voltro_mirror_<table>` (a `ReplacingMergeTree`) so analytical queries JOIN events against live user data. |
65
+ | `mirrorPrimaryKey` | `string` | `'id'` | Primary-key column on the mirrored source rows. |
66
+
67
+ `database`, `table`, and each `mirrorTables` entry are validated as SQL
68
+ identifiers at boot (they're interpolated into DDL) — a bad name fails loudly at
69
+ `app.config` eval, not on the first query.
70
+
71
+ > **Connection details come from `options`, not env.** The plugin reads no
72
+ > `CLICKHOUSE_*` variables itself — the example above wires `process.env` into
73
+ > the options. Use `@voltro/env`'s `defineEnv` to declare them as secrets.
74
+
75
+ ## No raw-client escape hatch
76
+
77
+ ClickHouse-specific features outside the cross-provider contract
78
+ (HyperLogLog, dictionaries, materialised views) are not reachable through
79
+ the plugin — it exposes no raw `@clickhouse/client` handle; the
80
+ `AnalyticsSink` contract is the extension seam. Where you need them, query
81
+ ClickHouse with your own client instance against the same tables.
82
+
83
+ Using the Tag binds your handler to ClickHouse — stay on `useAnalytics()` for
84
+ provider-portable code. The plugin also exports `EVENTS_TABLE_DDL` and
85
+ `MIRROR_TABLE_DDL` for advanced schema work.
86
+
87
+ ## See also
88
+
89
+ - [Analytics & warehouse sinks](/docs/plugins/analytics) — the shared
90
+ `AnalyticsSink` contract, the `useAnalytics()` read API, `composeAnalytics`,
91
+ and the CDC-mirror details.
@@ -0,0 +1,105 @@
1
+ # Datadog
2
+
3
+ > Agentless Datadog metrics exporter — pushes the unified Metrics-API to Datadog's /api/v2/series HTTP intake.
4
+
5
+
6
+
7
+ ---
8
+
9
+ <!-- source: en/plugins/datadog.md -->
10
+ ## Datadog
11
+
12
+ _Agentless Datadog metrics exporter — pushes the unified Metrics-API to Datadog's /api/v2/series HTTP intake._
13
+
14
+ `@voltro/plugin-datadog` pushes the framework's metrics straight to Datadog over HTTP — **no Agent, no OTLP collector**. It periodically snapshots the unified Metrics-API (the same Effect `MetricRegistry` that [`@voltro/plugin-prometheus`](/docs/plugins/prometheus) and the dashboard read) and POSTs to `/api/v2/series`.
15
+
16
+ > **Have a Datadog Agent (or run an OTLP collector)?** Prefer the OTLP path: point `OTEL_EXPORTER_OTLP_ENDPOINT` at the Agent's OTLP port — the framework's [runtime metrics export](/docs/observability/overview#metrics-export) covers it without this plugin. Reach for this plugin when you have NO Agent (serverless, edge, a box you don't control).
17
+
18
+ ## Wiring
19
+
20
+ ```ts
21
+ // app.config.ts
22
+ import { datadogPlugin } from '@voltro/plugin-datadog'
23
+
24
+ export default {
25
+ type: 'api' as const,
26
+ name: 'api',
27
+ plugins: [
28
+ datadogPlugin({
29
+ // apiKey defaults to DD_API_KEY; without it the plugin is a no-op.
30
+ site: 'datadoghq.com', // DD_SITE; EU → 'datadoghq.eu', US3 → 'us3.datadoghq.com'
31
+ intervalMs: 30_000,
32
+ prefix: 'voltro.', // optional metric-name prefix
33
+ tags: ['env:prod', 'service:api'],
34
+ }),
35
+ ],
36
+ }
37
+ ```
38
+
39
+ Without an API key the plugin logs a warning and stays inert — it never blocks boot, and a Datadog outage is fail-soft (logged, never thrown).
40
+
41
+ ## Mapping
42
+
43
+ Every sample is submitted as a Datadog **gauge** (`type: 3`) at its current value, with metric labels turned into `key:value` tags (plus your global `tags`):
44
+
45
+ | Framework metric | Datadog |
46
+ |---|---|
47
+ | counter (e.g. `voltro_rpc_requests_total`) | gauge at the cumulative value — use `.as_count()` / `diff()` in a Datadog query to recover the per-interval rate |
48
+ | gauge (e.g. `voltro_subscriptions_active`) | gauge at the current value |
49
+ | histogram (e.g. `voltro_rpc_duration_seconds`) | `<name>.count`, `<name>.sum`, `<name>.avg` (sum/count) |
50
+
51
+ Counters are submitted cumulatively (stateless exporter — no previous-value tracking); Datadog query functions recover the rate. Summary/frequency metrics aren't produced by the framework and are skipped.
52
+
53
+ ## Deep integration — logs + traces + profiling
54
+
55
+ Beyond agentless metrics, opt into the full trace-correlated stack:
56
+
57
+ ```ts
58
+ datadogPlugin({
59
+ apiKey: env.DD_API_KEY,
60
+ service: 'api', env: 'prod', version: '1.4.2', // DD unified service tagging
61
+ logs: true, // forward the framework log sink → /api/v2/logs
62
+ traces: true, // framework spans → the Datadog Agent's OTLP receiver
63
+ agentUrl: 'http://localhost:4318', // or DD_TRACE_AGENT_URL
64
+ profiling: true, // dd-trace continuous profiler (profiler-only mode)
65
+ })
66
+ ```
67
+
68
+ - **Logs (`logs:true`)** — every framework log line is batched + POSTed to Datadog's logs intake with `dd.trace_id` / `dd.span_id` (the lower 64 bits of the OTel trace id, as Datadog expects) so logs link to APM traces. Maps `level → status`, `scope → an attribute`, and the structured fields through.
69
+ - **Traces (`traces:true`)** — the framework's OTel spans are routed to the **Datadog Agent's OTLP receiver** (Datadog's official OpenTelemetry ingestion). This is the deep path: the framework keeps owning the tracer (the in-app [Traces dashboard](/docs/observability/distributed-tracing) stays intact) and the Agent forwards to DD APM. `dd-trace` is **not** used as the tracer — it would have to become the global provider, which would break the in-app dashboard.
70
+ - **Profiling (`profiling:true`)** — the official `dd-trace` continuous profiler, started in profiler-only mode (`tracing:false`) so it never hijacks the OTel tracer.
71
+
72
+ `dd-trace` + the OTel exporter packages are optional, lazy-loaded dependencies.
73
+
74
+ > **No Agent?** Metrics + logs are agentless (HTTP intake). Traces need an Agent — point `agentUrl` at it. If you already run an OTLP collector, you can also export traces+metrics via the framework's runtime OTLP path (`OTEL_EXPORTER_OTLP_ENDPOINT`) without this plugin.
75
+
76
+ ## Configuration
77
+
78
+ | Option | Type | Default | Notes |
79
+ |---|---|---|---|
80
+ | `apiKey` | `string` | `DD_API_KEY` env | Needed for metrics + logs (agentless). Traces use the Agent and don't need it. |
81
+ | `site` | `string` | `DD_SITE` env / `datadoghq.com` | EU → `datadoghq.eu`, US3 → `us3.datadoghq.com`, … |
82
+ | `intervalMs` | `number` | `30000` | Metrics push interval. |
83
+ | `prefix` | `string` | — | Prepended to every metric name (e.g. `voltro.`). |
84
+ | `tags` | `string[]` | — | Added to every series + log (`['env:prod','service:api']`). |
85
+ | `logs` | `boolean` | `false` | Forward the framework log sink → `/api/v2/logs`. |
86
+ | `logsIntervalMs` | `number` | `5000` | Logs batch flush interval. |
87
+ | `traces` | `boolean` | `false` | Route framework spans → the Agent's OTLP receiver. |
88
+ | `agentUrl` | `string` | `DD_TRACE_AGENT_URL` / `http://localhost:4318` | `/v1/traces` is appended. |
89
+ | `profiling` | `boolean` | `false` | dd-trace continuous profiler (profiler-only mode). |
90
+ | `service` / `env` / `version` | `string` | `DD_SERVICE` / `DD_ENV` / `DD_VERSION` | DD unified service tagging — consistent across metrics, logs + traces. |
91
+ | `name` | `string` | — | Disambiguates multiple instances. |
92
+
93
+ ## Troubleshooting
94
+
95
+ | Symptom | Cause / fix |
96
+ |---|---|
97
+ | Boot logs `datadog inactive` | No `DD_API_KEY` **and** neither `traces` nor `profiling` enabled — nothing to do. Set a key (metrics/logs) or `traces`/`profiling`. |
98
+ | Metrics/logs don't appear | `apiKey` missing or wrong `site` region. A failed POST is fail-soft — check `voltro logs` for `datadog series/logs POST failed`. |
99
+ | `traces: true` but nothing in DD APM | The Agent's OTLP receiver must be enabled and reachable at `agentUrl` (`/v1/traces`). No Agent → traces won't ship (metrics/logs still do). |
100
+ | Logs not linked to traces in DD | Correlation needs `dd.trace_id` — emitted only for log lines carrying `fields.traceId` (i.e. inside an rpc trace). Set `service` so DD groups them. |
101
+ | In-app Traces dashboard intact? | Yes — the contributed OTLP exporter is added alongside the framework's buffer sink; it never replaces the tracer. |
102
+
103
+ ## Permissions
104
+
105
+ The plugin declares `network:outbound:<site>` (metrics/logs) — plus `network:outbound:<agent-host>` when `traces`/`profiling` is on. It mounts no routes and intercepts nothing.
@@ -0,0 +1,114 @@
1
+ # Deactivation
2
+
3
+ > A schema mixin that marks a subject as deactivated (can't log in) while keeping its data fully visible — the deliberate opposite of soft-delete.
4
+
5
+
6
+
7
+ ---
8
+
9
+ <!-- source: en/plugins/deactivation.md -->
10
+ ## Deactivation
11
+
12
+ _A schema mixin that marks a subject as deactivated (can't log in) while keeping its data fully visible — the deliberate opposite of soft-delete._
13
+
14
+ `@voltro/plugin-deactivation` ships a single surface: the `deactivation()` schema mixin. A deactivated subject — typically a user who can no longer log in — has its data stay fully **visible** (assignments, history, audit trail). That's the deliberate opposite of `softDelete()`, which **hides** the row from default reads and anonymises PII for GDPR.
15
+
16
+ **Status:** ✓ shipped.
17
+
18
+ ## What it adds
19
+
20
+ The mixin contributes two columns:
21
+
22
+ | Column | Type | Meaning |
23
+ |---|---|---|
24
+ | `deactivatedAt` | nullable timestamp | set ⇒ the subject is deactivated; `null` ⇒ active |
25
+ | `deactivatedBy` | nullable reference → Actor | who performed the deactivation |
26
+
27
+ It's a **pure schema mixin** (columns only), exactly like `audit()` — no read scoping, no delete interception. You set `deactivatedAt` with a normal `ctx.store.update(...)`.
28
+
29
+ `deactivation()` transitively requires `audit()` (a deactivation is an audit-worthy event, and the timestamps sit together on the row), so `audit()` auto-stamps `updatedAt` / `updatedBy` alongside — the WHO/WHEN of the change is captured for free. Its stable id is `voltro/deactivation`.
30
+
31
+ ## Composing it
32
+
33
+ ```ts
34
+ import { id, table, text } from '@voltro/database'
35
+ import { deactivation } from '@voltro/plugin-deactivation'
36
+
37
+ export const users = table('users', {
38
+ id: id(),
39
+ email: text().unique(),
40
+ name: text(),
41
+ }).with(deactivation())
42
+ ```
43
+
44
+ The dep resolver dedupes, so listing `audit()` explicitly alongside is harmless:
45
+
46
+ ```ts
47
+ import { audit } from '@voltro/plugin-audit/mixin'
48
+ import { deactivation } from '@voltro/plugin-deactivation'
49
+
50
+ table('users', { /* … */ }).with(audit(), deactivation())
51
+ ```
52
+
53
+ A project that doesn't want the barrel can import from the `./mixin` subpath:
54
+
55
+ ```ts
56
+ import { deactivation } from '@voltro/plugin-deactivation/mixin'
57
+ ```
58
+
59
+ ## Deactivating + reactivating
60
+
61
+ Both are plain updates — `audit()` records the actor and time:
62
+
63
+ ```ts
64
+ // deactivate — update(table, primaryKey, patch)
65
+ await ctx.store.update('users', userId, {
66
+ deactivatedAt: new Date(),
67
+ deactivatedBy: ctx.request.subject.id,
68
+ })
69
+
70
+ // reactivate
71
+ await ctx.store.update('users', userId, {
72
+ deactivatedAt: null,
73
+ deactivatedBy: null,
74
+ })
75
+ ```
76
+
77
+ Because the row is never hidden, your own queries decide what "active" means — the user stays visible everywhere in the app while their login is refused.
78
+
79
+ ## Enforcing "a deactivated user can't log in"
80
+
81
+ The mixin's promise is **self-enforcing** through `@voltro/plugin-auth`'s post-authentication subject-guard seam — you do **not** hand-roll a `deactivatedAt === null` check in an auth resolver. Import `deactivationGuard()` from the `/guard` subpath and wire it onto the auth plugin in one line:
82
+
83
+ ```ts
84
+ import { authRoutesPlugin } from '@voltro/plugin-auth/plugin'
85
+ import { deactivationGuard } from '@voltro/plugin-deactivation/guard'
86
+
87
+ authRoutesPlugin({
88
+ store,
89
+ secret: process.env.VOLTRO_SESSION_SECRET!,
90
+ defaultTenantId: 'acme',
91
+ subjectGuards: [deactivationGuard()],
92
+ })
93
+ ```
94
+
95
+ The auth pipeline runs each subject guard **after** the credential check (password / MFA / magic-link / passkey) but **before** it issues a session. When the resolved user's `deactivatedAt` is set, `deactivationGuard()` vetoes: the login returns a **403** whose body `error` is `account_deactivated`, and **no session cookie is issued** — on every sign-in path. Reactivate the user (`deactivatedAt = null`) and the next login proceeds normally. See the [auth plugin's subject-guard seam](/docs/plugins/auth#post-authentication-subject-guards).
96
+
97
+ `AccountDeactivated` — a `Schema.TaggedError` on the same `/guard` subpath — is the typed form of the rejection for code that prefers matching on `_tag === 'AccountDeactivated'` over the wire body. The `/guard` subpath is **server-only** and is not imported by the mixin, so a `*.entity.ts` importing `deactivation()` never drags the auth surface into the browser bundle.
98
+
99
+ ## Deactivation vs soft-delete
100
+
101
+ | | `deactivation()` | `softDelete()` |
102
+ |---|---|---|
103
+ | Row visible in default reads | yes | no (hidden) |
104
+ | PII | untouched | anonymised |
105
+ | Typical use | user can't log in, data stays | GDPR erasure / "delete" |
106
+ | Read scoping | none | filters hidden rows out |
107
+
108
+ The two are **orthogonal and compose**. A user can be deactivated (visible) and later soft-deleted (hidden) — apply both mixins.
109
+
110
+ ## When to use which
111
+
112
+ - **Deactivation** — revoke access but keep the subject's contributions intact and attributable (the common "disable an employee account" case).
113
+ - **Soft-delete** — the user must disappear from the app and their PII must be scrubbed.
114
+ - **Both** — start with deactivation, escalate to soft-delete if an erasure request arrives.
@@ -0,0 +1,82 @@
1
+ # DuckDB
2
+
3
+ > Embedded DuckDB AnalyticsSink — real column-store OLAP in-process, no external service to run.
4
+
5
+
6
+
7
+ ---
8
+
9
+ <!-- source: en/plugins/duckdb.md -->
10
+ ## DuckDB
11
+
12
+ _Embedded DuckDB AnalyticsSink — real column-store OLAP in-process, no external service to run._
13
+
14
+ `@voltro/plugin-duckdb` is the embedded `AnalyticsSink` — it runs DuckDB
15
+ **in-process** (via `@duckdb/node-api`) so you get real column-store +
16
+ vectorized OLAP performance without deploying an external service. Events land
17
+ in a `voltro_events` column-store table inside the DuckDB instance; the sink
18
+ implements all four contract methods against it. It's the sweet spot between
19
+ [postgres-lite](/docs/plugins/analytics-postgres) (no infra, but row-store) and
20
+ [ClickHouse](/docs/plugins/clickhouse) (fastest, but an external cluster). The
21
+ catch: it's single-process. For the shared `AnalyticsSink` API and the
22
+ `useAnalytics()` read methods, see [Analytics & warehouse sinks](/docs/plugins/analytics).
23
+
24
+ ## Install
25
+
26
+ ```
27
+ pnpm add @voltro/plugin-duckdb
28
+ ```
29
+
30
+ `@duckdb/node-api` is an optional dependency — installed alongside the plugin.
31
+
32
+ ## Wiring
33
+
34
+ ```ts
35
+ // app.config.ts
36
+ import { duckdbAnalytics } from '@voltro/plugin-duckdb'
37
+
38
+ export default {
39
+ type: 'api' as const,
40
+ name: 'myApi',
41
+ store: 'postgres' as const,
42
+ analytics: duckdbAnalytics({ path: '.voltro/analytics.duckdb' }),
43
+ }
44
+ ```
45
+
46
+ At boot the plugin opens the DuckDB connection and creates the events table; the
47
+ connection + instance are closed on graceful shutdown.
48
+
49
+ ## Options
50
+
51
+ `duckdbAnalytics(options?)` — all optional:
52
+
53
+ | Option | Type | Default | Notes |
54
+ |---|---|---|---|
55
+ | `path` | `string` | in-memory | A file path (`.voltro/analytics.duckdb`) → durable across restarts, single-process. Omit or pass `:memory:` → events live in process memory, lost on restart (ephemeral dev / tests). |
56
+ | `mirrorTables` | `ReadonlyArray<string>` | `[]` (events-only) | Reactive tables to CDC-mirror into `voltro_mirror_<table>` inside DuckDB so analytical queries JOIN events against live user data. |
57
+ | `mirrorPrimaryKey` | `string` | `'id'` | Primary-key column on the mirrored source rows. |
58
+
59
+ ```ts
60
+ analytics: duckdbAnalytics({
61
+ path: '.voltro/analytics.duckdb',
62
+ mirrorTables: ['users', 'teams'],
63
+ })
64
+ ```
65
+
66
+ ## Tenant isolation
67
+
68
+ Parity with [postgres-lite](/docs/plugins/analytics-postgres): `track` stamps the event's `tenantId` into a `tenant_id` column, and `aggregate`/`timeseries`/`topN` filter by the query's `tenantId` (injected by `useAnalytics()` from the caller's subject), so a tenant-scoped read never spans tenants. A query with no `tenantId` is an unscoped system read. Swapping `postgresAnalytics()` → `duckdbAnalytics()` preserves the same tenant semantics.
69
+
70
+ ## Single-process constraint
71
+
72
+ DuckDB can't open the same file from two workers. For multi-instance
73
+ deployments, either pin all analytics traffic to one replica
74
+ (sticky-session style) or use [`@voltro/plugin-clickhouse`](/docs/plugins/clickhouse)
75
+ instead. There is no provider escape-hatch Tag — the contract methods are the
76
+ whole surface.
77
+
78
+ ## See also
79
+
80
+ - [Analytics & warehouse sinks](/docs/plugins/analytics) — the shared
81
+ `AnalyticsSink` contract, the `useAnalytics()` read API, `composeAnalytics`,
82
+ and the CDC-mirror details (incl. the `voltro_mirror_<table>` JOIN pattern).
@@ -0,0 +1,146 @@
1
+ # Feature flags
2
+
3
+ > Feature flags — per-subject / per-tenant targeting, deterministic % rollouts, kill-switch, declarative rpc gating + client UI gating.
4
+
5
+
6
+
7
+ ---
8
+
9
+ <!-- source: en/plugins/flags.md -->
10
+ ## Feature flags
11
+
12
+ _Feature flags — per-subject / per-tenant targeting, deterministic % rollouts, kill-switch, declarative rpc gating + client UI gating._
13
+
14
+ `@voltro/plugin-flags` is feature flags done the framework way: flags as code (or runtime-toggleable), evaluated per caller with **targeting + deterministic % rollout**, gating rpc calls declaratively and the UI via a hook.
15
+
16
+ ## Wiring
17
+
18
+ ```ts
19
+ // app.config.ts
20
+ import { flagsPlugin } from '@voltro/plugin-flags'
21
+
22
+ export default {
23
+ type: 'api' as const,
24
+ name: 'api',
25
+ plugins: [
26
+ flagsPlugin({
27
+ flags: {
28
+ betaExport: false, // kill-switch
29
+ newDashboard: { rollout: 25, description: 'Gradual rollout' }, // 25% of subjects
30
+ aiSummaries: { targeting: [{ metadata: { plan: 'pro' } }] }, // pro plans only
31
+ },
32
+ // Optional: fail a gated rpc with typed `FlagDisabled` BEFORE the handler runs.
33
+ gatedBy: { 'reports.export': 'betaExport' },
34
+ }),
35
+ ],
36
+ }
37
+ ```
38
+
39
+ A flag is a bare boolean (kill-switch) or `{ enabled?, rollout?, targeting?, rolloutBy?, variants?, offVariant?, schedule?, description? }`.
40
+
41
+ ## Evaluation order
42
+
43
+ `enabled:false` (off for everyone) → **schedule** (time-box + ramp, see below) → **targeting** (OR-of-rules; each rule ANDs `subjectIds` / `tenantIds` / `subjectTypes` / `metadata`) → **% rollout** (0–100, deterministic FNV-1a bucket on the subject — or tenant via `rolloutBy:'tenant'` — so a caller stays in/out consistently across processes).
44
+
45
+ ## Multivariate / variant flags
46
+
47
+ Beyond boolean on/off, a flag can carry a set of **variants** — named string / number / boolean / JSON values the flag resolves ONE of per subject. Allocation is deterministic and weighted: a subject stays in the same variant across calls and processes (an FNV-1a hash of `(flag:variant, id)`, independent of the % rollout bucket). Omit `weight` for an even split; `[{weight:3}, {weight:1}]` is a 75% / 25% split.
48
+
49
+ ```ts
50
+ flagsPlugin({
51
+ flags: {
52
+ checkoutButton: {
53
+ variants: [
54
+ { name: 'control', value: 'Buy now' },
55
+ { name: 'urgent', value: 'Buy now — 2 left!', weight: 2 }, // 2× exposure
56
+ ],
57
+ },
58
+ pricingConfig: {
59
+ // JSON / number variants, not just strings
60
+ variants: [
61
+ { name: 'a', value: { plan: 'pro', seats: 5 } },
62
+ { name: 'b', value: { plan: 'pro', seats: 10 } },
63
+ ],
64
+ },
65
+ },
66
+ })
67
+ ```
68
+
69
+ Resolve the served variant in the browser with `useVariant` / `useVariants`:
70
+
71
+ ```tsx
72
+ import { useVariant } from '@voltro/plugin-flags/web'
73
+
74
+ const v = useVariant('checkoutButton') // { name, value, enabled } | undefined
75
+ return <button>{String(v?.value ?? 'Buy now')}</button>
76
+ ```
77
+
78
+ Server-side, `evaluateVariant(key, value, subject)` returns `{ name, value, enabled }`. A boolean flag surfaces a synthetic `{ name: 'on' | 'off', value, enabled }`, so every flag has a uniform variant shape. Set `offVariant: '<name>'` to make the boolean resolution (`isFlagEnabled` / `flags.evaluate`) treat one variant as "off".
79
+
80
+ ## Scheduled / time-boxed rollouts
81
+
82
+ A flag can carry a `schedule`, evaluated against the current time:
83
+
84
+ ```ts
85
+ flagsPlugin({
86
+ flags: {
87
+ // Time-boxed: off before activateAt, on inside, off at/after deactivateAt.
88
+ blackFridayBanner: {
89
+ schedule: { activateAt: '2026-11-27T00:00:00Z', deactivateAt: '2026-11-28T00:00:00Z' },
90
+ },
91
+ // Ramping rollout: exposure grows 0% → 100% linearly across the window.
92
+ gradualLaunch: {
93
+ schedule: { ramp: { from: 0, to: 100, startAt: '2026-07-01T00:00:00Z', endAt: '2026-07-08T00:00:00Z' } },
94
+ },
95
+ },
96
+ })
97
+ ```
98
+
99
+ Instants are epoch millis or ISO strings. A `ramp`'s interpolated percentage REPLACES the static `rollout` while active (before `startAt` ⇒ `from`%, after `endAt` ⇒ `to`%), so exposure increases smoothly over the window while every subject's in/out decision stays deterministic.
100
+
101
+ ## Audit trail of kill-switch flips
102
+
103
+ On the `postgres` tier every `/toggle` flip is recorded to a durable, append-only `_voltro_feature_flag_audit` table (who, when, flag, old→new state). Read the trail — newest-first — via the inspect endpoint:
104
+
105
+ ```http
106
+ GET /_voltro/inspect/plugins/flags/audit # all flips, newest-first
107
+ GET /_voltro/inspect/plugins/flags/audit?flag=beta # one flag's history
108
+ ```
109
+
110
+ The `POST /toggle` body accepts an optional `actor` (the acting admin id) that is stored on the audit row. On the `memory` tier there is no durable audit (the endpoint returns an empty trail with a note).
111
+
112
+ ## Three ways to use a flag
113
+
114
+ **1. Declarative gate** — `gatedBy: { '<rpcTag>': '<flag>' }` (exact tag or `/regex/`). An off flag fails the call with typed `FlagDisabled` before the handler runs (merged into every procedure's wire-error union → typed on the client).
115
+
116
+ **2. In-handler guard:**
117
+
118
+ ```ts
119
+ import { requireFlag, isFlagEnabled } from '@voltro/plugin-flags'
120
+
121
+ export default (input, ctx) => Effect.gen(function* () {
122
+ yield* requireFlag(ctx, 'aiSummaries') // fails FlagDisabled if off for the caller
123
+ // …or branch: if (isFlagEnabled(ctx, 'newDashboard')) { … }
124
+ })
125
+ ```
126
+
127
+ **3. Client UI gating** — the `flags.evaluate` query returns the caller's resolved flag set:
128
+
129
+ ```tsx
130
+ import { useFlags, useFlag } from '@voltro/plugin-flags/web'
131
+
132
+ const flags = useFlags() // { newDashboard: true, … }
133
+ if (useFlag('newDashboard')) return <NewDashboard />
134
+ ```
135
+
136
+ ## Dashboard panel
137
+
138
+ Both the local devtools dashboard and the cloud dashboard ship a **Flags** panel (api apps) — a live list of every resolved flag with its rollout % + a **kill-switch toggle**. Toggling flips the flag in the live registry immediately (gated on the `canToggleFlag` capability in cloud). Backed by the plugin's `/_voltro/inspect/plugins/flags/{list,toggle}` endpoints — the plugin declares `inspect:read` (the `list` read) **and** `inspect:write` (the state-mutating `toggle` POST).
139
+
140
+ ## Store
141
+
142
+ `'memory'` (config-as-code, default) — flags live in `app.config`. `'postgres'` overlays runtime-toggleable overrides on the config baseline (the override wins, so toggling off in the DB beats the code default).
143
+
144
+ ## Permissions
145
+
146
+ `rpc:intercept:{mutation,query,action}` only when `gatedBy` is set (otherwise none — the `flags.evaluate` route + guards need no interceptor).