@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.
- package/CHANGELOG.md +163 -0
- package/dist/apiBuild-BOqL1dlg.js +2 -0
- package/dist/{apiBuild-DmkzRyv7.js → apiBuild-CLPt-pX6.js} +2 -2
- package/dist/appGraph-CvQCte0z.js +2 -0
- package/dist/appGraph-gQ_6GkQQ.js +110 -0
- package/dist/bin.js +3 -3
- package/dist/checkCommand-ClRvH_SN.js +2 -0
- package/dist/{checkCommand-C4konUy9.js → checkCommand-D7r6_pk7.js} +439 -530
- package/dist/{commands-BkJft9K2.js → commands-CNVVv03h.js} +1941 -1880
- package/dist/{dbCommand-qn__kLEt.js → dbCommand-BH8GAYuf.js} +76 -19
- package/dist/dbCommand-BwQ_Jl98.js +2 -0
- package/dist/{dev-DXhhZ7-f.js → dev-DOZVMVfi.js} +1 -1
- package/dist/{dev-C_NPcLDp.js → dev-DdQEn2Ia.js} +1986 -1967
- package/dist/{frameworkTableAssembly-Cpgn2e88.js → frameworkTableAssembly-BwIrO5nv.js} +122 -103
- package/dist/frameworkTableAssembly-D-EebUQX.js +2 -0
- package/dist/index.js +1 -1
- package/dist/{inspectMetrics-CkB2J0Zn.js → inspectMetrics-BeEAnjqw.js} +774 -527
- package/dist/{sdkgen-wsWLrq4l.js → sdkgen-v9WV8zRi.js} +1 -1
- package/dist/{serveCommand-HtI4X5kj.js → serveCommand-KsKXXWn4.js} +294 -293
- package/dist/serveEntry.js +2 -2
- package/dist/{start-egkbpugc.js → start-CI2QdKy9.js} +351 -346
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.md +43 -43
- package/templates/agent-docs/_index.md +43 -43
- package/templates/agent-docs/_manifest.json +84 -42
- package/templates/agent-docs/cli.md +4 -2
- package/templates/agent-docs/database/migrations.md +14 -1
- package/templates/agent-docs/internationalization.md +54 -3
- package/templates/agent-docs/plugins/ai-flows.md +148 -0
- package/templates/agent-docs/plugins/analytics-postgres.md +90 -0
- package/templates/agent-docs/plugins/atlassian.md +378 -0
- package/templates/agent-docs/plugins/audit.md +324 -0
- package/templates/agent-docs/plugins/auth-auth0.md +92 -0
- package/templates/agent-docs/plugins/auth-clerk.md +90 -0
- package/templates/agent-docs/plugins/auth-kinde.md +88 -0
- package/templates/agent-docs/plugins/auth-oidc.md +95 -0
- package/templates/agent-docs/plugins/auth-supabase.md +103 -0
- package/templates/agent-docs/plugins/auth-workos.md +105 -0
- package/templates/agent-docs/plugins/auth.md +227 -0
- package/templates/agent-docs/plugins/billing.md +324 -0
- package/templates/agent-docs/plugins/broadcast.md +144 -0
- package/templates/agent-docs/plugins/cdc-out.md +116 -0
- package/templates/agent-docs/plugins/clickhouse.md +91 -0
- package/templates/agent-docs/plugins/datadog.md +105 -0
- package/templates/agent-docs/plugins/deactivation.md +114 -0
- package/templates/agent-docs/plugins/duckdb.md +82 -0
- package/templates/agent-docs/plugins/flags.md +146 -0
- package/templates/agent-docs/plugins/governance.md +143 -0
- package/templates/agent-docs/plugins/licensing.md +74 -0
- package/templates/agent-docs/plugins/logship.md +56 -0
- package/templates/agent-docs/plugins/mail.md +334 -0
- package/templates/agent-docs/plugins/moderation.md +65 -0
- package/templates/agent-docs/plugins/multitenancy.md +99 -0
- package/templates/agent-docs/plugins/notifications.md +252 -0
- package/templates/agent-docs/plugins/openapi.md +94 -0
- package/templates/agent-docs/plugins/postgis.md +140 -0
- package/templates/agent-docs/plugins/posthog.md +91 -0
- package/templates/agent-docs/plugins/presence.md +115 -0
- package/templates/agent-docs/plugins/prometheus.md +124 -0
- package/templates/agent-docs/plugins/ratelimit.md +274 -0
- package/templates/agent-docs/plugins/rbac.md +270 -0
- package/templates/agent-docs/plugins/scim.md +63 -0
- package/templates/agent-docs/plugins/search.md +116 -0
- package/templates/agent-docs/plugins/sentry.md +192 -0
- package/templates/agent-docs/plugins/soft-delete.md +72 -0
- package/templates/agent-docs/plugins/sso-saml.md +132 -0
- package/templates/agent-docs/plugins/storage.md +709 -0
- package/templates/agent-docs/plugins/tinybird.md +92 -0
- package/templates/agent-docs/plugins/versioning.md +226 -0
- package/templates/agent-docs/plugins/webhooks.md +585 -0
- package/templates/agent-docs/security.md +12 -1
- package/templates/agent-docs/whats-new.md +81 -37
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +10 -10
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-collab/package.json +9 -9
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +2 -2
- package/dist/apiBuild-Ca6Sq5jy.js +0 -2
- package/dist/checkCommand-DHD0aBeR.js +0 -2
- package/dist/dbCommand-wfGi3FgG.js +0 -2
- 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).
|