# Framework guidelines

> Reference for AI coding agents (Claude Code, Cursor, GitHub Copilot, …) working
> in a Voltro project. This file is the **always-loaded core**: the mental model,
> the rules that are true everywhere, and an **index** into deep, on-demand docs.
> When you work on a topic, OPEN its linked doc — don't guess from this summary.
>
> If anything here conflicts with what you observe in the codebase, trust the
> codebase. When unsure, grep for an existing example of the same primitive and
> pattern-match against it — code that already passes `voltro dev` is the most
> reliable source of truth.
>
> Seeded by `voltro dev` / `voltro agents-md`. Yours to extend — add
> project-specific rules below. `voltro agents-md --force` rewrites it; to shield
> a hand-maintained file from `--force`, mark it with the keep sentinel
> (see `agent-docs/cli.md`).

## Mental model

Voltro is **reactive end-to-end**. A web client subscribes to a server-side
query; when the underlying data changes (a mutation, a workflow side-effect, an
out-of-band DB write on postgres/mariadb), the server pushes a delta over
WebSocket and the React tree re-renders. There is **no `refetch`** —
subscriptions stay live for the component's lifetime.

The wire protocol is `@effect/rpc` over WebSocket (JSON). Types flow from each
descriptor's `effect/Schema` through codegen (`rpcGroup.generated.ts`) to the
web client's hooks — no manual type-emit. The runtime is **Effect-first**:
handlers may return `Effect`, `Stream`, or a plain `Promise`. Do **not** add a
parallel runtime (no `bullmq`, `temporal`, Restate, `react-query`, SWR) — the
Effect dependency is mandatory; subscriptions own the live-data axis.

Every RPC primitive is **two files paired by basename**: a browser-safe
**descriptor** (`*.query.ts` / `*.mutation.ts` / `*.action.ts` / `*.stream.ts`)
and a server-only **executor** (`*.server.ts`). The framework pairs them at boot.
Workflows split the same way (`*.workflow.tsx` + `*.workflow.server.tsx`).

### Pick the primitive (decision rubric)

Ask in order; stop at the first "yes":

1. **Something HAPPENED, with no row behind it?** → **event** (`*.event.ts`).
   A game started, a door opened, a terminal confirmed. It has a time and no
   value afterwards, where a row has a value and no time. Ephemeral, fan-out to
   every listening client, at-most-once. **If you are about to write a table so
   that a subscriber fires, this is what you want.**
2. **Needs a LIVE feed that auto-updates on writes?** → **query** (`queries/`).
   The reactive read primitive. Declares `source: 'table'` so writes invalidate it.
3. **A single atomic DB write (one commit boundary)?** → **mutation** (`mutations/`).
   Runs in a transaction; a throw rolls back; ChangeEvents drain only on commit.
   Declares `target: { table, op }` → drives client auto-optimistic.
4. **External I/O — HTTP, file, AI, payment, email, signed URL?** → **action**
   (`actions/`). NOT transactional; no rollback of side effects.
5. **Multi-step work that must survive a crash/deploy, retry-from-where-it-died?**
   → **workflow** (`*.workflow.tsx`). Durable, suspend/resume.

| Use case | Primitive |
|---|---|
| A game starts / a door opens / a printer finishes — many screens react | Event |
| List todos / messages on a page | Query |
| Toggle a todo, send a message, edit a name | Mutation |
| Generate a thumbnail / send a welcome email / call an LLM ad-hoc | Action |
| Onboarding flow (create → email → wait → poke); order fulfilment | Workflow |
| Nightly/hourly periodic job | Schedule (`*.cron.tsx`) → workflow for durable work |
| Read a feature flag once | Action (or a query if it must push updates) |

Deep dive: **`data.md`** (queries/mutations/actions/streams/errors),
**`workflows.md`**, **`scheduling.md`**.

**Do NOT hand-roll debounce / one-at-a-time / rate limits around a workflow.**
They are fields on `workflow({...})`, enforced BEFORE a run exists:

```ts
debounce:    { key: (p) => `tour:${p.rowId}`, period: '15 minutes' }
singleton:   { key: (p) => p.tenantId, mode: 'skip' | 'cancel' }
concurrency: { limit: 5, key: (p) => p.tenantId }
throttle:    { limit: 100, period: '1 minute' }   // queues the excess
rateLimit:   { limit: 100, period: '1 minute' }   // DROPS the excess
batch:       { item: Item, key: (i) => i.tenantId, maxSize: 100, timeout: '30 seconds' }
timeouts:    { start: '1 hour', finish: '10 minutes' }
onFailure:   'notifierWorkflowName'
cancelOn:    [{ event: 'jira.issue.deleted', schema: Deleted,
                match: (e, p) => e.issueKey === p.issueKey }]
```

The hand-rolled version (an idempotency key carrying a timestamp + a re-check
loop + a round cap) costs one durable cluster entity PER START to express "one
job, latest deadline". `voltro workflows flow` shows what is queued and why.

`cancelOn` reaches a run whose fiber is not executing anything — sleeping,
suspended, or still queued — which a race inside the body cannot. `match` is
REQUIRED (the omitted case would cancel every live run); a run that started AFTER
the event is never cancelled; queued starts of the same workflow are discarded
too, or the debounced duplicate fires seconds later against the deleted row.

Stopping or re-driving MANY runs is `voltro workflows cancel-many` /
`replay-many`. Both are a DRY RUN until `--commit`, `--limit` is required (the cap
IS the blast radius; `truncated` says whether more matched), and a cancel needs a
`--reason` that lands on every affected run.

A model call inside a workflow: `aiStep` / `aiObjectStep` from
`@voltro/ai/workflow`. Pass `store: ctx.store` and the spend is recorded per step;
the prompt is journaled as a DIGEST unless you write `recordPrompt: 'full'`. Add
`offload: true` and the run SUSPENDS instead of holding a worker while the model
thinks — a dispatcher owns the call and resumes the run. Costs one suspend/resume
round trip (~250ms), so offload the slow calls and leave a fast classification
inline.

**`idempotencyKey` is the execution's IDENTITY, permanently — not a dedupe
window.** A second start with the same key replays the first run's result
forever; after it completes the key is SPENT and a genuinely new invocation is a
silent no-op. So it must vary per unit of work (`` `tour:${rowId}:${editedAt}` ``,
not `` `tour:${rowId}` ``), while a flow-control `key` — the RESOURCE runs
compete for — stays stable. Those are two different fields; conflating them is
what makes "I need to re-arm a key" feel like a missing feature.

A start can now come back `queued` / `dropped` / `skipped` with
`executionId: null`, so narrow on `handle.status` before using the id.

### Pick the SERVER primitive (inside a handler)

The four above answer "which FILE do I write". This answers "what do I write
INSIDE it" — the part where hand-rolled boilerplate actually accumulates. Every
line below replaces something real apps write by hand hundreds of times.

1. **Loading one row you expect to exist?** → **`.one()`**
   (`ctx.store.select('t').where('id', x).one()`). It fails with the typed
   `NoRowFound` when there is no row — AND when there is more than one, so a
   filter that stops being unique fails loudly instead of returning an
   arbitrary row. `NoRowFound` is declarable in the descriptor's `error:` union.
   Nullable variant: **`.first()`** / **`.maybeOne()`**.
   Never `const rows = await …; if (!rows[0]) throw new NotFound()`.
2. **Need related data — 2+ reads to assemble one result?** → declare
   **`relations()`** (`*.relations.ts`) and eager-load with **`.with({ posts: true })`**.
   That compiles to ONE JSON-aggregate query on every SQL dialect. Sequential
   `store.query` calls per parent row are an N+1 you are writing on purpose.
3. **Something must happen AFTER a mutation commits** (notify, webhook, sync)?
   → **`defineSubscriber`** (`*.subscribe.ts`, runs any handler on a table
   change) or **`defineReaction`** (`*.reaction.tsx`, fires a named agent or
   workflow; requires a `dedupeKey`, supports `rateLimit` / `costBudgetUsd`).
   Both are post-commit and receive the `old`/`new` diff. Calling a
   notify-helper at the tail of the mutation body instead makes the reactivity
   invisible — you can only find it by reading every executor. Both are
   best-effort, so genuinely critical delivery still belongs in a workflow.
4. **A roll-up / counter recomputed on every read?** → **`defineAggregate`**
   (`*.aggregate.ts`). Add `incremental:` for `count|sum|avg|min|max` group-bys
   and it is MAINTAINED on write rather than recomputed. `read({ where })`
   parameterises it, so one aggregate serves per-team / per-period slices.
5. **A permission check?** → declare **`guards:`** on the descriptor — it is
   enforced before the executor (and before a mutation's transaction opens) and
   fails with a typed `ScopeError`. **This is not optional: a wire-exposed
   procedure that declares neither `guards:` nor `openAccess:` is REFUSED AT
   BOOT** (`security.defaultDeny`, on by default). Two kinds, same array, all
   must pass:
   - `{ scope: 'notes:write' }` — may the caller do this AT ALL.
   - `{ action, resourceType, resource: (input) => input.id }` — may they do it
     to THIS ROW (relationship/ReBAC, against a `defineResourcePolicy`). Needs a
     registered `setTupleSource`; **everything unanswerable DENIES** (no source,
     no policy, no id, source throws).
   Guards are re-checked on every subscription delivery, so a revoked grant ends
   the stream instead of continuing to push rows. Only use `ctx.access`
   in-handler for checks that need LOADED data. A hand-written `requireScope(...)`
   at the top of every executor — or a hand-kept map from rpc tag to policy rule,
   which is fail-open by omission — is what `guards:` exists to delete.
   **If the procedure really is open to everyone, say so — do not invent a
   guard.** `openAccess: '<why anyone may call this>'` is the other accepted
   answer, and the reason is required. Reaching for a scope every caller already
   holds satisfies the gate, reads as protection, and enforces nothing. A
   procedure only SERVER code calls wants neither: `internal: true` takes it off
   the wire entirely (and then `openAccess` is refused — there is no wire to
   decide about). `voltro doctor` lists every undecided procedure with its file.
6. **Which ROWS may the caller see** (not: may they make the call)? →
   **`setRowFilter`** — a predicate derived from the Subject, AND-merged into
   EVERY read of that table, including every subscription delivery. `guards:`
   and this answer different questions and the pair is easy to conflate:
   - `guards:` → *may I call this procedure?* → a typed `ScopeError`.
   - `setRowFilter` → *which rows may I see?* → the rows are simply absent.
   A `WHERE ownerId = me` in the list handler covers the query and **NOT** the
   stream, which is the failure this deletes. Register it from a
   `*.startup.tsx`; its `load` must read through an UNFILTERED store (the boot
   store / the `database` handle) — reading through the filtered one applies the
   filter to itself and blows the stack. Depth → **`authentication`**.
7. **Storing a token, secret, or credential in a column?** → **`.encrypted()`**
   on the column. Boot fails loudly if no cipher is configured, so an
   `.encrypted()` column can never silently persist plaintext.
8. **Writing an Effect-form handler?** → `const store = yield* EffectStore` —
   its failures land on the typed error channel. `Effect.promise(() =>
   ctx.store.query(…))` throws that channel away and turns a store failure into
   a defect.
9. **Cursor pagination?** → **`paginateBy(descriptor, column, cursor, limit,
   direction?)`** (or **`paginateById`**, its `id`-column shorthand). Not a
   hand-rolled limit+1 / slice / `hasMore` triple. `direction` flips the
   COMPARISON as well as the sort — a `desc` feed pages with `<`.
10. **Assembling data whose SHAPE depends on the data** (a tree walk where each
   level's ids come from the level above)? → **`ctx.load` / `ctx.loadMany`** —
   same-tick reads of one table coalesce into one `WHERE id IN (...)`, so the
   walk costs one query per LEVEL. Use `relations()` + `.with()` whenever the
   shape IS static; this is the fallback, not the default.
11. **A mutation must cause an EXTERNAL side effect** (webhook, Jira sync,
   payment)? → **`ctx.outbox.enqueue(effect, payload)`** + a
   **`defineOutboxHandler`** in a `*.outbox.ts`. The enqueue writes through the
   mutation's TRANSACTION, so the intent commits with the write or not at all;
   delivery happens after commit, with backoff and a dead-letter. Do NOT call
   the remote from the mutation (not transactional), and do not hand-build a
   deliveries table + drain cron — that IS this primitive.
12. **Calling a third party ON BEHALF OF A USER** (their Jira token, their Slack
   grant)? → **`defineConnection({ id, kind: 'oauth2' | 'pat' })`** in a
   `*.connection.ts`, then **`ctx.connections.get(id)`**. One declaration yields
   the encrypted per-user token store, the OAuth authorize/callback pair,
   refresh-before-use, and the `useConnection` + `<ConnectAccount>` connect UI.
   The credential is bound to the REQUEST's subject — there is no parameter for
   another subject. Do NOT hand-build a per-user token table, an OAuth action
   pair, or a "credentials resolver" over your own schema.

| You're about to write | Reach for instead |
|---|---|
| `const rows = …; if (!rows[0]) throw new NotFound()` | `.one()` |
| 3+ sequential `store.query` to assemble related data | `relations()` + `.with()` |
| a notify/webhook helper called at the end of a mutation | `defineSubscriber` / `defineReaction` |
| `WHERE ownerId = me` in every list handler | `setRowFilter` (covers the SUBSCRIPTION too) |
| a counter recomputed by scanning rows on every read | `defineAggregate` (+ `incremental:`) |
| `requireScope(...)` as the first line of every executor | `guards:` on the descriptor |
| a scope every caller holds, added to make a boot gate pass | `openAccess: '<why it is open>'` — the honest spelling |
| a token/secret column written as plain text | `.encrypted()` |
| `Effect.promise(() => ctx.store.query(...))` in an Effect handler | `yield* EffectStore` |
| hand-rolled limit+1 / `hasMore` cursor paging | `paginateBy` / `paginateById` |
| a loop issuing one `store.query` per node of a walk | `ctx.load` / `ctx.loadMany` |
| a deliveries table + drain cron + retry worker | `ctx.outbox.enqueue` + `defineOutboxHandler` |
| a per-user `integrationTokens` table + OAuth actions + a refresh check | `defineConnection` + `ctx.connections.get(id)` |
| `row['name'] as string` on every field you read | nothing — `ctx.store.query(database.t….descriptor)` already returns the table's row type. A cast here means the descriptor was hand-built, or you are casting out of habit |

Deep dive: **`data.md`** (`.one()`, subscribers, reactions, aggregates),
**`database/querying.md`** (relations + `.with`), **`authentication.md`**
(`guards:`, resource policies), **`plugins/governance.md`** (`.encrypted()`),
**`data.md`** (`defineConnection`, the credentials vault).

### Bind the api ONCE — typed hooks, not string tags

Before any of the rubric below: app code calls the api through a **typed
binding**, not through `@voltro/client`'s tag-taking hooks.

```ts
// src/lib/api.ts — one file, once per api
import { createHooks } from '@voltro/client'
import type { AppProcedures } from '@acme/api/rpcGroup'   // generated by codegen

export const { useSubscription, useMutation, useAction } = createHooks<AppProcedures>('app')
```

`'app'` is the key the web app's `app.config.ts` gave the api in `apis:` — spelled
once here, never at a call site again. Then:

```tsx
import { useSubscription, useMutation } from '../lib/api'

const { data } = useSubscription('notes.list')   // rows inferred — NO `<T>`
const create = useMutation('notes.create')       // input + output inferred
```

Do NOT write `useSubscription<ReadonlyArray<Note>>('app', 'notes.list')`. That
form spells the api name at every call site, leaves the tag an unchecked string
(a typo is a runtime error), and the `<T>` is an ASSERTION nothing compares
against the server — a hand-kept mirror of a type the generator already knows.
With the binding, a wrong tag, the wrong hook for the tag's kind, a missing
required input, and a wrongly-shaped input are all compile errors, and the row
type — including the client's `optimistic` marker — needs no annotation.

Destructure the result; do not `export const api = createHooks(...)` and call
`api.useSubscription(...)`. React's rules-of-hooks lint only sees a member call
as a hook when the object is PascalCase, so the namespace form silently turns
the hook lint off everywhere.

The tag-taking hooks stay for code that only learns the tag at RUNTIME — plugin
web bindings, libraries shipped against an unknown app. That is not app code.

### Pick the CLIENT primitive (decision rubric)

`useSubscription` / `useMutation` / `useAction` (from your `src/lib/api.ts`
binding above) are the transport — they are NOT the whole client surface. The framework ships a **headless primitive for every
common UI job**, each derived from the descriptor Schema you already wrote. Reach
for one BEFORE hand-rolling; ask in order:

1. **A form / any typed write UI?** → **`useFormBinding`** — fields + validation
   derived from the mutation's input Schema, and a server
   `ValidationError({ field })` routes itself to that field. Not per-field `useState`.
2. **A table / list with sort, filter, pagination?** → **`useDataTable`** (columns
   from the query's output Schema, live rows). **`useQueryFilters`** builds filter
   controls from the query's INPUT Schema; **`useQueryField`** is the query-bound picker.
3. **A file upload?** → **`useUpload`** (progress, cancel, provider-agnostic).
   Not `FileReader` → base64 → action.
4. **Gating UI on permission?** → **`useCan`** / **`useCanAny`** (scopes, from
   `@voltro/client`, fed by `<PermissionProvider scopes>`) or
   **`useResourceCan`** / **`useResourceCans`** (per-resource ReBAC, reactive).
   These live in the client, not behind a plugin — scopes are a framework
   concept, so gating a button never requires installing rbac.
5. **A value derived from one or more subscriptions?** → **`useDerived`**
   (dependency-tracked, referentially stable). Not a hand-maintained `useMemo` dep array.
6. **A very large list?** → **`useWindowedSubscription`** (subscribe to the visible
   window only, not a million rows).
7. **Debounced input?** → **`useDebounced`**; **`useAsyncValidation`** for checks
   that need the DB (uniqueness, cross-row) — live, over a query binding.
8. **A loading placeholder?** → **`useFormSkeleton`** / **`useTableSkeleton`** —
   shaped like the REAL data, from the same Schema.
9. **Offline writes · undo · dry-run · "why is this value here?"** → **`useOutbox`**,
   **`useUndoLog`**, **`usePreview`**, **`useProvenance`**.
10. **Global rpc-error handling?** → **`useOnRpcError`** + **`reportClientError`**.

| You're about to write | Reach for instead |
|---|---|
| per-field `useState` + `isSubmitting` + `resetForm()` | `useFormBinding` |
| shadcn `<Table>` + per-table sort/filter state | `useDataTable` |
| `FileReader` → base64 → action → `storage.put` | `useUpload` |
| `useMemo` fanning in 3+ subscriptions | `useDerived` |
| `setTimeout` debounce inside `useEffect` | `useDebounced` |
| `data === undefined ? <Skeleton/> : …` | `useFormSkeleton` / `useTableSkeleton` |
| a hand-kept page-scope check | `useCan` / `useResourceCan` |
| a hand-rolled presence roster / notification inbox / feature flag | `@voltro/plugin-presence` / `-notifications` / `-flags` |

Deep dive: **`schema-driven-ui.md`** (forms, tables, pickers, filters, skeletons),
**`reference.md`** (the complete hook list). If you are hand-writing more than ~20
lines of generic UI plumbing, stop and check those two first.

## Project layout

One OR MORE **projects** live under `apps/`. A project (`project.json`) is the
cloud-binding unit; it groups apps that ship together — an `api/` (backend) and
a `web/` (frontend).

```
<repo>/                         # pnpm-workspace root
├── apps/<projectName>/
│   ├── project.json
│   ├── api/
│   │   ├── app.config.ts       # { type:'api', store, cache, plugins, … }
│   │   ├── database/*.entity.ts + index.ts
│   │   ├── queries/  mutations/  actions/  workflows/  schedules/
│   │   └── rpcGroup.generated.ts   # codegen — do NOT edit
│   └── web/
│       ├── app.config.ts       # { type:'web', apis, theme, locales, … }
│       └── src/pages/**/page.tsx    # a DIRECTORY is a route segment
├── packages/                   # shared libs
└── AGENTS.md / CLAUDE.md       # this file
```

Never hand-create a project tree — run `node scripts/new-project.mjs`.
Discovery is **file-convention based**; nothing is registered manually (except
public REST routes, declared via `restRoutes` in `app.config.ts`).

### File conventions (the discovery map)

| Convention | What it is |
|---|---|
| `*.query.ts` + `*.query.server.ts` | streaming reactive query |
| `*.mutation.ts` + `*.mutation.server.ts` | transactional unary write |
| `*.action.ts` + `*.action.server.ts` | non-transactional external I/O |
| `*.stream.ts` + `*.stream.server.ts` | server→client stream (e.g. AI tokens) |
| `*.event.ts` | ephemeral fan-out event (`defineEvent`; no `.server` half) |
| `*.workflow.tsx` + `*.workflow.server.tsx` | durable multi-step work |
| `*.trigger.tsx` | domain event → workflow |
| `*.cron.tsx` | scheduled job (single file) |
| `*.subscribe.ts` | best-effort post-commit reaction to a table |
| `*.reaction.tsx` | standing reactive agent |
| `*.aggregate.ts` | scheduled materialised query |
| `*.startup.tsx` | run-once boot hook holding a resource |
| `*.seed.ts` | boot/seed data |
| `*.webhook.tsx` | incoming/outgoing webhook |
| `*.email.tsx` | React-Email template |
| `*.agent.tsx` + `*.agent.server.tsx` | server-side LLM chat |
| `*.tool.tsx` | tool an agent can call |
| `*.entity.ts` / `*.schema.ts` / `schema.ts` | one table per file |
| `page.tsx` (under `src/pages/`) | the route its DIRECTORY serves + `page.test.tsx` |
| `*.component.tsx` | exactly ONE component (+ types) |
| `*.component.ui.tsx` | presentational: one component, READS only — never writes |
| `*.hook.ts` | exactly ONE `use*` hook (+ types) |
| `*.types.ts` | types only — zero runtime exports |
| `*.internal.ts` | only its own directory subtree may import it |
| `*.fixture.ts` | test material — no production path may reach it |
| `*.tracking.ts` | the ONLY place analytics may be called from |
| `*.store.ts` | exactly ONE `defineStore` — client state, never server state |
| `*.client.ts` | declares itself + its transitive imports browser-safe |

**A DIRECTORY is a route segment, and its route is `page.tsx`.**

```
src/pages/page.tsx                  → /
src/pages/pricing/page.tsx          → /pricing
src/pages/users/[id]/page.tsx       → /users/[id]
src/pages/docs/[...slug]/page.tsx   → /docs/<anything>
```

A parameter is a DIRECTORY name (`[id]/`), never a filename. Beside `page.tsx`
sit the other reserved names its segment owns — `layout.tsx`, `error.tsx`,
`loading.tsx`, `not-found.tsx` — plus its `page.test.tsx` and any co-located
components. **Anything in that folder that is not one of the reserved names is
structurally not a route**, so put a component next to the page that uses it
rather than in a distant `components/` directory.

A `page.tsx` outside `src/pages/` never routes. A page MUST default-export its
component — without it `voltro dev` refuses to boot and names the file.

Do NOT write `x.page.tsx`, `index.page.tsx` or `[id].page.tsx` — those were the
0.15.0 spelling and route nothing now.

**Every suffix above is a CONTRACT, not a label.** Something else depends on each
promise, and `voltro doctor` enforces all of them:

- `*.component.ui.tsx` may read (`useT`, `useTFn`, `useCan`, `usePermissions`,
  the `@voltro/i18n` formatters) but must never import a write hook
  (`useMutation`, `useAction`, `useUpload`, …). That is what lets a caller render
  it ten thousand times in a list, reuse it across features, and prerender it
  without reading its source. Lift the write into the owning `*.component.tsx`
  and pass a handler down.

  **Reading is a recommendation, not a grudging allowance — call the hook, do
  not take a prop.** Threading `t` / `formatDate` / `useCan` through props is
  prop-drilling: it grows the prop surface with every formatter, has to be
  restated in every intermediate signature and every test, and does NOT make the
  component more reusable — it makes every call site worse. The one argument for
  the prop, "it renders without a provider", is already paid for the moment any
  test mounts one. Pick one way per component head: a `t` prop beside a
  formatter hook is the worst of both.
- `*.internal.ts` is the promise that refactoring inside that directory breaks
  nobody. An import from another subtree revokes it.
- `*.types.ts` having no runtime export is what makes importing it free and makes
  it impossible for it to sit in a runtime import cycle.
- `*.store.ts` holds ONE `defineStore`. Reads go through a selector
  (`wizard.use((s) => s.step)`) — there is no whole-state read, because a
  component holding all of it re-renders on every field. Scope a per-entity
  instance with a KEY (`{ key: orderId }`), never a Provider. Seed it for SSR
  from a loader with `seedStore(...)`; it rides the hydration payload the router
  already writes. An action that writes SEVERAL fields wraps them in
  `store.batch('label', () => …)` — one notification, one devtools entry, one
  undo step, and a rollback if it throws; `await` first, then batch (an async
  callback is an error). `{ persist: { key } }` survives a reload.
  NEVER put server data in a store: a subscription is already
  live, and a copy is not — the page then shows the stale one.
- `*.tracking.ts` is where every `defineTracking(...)` spec lives — the event
  catalogue, so "what do we send to third parties" is a file listing rather than
  archaeology. A component wires one up with `useTracking(spec, props, sink)`; it
  NAMES a spec, it never declares one. (`useTracking` is a hook, so it is not
  confined — only the declaration is.)
- Do NOT rename a file to `*.component.ui.tsx` just because it happens not to
  write today. The suffix is a promise about what it MAY do.

**These rules apply to code YOU write — never to vendored code.** A shadcn
component (`npx shadcn add`) follows shadcn's conventions and is overwritten by
the next `add`; renaming it breaks their convention and is undone next run. A
directory is exempt when the app's `components.json` names it (`aliases.ui`) or
it carries a `.voltro-vendored` file whose first line says where the code came
from. Never add that marker to a directory of your own code — the point of the
rules is that everything we author follows them.

**Write the test in the same step as the code — always, for every primitive.**
Not "afterwards", not in a cleanup pass: the filename is derivable
(`users/[id]/page.tsx` → `users/[id]/page.test.tsx`,
`Card.component.tsx` → `Card.component.test.tsx`,
`order.mutation.server.ts` → `order.mutation.server.test.ts`), so there is
nothing to decide. A change is not finished until its test exists and passes.

Two traps worth naming, because a green run can hide both:

- **`voltro test` transpiles without type-checking.** A type error in a TEST file
  passes the suite and fails the package's `typecheck`. Run both.
- **Never fake a service to make a test pass.** A test that only re-asserts its
  own mock's return value is worse than no test — it reports coverage it does not
  have. Assert the descriptor shape instead and cover the real path elsewhere.

## app.config.ts

One default export per app. Shapes differ by `type`:

```ts
// api
export default { type: 'api' as const, name: 'myApi',
  store: 'postgres' as const,   // 'postgres'|'mysql'|'mariadb'|'mssql'|'sqlite'|'turso'|'memory'
  cache: 'redis' as const,      // 'memory' (default) | 'redis'
  plugins: [],                  // storagePlugin(), mailPlugin(), …
  // auth, env, secrets, apiKeys, idempotency, dormancy, layers — see configuration.md
}
// web
export default { type: 'web' as const, name: 'myWeb',
  theme: 'system' as const,     // pre-paint, no flash — never toggle `dark` from useEffect
  locales: ['en','de'] as const, defaultLocale: 'en' as const,
  apis: { app: { package: '@app/api' } },   // name → spec; the hook lookup key
}
```

Env, secrets, auth strategies, layers, plugins config → **`configuration.md`**.
`store: 'memory'` loses data on restart — default to `postgres` for real work.

**NEVER hardcode a secret, not even as a fallback.** Not
`process.env.X ?? 'dev-only-change-me'`, not a value committed to `.env`. A
hardcoded fallback means every environment that forgot to configure the
variable shares one key that is readable by anyone with the repo — and it looks
completely healthy until someone forges a session. Declare it instead:

```ts
env: defineEnv({
  VOLTRO_SESSION_SECRET: envVar.secret({ generate: 'base64url' }),  // ours → minted
  STRIPE_SECRET_KEY:     envVar.secret({ minLength: 20 }),          // theirs → must be set
})
```

`generate` makes `voltro dev` mint a unique per-project value into a gitignored
`.env.local` on first boot, so local development needs no placeholder at all.
Omit it for third-party credentials — an invented API key authenticates nobody,
and failing the boot gate is the useful outcome. Deployment values come from
`voltro secret generate <purpose>`; `voltro serve` refuses to start without them.

Also: don't read `process.env` for a secret at config time to pass it to a
plugin. `app.config.ts` is evaluated BEFORE the env gate and before minting, so
that read is what pushes you into writing a fallback. `authRoutesPlugin` and
`voltroPasswordStrategy` resolve `VOLTRO_SESSION_SECRET` themselves — omit
`secret:` entirely.

## The browser/server boundary (load-bearing — verify when you touch it)

Codegen pulls every **descriptor** (`*.query.ts` / `*.mutation.ts` /
`*.action.ts` / `*.stream.ts` + every `*.workflow.tsx`) into
`rpcGroup.generated.ts`, which the **web client loads value-level**. So every
descriptor — **and every module it transitively imports** — must be
browser-safe: no `node:*`, no `@voltro/database` / the `database` handle, no
`@voltro/ai` / cluster / plugins, no `@voltro/protocol/session`.

The real trap is **transitive**: a descriptor importing a shared typed-error or
helper from a `lib/` file that ALSO imports the `database` handle drags the whole
server graph (and any `node:crypto` behind it) into the browser bundle. Keep
browser-safe symbols (typed errors, Schemas, pure helpers) in files with ZERO
server imports; put DB-backed guards/helpers in `.server.ts` or a server-only
module. Several packages ship a browser-safe subpath for exactly this (e.g.
`@voltro/database/wire`, `@voltro/plugin-multitenancy/guard`,
`@voltro/plugin-webhooks/errors`, `@voltro/ai/events`) — import typed errors /
Schemas from there in descriptors, never from the package root.

A consequence worth stating, because it is not obvious: a descriptor's `output`
can never be **derived from a table**. `rowSchema(table)` / `columnSchema(def)`
take the table as a VALUE, so reaching one means importing your
`database/schema.ts`, which imports `@voltro/database` — the boot aborts with
the import chain. Write `output` as your own `Schema.Struct` and take the field
schemas from `@voltro/database/wire` (`timestampMs` for a `timestamp()` column,
`timestampMsOrNull` when it is `.nullable()`, `Schema.optional(timestampMs)`
when the field may be absent). `rowSchema` is a row CODEC for server-only code
(`*.server.ts`, `*.seed.ts`, jobs, scripts) — file exports, queue payloads,
validating seed data — not a descriptor helper.

**Symptom of a leak:** the web app fires hundreds of module requests / tens of MB
on first load, or crashes with `Module "node:crypto" has been externalized for
browser compatibility`. Diagnose by walking what `rpcGroup.generated.ts` imports
+ each descriptor's transitive imports — none may reach a server-only module.

## Schema essentials

Tables are declared **one per `*.entity.ts` file**; `database/index.ts` builds
the `databaseHandle`. Two core tables underpin the mixins: **`actors`**
(framework-provided audit subject — do NOT declare it) and **`tenants`** (you
declare it). Mixins compose via `.with(...)` and auto-stamp bookkeeping columns:

```ts
export const todos = table('todos', { id: id(), title: text(), done: boolean().default(false) })
  .with(softDelete(), tenant())   // tenant() transitively requires audit()
```

- Every table is REACTIVE by default — nothing to opt into. `.nonReactive()`
  turns reactivity OFF for a table entirely: no subscriber fires, locally or
  across instances (the write still happens). Use it for a hot table nobody
  subscribes to; never for one a query reads — that subscription never fires,
  and `voltro dev` warns.
- `id()` defaults to a branded **TypeID** (`todo_…`). Insert auto-injects it.
- Indexes are **table-level** (`.index('name', [...])`) — there is NO
  column-level `.index()`. Declare `.index([...])` AFTER `.with(...)` to address
  mixin columns. Index names must be unique across the WHOLE schema.
- Auto-stamping fills `createdAt/updatedAt/createdBy/...` + `tenantId` — don't
  set them by hand; don't add tenant filters in queries on `tenant()` tables
  (the runtime does it).

Column types, relations + `.with()` eager-loading, the query builder,
aggregations, FTS, vectors/RAG, migrations, dialect parity → the **`database/*`**
docs (see index). Async-vs-Effect handlers, `EffectStore`, typed store errors →
**`data.md`**.

## What may cross the wire (three ORTHOGONAL markers — do not substitute one for another)

A column is not "protected" or "unprotected". Three separate questions get three
separate markers, and using one to answer another's question is the mistake:

| Marker | Answers | Enforced by |
|---|---|---|
| `.serverOnly()` | may this value leave the server AT ALL? | **`voltro serve` fails to boot; `voltro doctor` exits non-zero.** `voltro dev` only warns |
| `.sensitive('class')` / `.safe()` | may it appear in a `voltro data export`? | the export's masking profile, **fail-closed** |
| `.encrypted()` | is it encrypted AT REST? | the store's codec |

```ts
export const users = table('users', {
  id:       id(),
  email:    text().sensitive('pii'),      // exportable only through a profile
  pinHash:  text().serverOnly(),          // never reaches a client, ever
  ssn:      text().encrypted().serverOnly(),  // both — they are not the same claim
})
```

- **`.serverOnly()` is the one that decides "leak / no leak".** A wire-reachable
  query that declares such a column in its OUTPUT does not reach production:
  `voltro serve` refuses to boot and `voltro doctor` exits non-zero, so the
  failure happens at deploy time rather than in a bundle. `voltro dev` only
  WARNS — mid-edit a refused boot is worse than the bug — so do not read a green
  dev boot as a clean audit. `VOLTRO_SERVER_ONLY=strict` makes dev fail too;
  put `voltro doctor` in CI if you want one gate that covers both.
- **`.encrypted()` is NOT an exposure marker.** It says the bytes at rest are
  encrypted; the runtime decrypts them for a handler, so an encrypted column
  flows to the client exactly like any other unless it is ALSO `.serverOnly()`.
  Reading `.encrypted()` as "safe to expose" is a category error, and a plausible
  one — say both when you mean both.
- **Masking is fail-closed**: an unclassified column blocks the export rather
  than passing through, so `.sensitive()` / `.safe()` is a decision you make once
  per column, not a filter you remember to apply.
- `crud.list` / `getById` / `create` / `update` are **redacted by construction** —
  they strip `serverOnly` columns for you. Hand-rolling the same CRUD is where
  that stripping gets forgotten.

Depth (classes, profiles, the export flow) → **`database/sensitivity`**; the
redacted CRUD surface → **`data/crud`**.

## Naming / RPC tags

- **camelCase** for vars, files, schemas, and rpc tags. The tag IS the wire
  identifier — collisions break codegen.
- Tag shape `<domain>.<action>` — `todos.list`, `messages.send`. The tag becomes
  a JS identifier (`todos.list` → `todosListRpc`); `-`/`_` are camelCased away,
  so `a-b.c` and `a.b.c` collide. Prefer plain camelCase segments.
- **Never name a tag identical to another tag's namespace prefix.** A workflow
  `name: 'ticketAnalysis'` alongside a query `ticketAnalysis.get` makes
  `client.ticketAnalysis` the workflow and hides the query. Nest the workflow
  (`ticketAnalysis.analyze`). `voltro dev` fails codegen on collisions.

## Auto-optimistic

Declare `target` on the mutation + `source` on the query and the client patches
the cached subscription automatically (insert prepends, update merges by id,
delete filters by id; reverts on failure). Override with `.withOptimistic(...)`,
disable with `.withoutOptimistic()` (payments, sends). Details → **`data.md`**.

## Generated files — do NOT edit

`apps/api/rpcGroup.generated.ts` (committed) and `apps/web/.framework/*`
(gitignored) are rewritten on every `voltro dev` boot. Add/rename/remove a
primitive → just save; the supervised dev loop respawns and regenerates.

## Anti-patterns

- **Don't poll / refetch.** Push-based; subscriptions stay live. Reaching for
  `setInterval(refetch)` means the subscription is wrong.
- **Don't bypass `defineQuery`/`defineMutation`/etc.** The descriptor IS the
  contract (types the client, drives codegen, tenant scoping, typed errors).
- **Don't import server-only modules from a descriptor — including transitively.**
  See the boundary section above.
- **Don't add `tenantId` filters on `tenant()` tables** — the runtime does it.
  But DO call `assertOwnTenant(input.tenantId, ctx.request.subject)` (from
  `@voltro/plugin-multitenancy/guard`) in custom mutations that write raw rows,
  and declare `error: TenantMismatch` — subscriptions are auto-scoped, writes are not.
- **Every wire-exposed procedure declares an access decision, or it does not
  boot.** `guards: [...]` (a check runs), `openAccess: '<why>'` (no check, on
  purpose, with the reason in the source), or `internal: true` (off the wire).
  `security: { defaultDeny: false }` in `app.config.ts` is the whole-app opt-out;
  there is no env var for it.
- **Gate authorization declaratively with `guards:`.** Add
  `guards: [{ scope: 'notes:write' }]` to `defineMutation`/`defineQuery`/`defineAction`
  — the framework enforces it BEFORE the executor (before the txn opens), fails
  with a typed `ScopeError` (auto-merged into the wire error union), and checks the
  caller's EFFECTIVE scopes (raw ∪ rbac roles). Guards are browser-safe DATA (scope
  strings + a pure `resource: (input) => id` extractor — never a server fn). Use the
  in-handler `ctx.access.has(scope)` / `yield* ctx.access.require(scope)` (or rbac's
  `permission()`) only for checks that need LOADED data (row ownership).
- **Don't store secrets in the schema or in `Subject`.** Declare env via
  `defineEnv` (`configuration.md`); carry only ids in `Subject`.
- **In production, `voltro build` BEFORE `voltro serve`.** A production
  (`NODE_ENV=production`) serve REQUIRES the precompiled serve bundle and fails
  loud if it's missing — production never transpiles on demand. The generated
  Dockerfiles already do `voltro build` then `voltro serve`; if you write your
  own prod start, build first. (`voltro dev` + a non-prod local `serve` still use
  tsx.) Depth: the deployment topic.
- **Don't copy prod data down unmasked.** `voltro data export` (and
  `--target api`) reads REAL rows — PII included. Copying prod → dev/stage MUST
  go through a masking profile (`--profile`; classify columns `.sensitive()` /
  `.safe()` — masking is **fail-closed**: an unclassified column blocks the
  export). The `POST /_voltro/admin/export` endpoint is a data-exfiltration
  surface — off unless `VOLTRO_DATA_TRANSFER_SECRET` is set, and that ONE secret
  also gates import, so never leak it or ship an unmasked dump. Depth: the
  data-transfer topic + `.sensitive()`/`.safe()` classification.
- **Don't trust `x-tenant` in production** — derive `tenantId` from the verified
  session in a real `AuthMiddleware`. The dev default is unauthenticated.
- **Don't do external I/O in a mutation** — it isn't transactional; use an action.
- **Don't read `window.location` in pages/layouts** — use `useLocation()`
  (`@voltro/web`); SSR has no `window`.
- **Don't toggle the `dark` class from `useEffect`** — set `theme` in
  `app.config.ts` (pre-paint, no flash).
- **Don't roll your own session token / ICU/i18n / job runner** — use
  `signSession`/`verifySession`, `@voltro/i18n`, `@effect/workflow`.
- **Don't add `bullmq`/`temporal`/`react-query`/SWR** — see Mental model.
- **Don't build a GraphQL layer over the stores.** It would bypass the
  reactivity path (source-based invalidation, per-delivery guards) — a second,
  dead read path. External consumers → REST + OpenAPI (`defineRestRoute` +
  `@voltro/plugin-openapi`); internal clients → RPC + live subscriptions.
- **Don't write `'use server'` / `'use client'` — there is no RSC.** Islands +
  loaders + streaming SSR are the model; those directives mark a boundary this
  framework does not have.
- **Don't split the app into microservices with service-to-service RPC.** One
  monolith process, scaled by running more replicas (`@effect/cluster` shards
  durable workflows across instances), is the architecture. An external system
  boundary is REST + OpenAPI; a reliable outbound effect is `ctx.outbox`.

## Plugins

Cross-cutting capabilities are added via `plugins: [...]` in `app.config.ts` (or
schema mixins via `.with(...)`). Each plugin's full API lives in its own README —
the index below links ONLY the ones this project has installed. Wiring patterns
that span 3 repos (dashboard panels) etc. are framework-internal — not here.

---

<!-- generated by voltro/scripts/gen-agent-docs.mjs — do not edit -->
## Deep reference (read on demand)

Each row below is a file to open WHEN you work on that topic — not loaded into
context by default. Topics live in the installed `@voltro/cli`; plugin depth is
each plugin's own README.

### Topics

| Topic | Open | Summary |
|---|---|---|
| **What's new in 0.52.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
| CLI | `node_modules/@voltro/cli/templates/agent-docs/cli.md` | The voltro CLI — every command, grouped by purpose, with the flags that actually matter. |
| Configuration | `node_modules/@voltro/cli/templates/agent-docs/configuration.md` | Typed, schema-validated environment variables with a structural public/secret boundary. Declare once in app.config.ts; read public vars in the browser, secrets only on the server — and let the build fail loudly if you ever cross the line. |
| Data | `node_modules/@voltro/cli/templates/agent-docs/data.md` | How Voltro's reactive data layer works — queries, mutations, actions, streams, all over one WebSocket with typed errors and tracked dependencies. |
| database.advancedQueries | `node_modules/@voltro/cli/templates/agent-docs/database/advancedqueries.md` | Btree, GIN, GiST, BRIN, HNSW — when to use what, plus partial + expression + composite indexes. |
| database.columnTypes | `node_modules/@voltro/cli/templates/agent-docs/database/columntypes.md` | Postgres-native ENUM types via dbEnum() — cheap ADD VALUE migrations, full type-narrowing. Falls back to CHECK constraints on other dialects. |
| database.hosting | `node_modules/@voltro/cli/templates/agent-docs/database/hosting.md` | Run Voltro on any hosted Postgres, MySQL, MariaDB, or SQL Server — Supabase, Neon, Vercel Postgres, Railway, Render, Fly.io, AWS RDS, PlanetScale, Azure SQL, and more. Connection strings, pooling, gotchas. |
| database.migrations | `node_modules/@voltro/cli/templates/agent-docs/database/migrations.md` | Voltro's planner-based migration system — diff your declared schema against the live DB, classify each change, refuse-to-apply anything risky without explicit intent. Dev auto-applies, prod refuses. |
| Database | `node_modules/@voltro/cli/templates/agent-docs/database/misc.md` | Branch the live schema and REHEARSE your migration on it — apply the plan to a throwaway copy, flag every lossy operation, prove it converges, drop the branch. Plus the branch primitive itself (namespace snapshot on Postgres, Neon copy-on-write fast-path). |
| Database | `node_modules/@voltro/cli/templates/agent-docs/database/overview.md` | How Voltro talks to Postgres — the schema DSL, the query builder, mixins, migrations, and the reactive engine's relationship to all of it. |
| database.querying | `node_modules/@voltro/cli/templates/agent-docs/database/querying.md` | Three cardinalities, one DSL. Declaration via relations(), eager loading via .with(), reactive invalidation via the two-stage dependency-graph + per-field pre-filter. |
| database.scaling | `node_modules/@voltro/cli/templates/agent-docs/database/scaling.md` | Transparent read-replica routing with read-your-writes (RYW) consistency, multi-instance RYW via Redis, region-aware replica selection, and the per-dialect adapter surface. |
| database.schema | `node_modules/@voltro/cli/templates/agent-docs/database/schema.md` | Five generation schemes — TypeID (default), ULID, numeric, Snowflake, custom. Decision matrix, auto-injection lifecycle, cursor pagination, branded TypeScript types. |
| database.seedsDialects | `node_modules/@voltro/cli/templates/agent-docs/database/seedsdialects.md` | Six SQL backends, one schema DSL. Decision matrix, configuration, boot-log shape, and the cross-dialect feature parity table the framework hides for you. |
| database.transactions | `node_modules/@voltro/cli/templates/agent-docs/database/transactions.md` | How ctx.store behaves inside mutations, workflows, and explicit transaction blocks. |
| Deployment | `node_modules/@voltro/cli/templates/agent-docs/deployment.md` | Voltro Cloud (coming soon) — the managed runtime for your Voltro project. Today the Free control plane registers + observes your self-hosted apps. |
| Internationalization | `node_modules/@voltro/cli/templates/agent-docs/internationalization.md` | Voltro's i18n layer (@voltro/i18n) — an opinionated wrap over react-intl, auto-wired from a single app.config.ts field, with cookie + Accept-Language locale resolution. |
| Introduction | `node_modules/@voltro/cli/templates/agent-docs/introduction.md` | Scaffold a Voltro project and boot it locally in under a minute. |
| Local-first & Mobile | `node_modules/@voltro/cli/templates/agent-docs/local-first-mobile.md` | "@voltro/local-first — CRDT text merge (crdtText/mergeCrdtStates), the offline sync-queue + SyncClient wire, presence/awareness, durable persistence, and the localFirst table mixin. Pure and browser-safe; the React hooks live behind a subpath." |
| Multi-tenancy | `node_modules/@voltro/cli/templates/agent-docs/multi-tenancy.md` | Multi-tenancy as a runtime primitive — the tenant() mixin, ctx.subject.tenantId, automatic read scoping, explicit write gates. |
| Observability | `node_modules/@voltro/cli/templates/agent-docs/observability.md` | OpenTelemetry tracing in Voltro — the auto-emitted spans for every primitive, span attributes and nesting, the three enabling modes (console / OTLP / buffer), and adding your own spans with Effect.withSpan. |
| Plugins | `node_modules/@voltro/cli/templates/agent-docs/plugins.md` | How Voltro plugins compose into the runtime, what they can intercept, the catalogue, and writing your own. |
| Reference | `node_modules/@voltro/cli/templates/agent-docs/reference.md` | The client-side hook surface, grouped by purpose. |
| Releases | `node_modules/@voltro/cli/templates/agent-docs/releases.md` | 0.35 through 0.38 in one pass — the boot-breaking access declarations, the stricter input handling, and the runtime behaviour that moved underneath you. |
| Routing | `node_modules/@voltro/cli/templates/agent-docs/routing.md` | Voltro's file-based router — pages, layouts, render modes, loaders, navigation, islands. The web side of the framework. |
| Scheduling | `node_modules/@voltro/cli/templates/agent-docs/scheduling.md` | Deployment-agnostic scheduled jobs in Voltro — one *.cron.tsx definition that runs unchanged on a single box, a multi-instance fleet, or an external scheduler. |
| Schema-driven UI | `node_modules/@voltro/cli/templates/agent-docs/schema-driven-ui.md` | Project the typed descriptor graph into UI — forms, tables, pickers, and reactive components, all bound to a descriptor with near-zero glue. |
| Security | `node_modules/@voltro/cli/templates/agent-docs/security.md` | How Voltro handles security — reporting a vulnerability, the supply-chain gates (dependency audit, inbound-license allowlist, SBOM), supported versions, and why self-hosting keeps your data yours. |
| templates.apiBackends | `node_modules/@voltro/cli/templates/agent-docs/templates/apibackends.md` | The minimal Voltro backend — app.config + schema + one streaming query + one tenant-guarded mutation. Tenant-aware out of the box. |
| templates.appShells | `node_modules/@voltro/cli/templates/agent-docs/templates/appshells.md` | A marketing landing page — hero, features, CTA. Static-rendered with zero JS on the wire by default. |
| templates.custom | `node_modules/@voltro/cli/templates/agent-docs/templates/custom.md` | Add a template the CLI can scaffold from — a directory under voltro-templates/apps/ with a manifest and the files to copy. |
| templates.mobile | `node_modules/@voltro/cli/templates/agent-docs/templates/mobile.md` | An Expo (React Native) app that is the third consumer of your api — the same typed hooks (useSubscription / useMutation), offline-first by default, typed deep links, and device registration for push. Expo owns Metro; voltro dev runs the sibling api. |
| Templates | `node_modules/@voltro/cli/templates/agent-docs/templates/overview.md` | Dozens of dogfooded starter templates ship with the framework — backend shapes, frontend shapes, a serverless function library, and an Expo mobile app. voltro list-templates is the authority; scaffold any of them with one CLI call. |
| templates.serverless | `node_modules/@voltro/cli/templates/agent-docs/templates/serverless.md` | A library of standalone *.serverless.ts functions — pure compute, request-header/geo, outbound HTTP, Web Crypto HMAC, an LLM call, status-controlled errors. Run with voltro serverless; ship to node / Cloudflare / Scaleway. No server, no port. |
| Testing | `node_modules/@voltro/cli/templates/agent-docs/testing.md` | Voltro's test story — the @voltro/testing package, the test pyramid (unit handlers/tools → workflow runner → e2e), and the voltro test / voltro e2e CLI commands. |
| Workflows | `node_modules/@voltro/cli/templates/agent-docs/workflows.md` | Durable Effect workflows in Voltro — what they are, when to use them, and the current runtime boundaries. |

### Plugins (seeder lists only the INSTALLED ones)

| Plugin | Open | Summary |
|---|---|---|
| ai-flows | `node_modules/@voltro/cli/templates/agent-docs/plugins/ai-flows.md` (or `node_modules/@voltro/plugin-ai-flows/README.md`) | Durable multi-step AI pipelines — deterministic or agentic, with human-in-the-loop, chaining, and cadence. Author flows in code (defineFlow) or as data (visual-editor rows); one engine runs both. |
| analytics-postgres | `node_modules/@voltro/cli/templates/agent-docs/plugins/analytics-postgres.md` (or `node_modules/@voltro/plugin-analytics-postgres/README.md`) | First-party lite AnalyticsSink — stores events in the main DataStore, cross-dialect, zero external infra. ~10M events/day ceiling. |
| atlassian | `node_modules/@voltro/cli/templates/agent-docs/plugins/atlassian.md` (or `node_modules/@voltro/plugin-atlassian/README.md`) | JiraService + ConfluenceService over the Atlassian REST / Greenhopper / Agile APIs, with a per-subject PAT resolver, transient retry, SSRF guard, an avatar proxy, and per-tenant caching. |
| audit | `node_modules/@voltro/cli/templates/agent-docs/plugins/audit.md` (or `node_modules/@voltro/plugin-audit/README.md`) | Mutation audit log — sinks (console/memory/custom), include/exclude filters, the audit() mixin, testing with the memory buffer. |
| auth | `node_modules/@voltro/cli/templates/agent-docs/plugins/auth.md` (or `node_modules/@voltro/plugin-auth/README.md`) | Password + session-cookie auth — overview. Full docs in the Authentication section. |
| auth-auth0 | `node_modules/@voltro/cli/templates/agent-docs/plugins/auth-auth0.md` (or `node_modules/@voltro/plugin-auth-auth0/README.md`) | Auth0 AuthStrategy — verifies Auth0-issued JWTs via the tenant's JWKS (no client secret) and maps a namespaced custom claim → tenantId. |
| auth-clerk | `node_modules/@voltro/cli/templates/agent-docs/plugins/auth-clerk.md` (or `node_modules/@voltro/plugin-auth-clerk/README.md`) | Clerk AuthStrategy — verifies Clerk-issued __session JWTs via the Frontend API JWKS (no secret key) and maps org_id → tenantId. |
| auth-kinde | `node_modules/@voltro/cli/templates/agent-docs/plugins/auth-kinde.md` (or `node_modules/@voltro/plugin-auth-kinde/README.md`) | Kinde AuthStrategy — verifies Kinde-issued JWTs via JWKS at the configured issuer (no client secret) and maps org_code → tenantId. |
| auth-oidc | `node_modules/@voltro/cli/templates/agent-docs/plugins/auth-oidc.md` (or `node_modules/@voltro/plugin-auth-oidc/README.md`) | Generic OIDC AuthStrategy — verifies any OpenID-Connect IdP's JWTs via JWKS (Okta, Keycloak, Cognito, Azure AD, Google Workspace). Discovery or explicit JWKS URL; maps a claim → tenantId. |
| auth-social | `node_modules/@voltro/cli/templates/agent-docs/plugins/auth-social.md` (or `node_modules/@voltro/plugin-auth-social/README.md`) | First-party Sign in with Google / GitHub / Apple — mandatory PKCE + state, JWKS-verified ID tokens, a deliberate account-linking policy, no identity vendor. |
| auth-supabase | `node_modules/@voltro/cli/templates/agent-docs/plugins/auth-supabase.md` (or `node_modules/@voltro/plugin-auth-supabase/README.md`) | Supabase Auth (GoTrue) AuthStrategy — verifies Supabase-issued JWTs via the project's JWKS and maps app_metadata.tenant_id → tenantId. |
| auth-workos | `node_modules/@voltro/cli/templates/agent-docs/plugins/auth-workos.md` (or `node_modules/@voltro/plugin-auth-workos/README.md`) | WorkOS AuthStrategy — verifies WorkOS AuthKit / SSO JWTs via JWKS (no API key) and maps org_id → tenantId. |
| billing | `node_modules/@voltro/cli/templates/agent-docs/plugins/billing.md` (or `node_modules/@voltro/plugin-billing/README.md`) | Subscriptions, plans, entitlements, and usage metering over a pluggable provider (Stripe + mock). Money is integer minor units. |
| broadcast | `node_modules/@voltro/cli/templates/agent-docs/plugins/broadcast.md` (or `node_modules/@voltro/plugin-broadcast/README.md`) | Cross-replica reactivity over a pub/sub bus (Redis / NATS) for non-postgres dialects — closes the single-instance gap so a write on one pod surfaces on another. |
| cdc-out | `node_modules/@voltro/cli/templates/agent-docs/plugins/cdc-out.md` (or `node_modules/@voltro/plugin-cdc-out/README.md`) | Declaratively mirror table changes outward to external sinks (webhook, plus a CdcSink interface for custom sinks) through a durable outbox — ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered. |
| clickhouse | `node_modules/@voltro/cli/templates/agent-docs/plugins/clickhouse.md` (or `node_modules/@voltro/plugin-clickhouse/README.md`) | Production-grade OLAP AnalyticsSink over ClickHouse — self-hosted or ClickHouse Cloud — for billions of events with millisecond aggregates. |
| datadog | `node_modules/@voltro/cli/templates/agent-docs/plugins/datadog.md` (or `node_modules/@voltro/plugin-datadog/README.md`) | Agentless Datadog metrics exporter — pushes the unified Metrics-API to Datadog's /api/v2/series HTTP intake. |
| deactivation | `node_modules/@voltro/cli/templates/agent-docs/plugins/deactivation.md` (or `node_modules/@voltro/plugin-deactivation/README.md`) | 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. |
| duckdb | `node_modules/@voltro/cli/templates/agent-docs/plugins/duckdb.md` (or `node_modules/@voltro/plugin-duckdb/README.md`) | Embedded DuckDB AnalyticsSink — real column-store OLAP in-process, no external service to run. |
| flags | `node_modules/@voltro/cli/templates/agent-docs/plugins/flags.md` (or `node_modules/@voltro/plugin-flags/README.md`) | Feature flags — per-subject / per-tenant targeting, deterministic % rollouts, kill-switch, declarative rpc gating + client UI gating. |
| governance | `node_modules/@voltro/cli/templates/agent-docs/plugins/governance.md` (or `node_modules/@voltro/plugin-governance/README.md`) | Data governance — retention TTL sweep, GDPR export + erasure, consent ledger, and field-level encryption for .encrypted() columns. Builds on audit + soft-delete. |
| licensing | `node_modules/@voltro/cli/templates/agent-docs/plugins/licensing.md` (or `node_modules/@voltro/plugin-licensing/README.md`) | Offline-verified EdDSA license keys + cloud-issued entitlement snapshots that feed plugin-billing; pricing decided server-side, never baked into a published version. |
| logship | `node_modules/@voltro/cli/templates/agent-docs/plugins/logship.md` (or `node_modules/@voltro/plugin-logship/README.md`) | Ship structured logs to Better Stack / Axiom / Loki / any HTTP sink — batched, redacted, fail-soft. Rides the framework log-sink hook. |
| mail | `node_modules/@voltro/cli/templates/agent-docs/plugins/mail.md` (or `node_modules/@voltro/plugin-mail/README.md`) | Transactional email — Resend / Postmark / SendGrid / SMTP, React-Email templates with auto-discovery, per-tenant suppression, bounce/complaint handling, durable delivery via workflows. |
| moderation | `node_modules/@voltro/cli/templates/agent-docs/plugins/moderation.md` (or `node_modules/@voltro/plugin-moderation/README.md`) | Moderate user content before it commits — keyword denylist or AI provider, block or flag via rpc interceptor, plus an in-handler redact helper. |
| multitenancy | `node_modules/@voltro/cli/templates/agent-docs/plugins/multitenancy.md` (or `node_modules/@voltro/plugin-multitenancy/README.md`) | The tenant() schema mixin (auto-scoped reads, auto-filled inserts, tenant-resolved keyed writes), the assertOwnTenant write-guard, and the typed TenantMismatch error. |
| notifications | `node_modules/@voltro/cli/templates/agent-docs/plugins/notifications.md` (or `node_modules/@voltro/plugin-notifications/README.md`) | Unified notifications — one send API across email / Slack / SMS / push / in-app, with per-user channel preferences, an in-app inbox, and delivery records. |
| openapi | `node_modules/@voltro/cli/templates/agent-docs/plugins/openapi.md` (or `node_modules/@voltro/plugin-openapi/README.md`) | Generate an OpenAPI 3.1 spec + Swagger-UI docs from your defineRestRoute descriptors and (opt-in) your rpc procedures. Your routes ARE the API docs — nothing hand-maintained. |
| postgis | `node_modules/@voltro/cli/templates/agent-docs/plugins/postgis.md` (or `node_modules/@voltro/plugin-postgis/README.md`) | Postgres-native geography / geometry columns, geometry constructors (point/line/polygon/multi/GeoJSON), spatial predicates (ST_DWithin, ST_Within, ST_Contains, ST_Intersects, ST_Buffer, bbox &&), ST_Distance projection + <-> KNN ordering, and GiST indexes via expressionIndex. Postgres-only by design. |
| posthog | `node_modules/@voltro/cli/templates/agent-docs/plugins/posthog.md` (or `node_modules/@voltro/plugin-posthog/README.md`) | Track-only AnalyticsSink that forwards events to PostHog's /capture endpoint — compose it with a second sink that owns reads. |
| presence | `node_modules/@voltro/cli/templates/agent-docs/plugins/presence.md` (or `node_modules/@voltro/plugin-presence/README.md`) | Ephemeral realtime presence — who's online in a channel, with heartbeat, live roster, and per-member metadata (status, cursor). Works cross-instance. |
| prometheus | `node_modules/@voltro/cli/templates/agent-docs/plugins/prometheus.md` (or `node_modules/@voltro/plugin-prometheus/README.md`) | Prometheus exporter — scrapes the unified Metrics-API at GET /metrics in text exposition format. |
| ratelimit | `node_modules/@voltro/cli/templates/agent-docs/plugins/ratelimit.md` (or `node_modules/@voltro/plugin-ratelimit/README.md`) | Per-endpoint, per-subject and per-tenant request limits via the rpc interceptors. Sliding-window / fixed-window / token-bucket, memory / postgres / redis stores. |
| rbac | `node_modules/@voltro/cli/templates/agent-docs/plugins/rbac.md` (or `node_modules/@voltro/plugin-rbac/README.md`) | Roles + permissions + the permission() handler guard. Roles compile to scopes. |
| row-history | `node_modules/@voltro/cli/templates/agent-docs/plugins/row-history.md` (or `node_modules/@voltro/plugin-row-history/README.md`) | Full row history + time-travel. audit() records who/when; row-history records what-changed-to-what — a value snapshot of every row on every write, with as-of queries. |
| scim | `node_modules/@voltro/cli/templates/agent-docs/plugins/scim.md` (or `node_modules/@voltro/plugin-scim/README.md`) | SCIM 2.0 provisioning — Users + Groups REST endpoints so an enterprise IdP (Okta, Entra, OneLogin) can create / update / deactivate users in your app. |
| search | `node_modules/@voltro/cli/templates/agent-docs/plugins/search.md` (or `node_modules/@voltro/plugin-search/README.md`) | Keep an external search index (Typesense / Meilisearch / Algolia) in sync with your tables via the ChangeEvent tap, query it tenant-scoped through a typed action + hook. |
| sentry | `node_modules/@voltro/cli/templates/agent-docs/plugins/sentry.md` (or `node_modules/@voltro/plugin-sentry/README.md`) | Deep Sentry integration — errors correlated to the distributed trace, breadcrumbs from the log sink, and opt-in performance traces. |
| soft-delete | `node_modules/@voltro/cli/templates/agent-docs/plugins/soft-delete.md` (or `node_modules/@voltro/plugin-soft-delete/README.md`) | The softDelete() schema mixin — deletedAt / deletedBy columns, delete() redirected to an UPDATE, default reads filtered, hardDelete() bypass. |
| sso-saml | `node_modules/@voltro/cli/templates/agent-docs/plugins/sso-saml.md` (or `node_modules/@voltro/plugin-sso-saml/README.md`) | Enterprise SAML 2.0 SSO — SP-initiated login, ACS assertion consumer, SP metadata. Signature verification via @node-saml/node-saml; framework session minting built in. |
| storage | `node_modules/@voltro/cli/templates/agent-docs/plugins/storage.md` (or `node_modules/@voltro/plugin-storage/README.md`) | File storage behind one StorageService — public objects served direct from the bucket/CDN, private objects gated by an access policy + per-object grants. S3 / MinIO (R2 and GCS via their S3 interop) / Azure / database / filesystem / memory providers, presigned URLs, a dashboard browser. |
| tinybird | `node_modules/@voltro/cli/templates/agent-docs/plugins/tinybird.md` (or `node_modules/@voltro/plugin-tinybird/README.md`) | Hosted-ClickHouse AnalyticsSink over Tinybird's Events API + Pipes — pay-as-you-go OLAP without operating a cluster. |
| webhooks | `node_modules/@voltro/cli/templates/agent-docs/plugins/webhooks.md` (or `node_modules/@voltro/plugin-webhooks/README.md`) | First-class outgoing + incoming webhooks — declared events with a webhook: block, defineIncomingWebhook in *.webhook.tsx files, runtime subscriptions, durable delivery via @effect/workflow, HMAC signing, provider presets, and idempotency. |


---

When in doubt: grep this repo for a primitive that already does something
similar and pattern-match against it.
