# Introduction

> Scaffold a Voltro project and boot it locally in under a minute.



---

<!-- source: en/intro/getting-started.md -->
## Getting started

_Scaffold a Voltro project and boot it locally in under a minute._

Voltro is an AI-first, multi-tenant, reactive full-stack framework. You write your backend through file conventions for queries, mutations, actions, streams, workflows, and agents; hooks for reactive data; automatic live updates to every connected client over a single WebSocket.

This guide gets you from zero to a running stack in under a minute.

## Prerequisites

- **Node.js 24+** — Voltro relies on the `--experimental-strip-types` flag and modern ESM behaviour.
- **pnpm 10+** — workspaces, fast installs, deterministic lockfiles.
- **Postgres 16+** *(optional)* — Voltro's dev mode runs against an in-memory store by default; switch to Postgres when you want logical replication-backed subscriptions, durable workflows, or to mirror production locally.

## Scaffold a project

A Voltro monorepo is a pnpm workspace. `create-project` walks up from your current directory looking for `pnpm-workspace.yaml` — and **when there is none, it creates the workspace root right there** before scaffolding. So an empty directory is a perfectly good starting point:

```bash
mkdir acme && cd acme

pnpx voltro create-project acme \
  --api api-backend \
  --web frontend-landing \
  --port-range 5190-5199
```

You get:

```text
acme/
  pnpm-workspace.yaml     # the package globs + the install-script decisions
  package.json            # dev / build / test / typecheck (plain `pnpm -r` scripts)
  .gitignore              # incl. .env.local, where `voltro dev` mints your secrets
  .git/                   # unless you were already inside a repo
  AGENTS.md + CLAUDE.md   # the agent guide, seeded per project
  apps/acme/
    api/                  # Voltro backend
    web/                  # landing page
    project.json          # the project's port range + app map
```

The project name is kebab-cased (so `Acme` becomes `acme`). The `--port-range` is recorded in `project.json` so every new app added later gets a unique port without you thinking about it.

Three things worth knowing about this first run:

- **The install-script question is already answered.** pnpm refuses to finish an install that has an undecided `postinstall` (`ERR_PNPM_IGNORED_BUILDS`), and a Voltro workspace pulls three — all transitive, none of them anything you picked. `pnpm-workspace.yaml` ships the answers with a reason on each line: `esbuild: true` (vite's compiler binary), `@parcel/watcher` and `msgpackr-extract` `false` (optional native accelerators with pure-JS fallbacks, so your first install needs no C++ toolchain). Change your mind with `pnpm approve-builds`.
- **Already have a workspace?** Nothing is overwritten. An existing `pnpm-workspace.yaml` is left alone, and only root scripts you *don't* already define are filled in. To prepare a directory without scaffolding anything yet, run `voltro init` — it creates the same workspace root and stops there.
- **It registers the project with the cloud control plane** (self-hosted tracking) unless you pass `--no-register`. Offline, in CI, or just not interested: `--no-register` skips the network call entirely.

## Boot it

Install and run from the **workspace root** — there is no top-level `acme/api` to `cd` into; the project lives under `apps/acme/`:

```bash
pnpm install
pnpm dev
```

The root `dev` script is `pnpm -r --parallel dev`: it runs every workspace package that has a `dev` script, at once. No task runner to install — and apps that own their own dev loop (an Expo mobile app, a serverless bundle) simply don't define `dev`, so they opt out by construction. To run one app on its own, `pnpm --filter @acme/api dev`.

By default:

- `api` listens on `:4000` (RPC over WebSocket on `/ws`)
- `web` listens on the first port in your range (e.g. `5190`)
- The framework dashboard auto-launches on `:5179` (set `VOLTRO_DASHBOARD=off` to skip it)

Open the web app's URL in a browser — anything you edit in `apps/acme/api/` or `apps/acme/web/src/` hot-reloads.

## What you just got

- File-based **queries** (`*.query.ts`) and **mutations** (`*.mutation.ts`) — no manual registration
- **Streams** (`*.stream.ts`) for one-shot server-to-client element feeds
- **Reactive subscriptions** wired to Postgres logical replication (or in-memory CDC for the dev store)
- **Multi-tenancy** as a runtime primitive — drop the `tenant()` mixin on a table and the runtime scopes reads + flags cross-tenant writes
- **Durable workflows** via `@effect/workflow` — long-running jobs survive deploys and crashes
- **AI primitives** wrapping the Vercel AI SDK, plus a tool-calling convention
- **End-to-end type safety** from your Postgres schema to your React components, no codegen step

## Next steps

- [Why Voltro?](/docs/intro/why-voltro) — the bigger picture + what we won't build
- [Concepts](/docs/intro/concepts) — the vocabulary behind the framework
- [File conventions](/docs/intro/file-conventions) — what `*.query.ts`, `*.mutation.ts`, etc. actually do
- [Data overview](/docs/data/overview) — queries, mutations, actions, streams in five minutes



---

<!-- source: en/intro/why-voltro.md -->
## Why Voltro?

_The wager Voltro is making — reactive, multi-tenant, AI-first as primitives instead of libraries you bolt on._

Most frameworks make you re-derive the same primitives every time: auth, tenants, audit logs, billing, real-time updates, background jobs. Voltro ships them as first-class composable primitives so you can spend your time on what makes YOUR product different.

## The wager

AI agents will increasingly write your code. The frameworks that win are the ones that give agents a **stable, opinionated surface** to target — not a thousand decisions every project re-litigates.

Voltro is deliberately opinionated about boring things (HTTP, state, transport, schema) so the only decisions left are the ones that matter for your product.

## Where Voltro fits

| You're building... | Voltro is a good fit when... |
|---|---|
| A multi-tenant SaaS | You want tenant isolation enforced by the runtime, not "by hope" in app code |
| A real-time collaboration app | Live data should be the default, not a feature flag |
| An AI agent product | Tool calling, RAG, streaming, durability should be primitives |
| A control plane | You need durable workflows + audit logs + RBAC out of the box |

## Where Voltro is a bad fit

- **You need an existing JS/TS ecosystem feature on day one.** Voltro is pre-release; APIs change.
- **You're publishing to a CDN-only edge.** Voltro is a stateful, long-running runtime — Vercel, Fly, Railway, Render, or self-host on a Node 24 box.
- **You want a visual ORM / GUI builder.** The CLI + your editor + LLMs are the editor.

## What we won't do

- **Lock you in.** Self-hosting is fully supported. Cloud is the premium tier of the same runtime, not a fork.
- **Multi-runtime grab-bag.** Effect-TS end-to-end — no Restate, no Temporal, no Inngest, not even opt-in.
- **Magic.** Every file convention is documented; every generated file lives in `.framework/` and you can read it.
- **Codegen you have to remember to run.** Schema flows from your tables to your React components automatically via Vite's module graph.

## Deliberate noes

Two questions come up in every framework comparison. Both are decided — deliberately no — and here is why, so nobody has to re-litigate them.

### Why is there no GraphQL API?

1. **GraphQL's three core promises are solved differently here.** Type-safe selective reads ⇒ typed queries + schema inference. One endpoint for every client ⇒ the RPC socket with a generated client. Third-party consumers ⇒ [REST routes](/docs/data/rest-routes) + [OpenAPI 3.1](/docs/plugins/openapi) (`@voltro/plugin-openapi`).
2. **A GraphQL gateway would have no access to the reactivity path** — source-based invalidation, per-delivery guards. It would be a second, dead read path whose results are never live: exactly the kind of duplicate path this framework refuses to keep.
3. **Resolver N+1, persisted-query complexity, and a second permission model** (field-level vs. our guards/RLS) buy nothing the existing surface cannot do.

Don't build a GraphQL layer over the stores. External consumers get REST + OpenAPI; internal clients get RPC + live subscriptions.

### Why not React Server Components?

RSC is a second rendering **and** data model — Flight serialization, `'use client'` boundaries, deep bundler integration — that would compete with the reactive subscription model instead of composing with it. The problems it solves are covered by what exists today: [islands](/docs/routing/islands) for shipping less JS, loaders for server data at render time, and streaming SSR with `defer()` for progressive delivery. Don't write `'use server'` / `'use client'` directives in a Voltro app; they mark a boundary this framework does not have.

## Where to read next

- [Getting started](/docs/intro/getting-started) — scaffold + boot in under a minute
- [Concepts](/docs/intro/concepts) — the mental models behind the primitives
- [File conventions](/docs/intro/file-conventions) — what files do what



---

<!-- source: en/intro/concepts.md -->
## Concepts

_The core mental models behind Voltro: projects, apps, descriptors, executors, reactivity, streams, workflows, and runtime context._

This page is the map before the territory. It names the framework concepts you will see everywhere else in the docs and explains how they fit together.

## Project, Apps, And APIs

A **project** is the deployable unit under `apps/<projectName>/`. It can contain multiple apps:

- an `api` app: backend procedures, database schema, workflows, plugins
- a `web` app: pages, layouts, React hooks, SSR/static rendering
- optional sibling apps: dashboards, docs, admin panels, extra APIs

A web app talks to one or more API apps through names configured in `app.config.ts`:

```ts
apis: {
  app: { package: '@app/api' },
  billing: { package: '@app/billing-api' },
}
```

The first argument to client hooks is that API name:

```tsx
useSubscription('app', 'todos.list', {})
useMutation('billing', 'invoices.pay')
```

## RPC Tags

Every procedure descriptor declares a globally unique `name`. That name is the **RPC tag**:

```ts
defineQuery({    name: 'todos.list',   guards: [{ scope: 'todos:read' }],   /* ... */ })
defineMutation({ name: 'todos.create', guards: [{ scope: 'todos:write' }],  /* ... */ })
defineAction({   name: 'support.ping', guards: [{ scope: 'support:diagnostics' }], /* ... */ })
defineStream({   name: 'agent.run',    guards: [{ scope: 'agents:run' }],   /* ... */ })
```

Hooks use the API name plus RPC tag. There is no hand-written client SDK per endpoint.

## Descriptor And Server Executor

Procedures are split into two files:

| File | Purpose |
|---|---|
| `*.query.ts`, `*.mutation.ts`, `*.action.ts`, `*.stream.ts` | Browser-safe descriptor: name, schema, metadata. |
| `*.query.server.ts`, `*.mutation.server.ts`, `*.action.server.ts`, `*.stream.server.ts` | Server-only executor: database, SDKs, secrets, side effects. |

This keeps the wire contract importable from the client while server code stays server-only.

## Every Procedure Declares Who May Call It

Dropping a descriptor into the tree **puts it on the wire**. So each one carries an access decision, and a descriptor that carries none is **refused at boot** — by `voltro dev`, by `voltro serve`, and by `voltro doctor` as a preflight. Exactly one of three:

```ts
export const invoiceList = defineQuery({
  name: 'invoices.list',
  guards: [{ scope: 'invoices:read' }],   // the caller must hold a scope
  /* ... */
})

export const pricing = defineQuery({
  name: 'pricing.current',
  openAccess: 'public pricing page — reads no caller data',  // anyone may, and why
  /* ... */
})

export const stampAudit = defineMutation({
  name: 'auditLog.stamp',
  internal: true,                          // not on the wire at all
  /* ... */
})
```

`openAccess` takes a **reason, not a boolean**, and that is the whole design: it is what makes *"we decided this is open"* distinguishable from *"nobody looked"*. Without it, the only way to satisfy the gate would be to invent a guard — and the guard people invent is one every caller already holds, which reads as protection and enforces nothing.

The one to expect first: your **first self-written procedure file** will not boot until it has one of these three. Full rules in [Authorization](/docs/authentication/authorization#every-procedure-decides-guards-or-openaccess).

## The Main Primitives

| Primitive | Use it for | Client hook | Durable? | Reactive? |
|---|---|---|---|---|
| Query | Live reads and views | `useSubscription` | Reads durable state | Yes |
| Mutation | Atomic database writes | `useMutation().mutate` | Yes | Triggers queries |
| Action | One-shot side effects and external I/O | `useAction().run` | Request-scoped | No |
| Stream | One-shot server-to-client element feeds | `useAgentStream` | No, unless you persist | No |
| Workflow | Long-running multi-step work | Started from server code | Yes | Via the rows it writes |

Decision shortcut:

- Need live data on screen? Query.
- Need one atomic database commit? Mutation.
- Need HTTP/email/payment/upload/AI one-shot? Action.
- Need progressive tokens/progress/log events? Stream.
- Need retry/resume across crashes or deploys? Workflow.

> **Beyond the core hooks.** These five are the data layer. The
> **[Schema-driven UI](/docs/ui/overview)** section builds *on* them: `<AutoForm>`
> binds to a mutation, `<DataTable>` to a query, and a toolbox of bound hooks —
> `useCan`, `useUndo`, `usePreview`, `useProvenance`, `useAsyncValidation`,
> `useOutbox`, `useWindowedSubscription`, … — covers the rest. Full list:
> [Schema-driven UI → Client utilities](/docs/ui/client-utilities/use-can).

## Reactivity

Voltro is reactive end-to-end. A component subscribes to a query; the server keeps it live.

```tsx
const { data: todos } = useSubscription('app', 'todos.list', {})
```

When a mutation commits, the runtime emits change events. Queries whose `source` matches the changed table can update automatically.

```ts
defineQuery({ name: 'todos.list', source: 'todos', guards: [{ scope: 'todos:read' }], /* ... */ })
defineMutation({
  name: 'todos.create', target: { table: 'todos', op: 'insert' },
  guards: [{ scope: 'todos:write' }], /* ... */
})
```

There is no `refetch` as the normal path. The subscription lives for the lifetime of the component.

## Streams Are Not Subscriptions

Queries stream **subscription events**: an initial snapshot, then deltas when durable state changes.

Streams emit **plain elements** and then finish:

```tsx
const run = useAgentStream('app', 'agent.run')
run.start({ prompt })
```

Use streams for transient output: LLM tokens, progress updates, import logs. If the result must survive reloads, write rows and expose them through a query.

## Runtime Context

Server executors receive `ctx`:

```ts
export default async (input, ctx) => {
  const subject = ctx.request.subject
  const rows = await ctx.store.query(/* ... */)
}
```

Important pieces:

- `ctx.request.subject`: the authenticated user, API key, system actor, or anonymous subject
- `ctx.store`: typed data store with tenant/audit/soft-delete mixin behavior
- `ctx.cache`: async cache facade
- Effect services: actions and streams can return `Effect`s and use platform services such as `HttpClient`

## Tenant Scoping

Tables with the `tenant()` mixin are scoped by the runtime using `ctx.request.subject.tenantId`. Query executors do not need to repeat that predicate. Mutations should still validate any tenant IDs that arrive in input.

## Generated Files

Voltro discovers files and writes generated glue such as `rpcGroup.generated.ts` or `.framework/*`. Treat those as build artifacts. Edit descriptors, server executors, schema, pages, and config instead.

## Where To Go Next

- [File conventions](/docs/intro/file-conventions) - exact suffixes and discovery rules
- [Data overview](/docs/data/overview) - queries, mutations, actions, streams
- [Runtime context](/docs/reference/runtime-context) - what `ctx` contains
- [Workflows](/docs/workflows/overview) - durable work
- [AI streaming](/docs/ai/streaming) - transient vs persisted streams



---

<!-- source: en/intro/installation.md -->
## Installation

_Prerequisites, supported Node versions, and the three ways to install the Voltro CLI._

## Prerequisites

| Tool | Version | Why |
|---|---|---|
| Node.js | 24.x or newer | `--experimental-strip-types`, native ESM, `node:test`. |
| pnpm | 10.x or newer | Workspace + lockfile semantics Voltro's scaffolder targets. |
| Postgres | 16.x or newer | Optional for dev; required in production for logical replication-backed subscriptions. |

You don't need Docker for dev — Voltro ships an in-memory store. You'll want Docker (or a managed Postgres) when you turn on persistence.

## Three ways to install

### 1. `pnpx` (recommended)

The CLI runs from the registry without a global install:

```bash
mkdir my-app && cd my-app
pnpx voltro create-project my-app
```

This is the smallest blast radius — `pnpx` fetches the latest CLI release into the local cache and discards it after.

An empty directory is enough: `create-project` writes the pnpm workspace root (`pnpm-workspace.yaml`, a root `package.json` with `dev`/`build`/`test`/`typecheck`, a `.gitignore`, `git init`) when it can't find one above you. Inside an existing workspace it adds nothing but the project. `voltro init` does the workspace-root half on its own, for when you want the repo prepared before you pick templates.

### 2. Per-project dependency

Once your project exists, the CLI is already a `dependency` of the `api` app's `package.json`, so:

```bash
pnpm --filter @my-app/api exec voltro dev
```

The api template's own `dev` script is `voltro dev .`, so `pnpm dev` in the app runs the CLI directly. At the workspace root, `pnpm dev` is `pnpm -r --parallel dev` — every app at once. You rarely call the filtered form directly.

### 3. Global install

```bash
pnpm add -g @voltro/cli
voltro version
```

Convenient for jumping between projects from any directory. Downside: the global binary can drift away from the version pinned in your project — pin it in `~/.npmrc` or use a version manager if you go this route.

## Verifying the install

```bash
voltro version
voltro list-templates
```

The second command prints the app templates the CLI can scaffold — **dozens** of them, across four kinds: `api-*` backends, `frontend-*` web apps, an `edge-functions` serverless bundle, and `mobile-app` (Expo). The output is generated from the templates the installed CLI actually ships, so it is the authority on what your version can scaffold; do not go by a list in a doc. For what each one demonstrates and when to pick it, see the [app-template reference](/docs/reference/templates).

## Editor setup

Voltro ships an `AGENTS.md` file at the root of every scaffolded project. AI coding agents (Claude Code, Cursor, GitHub Copilot Chat) automatically read it for project-specific guidance — file conventions, schema DSL, common patterns. You don't need to do anything to enable it.

For VSCode, the recommended extensions are:

- `dbaeumer.vscode-eslint`
- `bradlc.vscode-tailwindcss`
- `vivaxy.vscode-conventional-commits`

Tailwind v4 needs no config — Voltro's Vite plugin auto-discovers the content from the module graph.

## Test utilities

`@voltro/testing` is a separate, public package — it is **not** bundled into a scaffolded project. The moment you write your first test, add it (and `vitest`) as a **devDependency** of the app you're testing:

```bash
pnpm --filter @my-app/api add -D @voltro/testing vitest
```

`voltro test` then runs vitest against the current app. See the [Testing](/docs/testing/overview) section for the full surface — `makeTestContext`, the deterministic mocks, the workflow runner, and the dialect-parity harness.

## Next

- [Getting started](/docs/intro/getting-started) — actually scaffold a project
- [Concepts](/docs/intro/concepts) — the vocabulary behind the framework
- [File conventions](/docs/intro/file-conventions) — what `*.query.ts` etc. mean



---

<!-- source: en/intro/file-conventions.md -->
## File conventions

_The dot-suffix file conventions Voltro uses for procedure descriptors, server executors, workflows, agents, schemas, and pages._

Voltro replaces router and registry config with **filesystem conventions**. Drop a file with the right suffix, the CLI's discovery walker picks it up, and the exports are wired into the runtime on the next boot. No manual registration, no import barrels to maintain.

## The api side (`apps/*/api/`)

| Suffix | What it is | Wired into |
|---|---|---|
| `*.query.ts` | Browser-safe reactive query descriptor: `defineQuery({ name, source, input, output, guards })`. | Typed RPC group + client metadata. |
| `*.query.server.ts` | Server executor for the matching query descriptor. | Reactive subscription runtime. |
| `*.mutation.ts` | Browser-safe mutation descriptor: `defineMutation({ name, target, input, output, error, guards })`. | Typed RPC group + auto-optimistic metadata. |
| `*.mutation.server.ts` | Server executor for the matching mutation descriptor. | Transactional mutation runner. |
| `*.action.ts` | Browser-safe action descriptor: `defineAction({ name, input, output, error, guards })`. | Typed RPC group. |
| `*.action.server.ts` | Server executor for the matching action descriptor. | Non-transactional action runner. |
| `*.stream.ts` | Browser-safe one-shot stream descriptor: `defineStream({ name, input, element, error, guards })`. | Typed streaming RPC group. |
| `*.stream.server.ts` | Server executor returning an Effect `Stream`. | Plain server-to-client element streams. |
| `*.workflow.tsx` | A durable Effect workflow. Survives restarts. | `@effect/workflow` runtime. |
| `*.agent.tsx` | Browser-safe AI agent **descriptor**: `defineAgent({ name, input })`. Codegen-typed routes. | Agent runtime + client types. |
| `*.agent.server.tsx` | Server agent **executor**: `defineAgentExecutor(descriptor, { system, tools, model, maxSteps })`. | Agent runtime. |
| `*.tool.tsx` | A tool an agent can call. Schema + handler. | Agent runtime. |
| `*.webhook.tsx` | Outgoing webhook spec (target, retry, schema). | Webhook delivery worker. |
| `*.ws.ts` | Raw WebSocket gateway — `defineWebSocket({ path, auth, onConnection })` as the default export, for FOREIGN protocols beside the rpc socket. | Upgrade listener on the api server, both boot paths. |
| `*.entity.ts` | Database table — one table per file: `table()` + columns + mixins. Re-exported from a `database/index.ts` barrel. | Migrations + the runtime data store. |
| `*.config.ts` | App-level config (`app.config.ts`, `tsconfig.json`, etc.). | The CLI. |

Procedure descriptors are intentionally separate from server executors. Descriptor files are safe for browser imports and codegen; `.server.ts` files can import the database, file system, SDK clients, secrets, and other server-only modules. Each descriptor has exactly one matching `.server.ts` file with the same primitive suffix. Workflows follow the same split: a browser-safe `*.workflow.tsx` descriptor (importing `workflow` from `@voltro/workflow/define`) paired with a `*.workflow.server.tsx` executor.

### The access decision is not optional

`guards` appears in all four procedure signatures above because dropping a file into the tree **puts it on the wire**, and a wire-exposed procedure has to say who may call it. Exactly one of three:

```ts
guards: [{ scope: 'notes:read' }]                    // the caller must hold a scope
openAccess: 'public pricing page — reads no caller data'  // anyone may call it, and why
internal: true                                        // not on the wire at all
```

A descriptor that declares none of them is **refused at boot** — by `voltro dev`, by `voltro serve`, and by `voltro doctor` as a preflight. This is the one field a newly-created procedure file is most likely to be missing, and the failure is a boot refusal naming the file rather than a subtle runtime surprise. Full rules, including `openAccess`'s required reason and the per-app `security.defaultDeny` switch: [Authorization](/docs/authentication/authorization#every-procedure-decides-guards-or-openaccess).

The browser-safe rule is **transitive**, and that is where it usually breaks. The codegen pulls every descriptor (and every workflow descriptor) value-level into `rpcGroup.generated.ts`, which the web client loads — so a descriptor plus *everything it imports* must stay free of server-only code (`node:*`, the `database` handle, `@voltro/ai`, cluster, plugins, `@voltro/protocol/session`). The classic mistake is not a literal `import 'node:crypto'` but a descriptor importing a shared typed-error or helper from a `lib/` file that *also* imports the database — which drags the whole schema graph into the browser bundle. Keep typed errors, Schemas, and pure helpers in files with zero server imports; put DB-backed guards in `.server.ts`. A leak shows up as the web app fetching hundreds of modules / tens of MB on first load, or crashing with `Module "node:crypto" has been externalized for browser compatibility`.

### Declaring a shared file browser-safe: `*.client.ts`

A shared `lib/` helper can state the rule about itself. Name it `*.client.ts` (or `*.client.tsx`) and it declares: *I, and everything I transitively import, are browser-safe.* `voltro dev` walks its import graph at boot and refuses to start if the claim is false, printing the chain.

```text
lib/orderErrors.client.ts   # I and my imports are browser-safe — checked at boot
lib/orderGuards.server.ts   # I may touch the database handle
lib/orderTypes.ts           # unmarked: no claim, the graph decides
```

This is the mirror of `*.server.ts`, and it works for the same reason: both declare a **permission**, which is something an import graph cannot derive. The graph can tell you what a file imports; it can never tell you what a file is *allowed* to import.

Without the marker the leak is still caught — by the rpcGroup guard — but only once some descriptor happens to reach the file, and the error is a forty-module chain you read backwards to find the one shared file that should never have touched the database. The marker moves the failure to that file, at the moment it is written.

An unmarked file makes no claim, and that is fine: `*.client.ts` is for the shared files where the mistake is expensive, not a label to sprinkle on everything.

### Raw WebSocket gateways: `*.ws.ts`

A `*.ws.ts` file's default export mounts a raw WebSocket upgrade path beside the rpc socket — for a protocol the framework does not speak (a Yjs provider, a legacy device fleet). Discovered on **both** boot paths, `voltro dev` and `voltro serve`:

```ts
// gateways/collab.ws.ts
import { defineWebSocket } from '@voltro/protocol'

export default defineWebSocket({
  path: '/gateways/collab',
  auth: 'subject',   // REQUIRED, no default — or 'public', a decision you write down
  onConnection: ({ send, onMessage, subject }) => {
    onMessage((data) => send(data))   // your protocol, your frames
    return () => { /* teardown — runs on disconnect, credential expiry, shutdown */ }
  },
})
```

`auth: 'subject'` authenticates through the same chain as rpc/SSR **before** the upgrade (401 while it is still http) and binds the connection to the credential's expiry (close code `4001`); every gateway path is origin-checked at upgrade. Two gateways on one path refuse the boot; a plain GET on a gateway path answers `426`. App realtime stays [subscriptions](/docs/data/subscriptions) — full detail under [Raw WebSocket gateways](/docs/data/subscriptions#raw-websocket-gateways--definewebsocket).

## The web side (`apps/*/web/`)

| Path | What it is |
|---|---|
| `src/pages/**/page.tsx` | A page. URL is the file path; `[id]/page.tsx` -> `/:id`, `[...slug]/page.tsx` -> catch-all. |
| `src/pages/layout.tsx` | Outer layout — wraps every page. |
| `src/pages/error.tsx` | Error boundary for the whole subtree. |
| `src/pages/not-found.tsx` | Fallback rendered when no page matches. |
| `src/pages/loading.tsx` | Pending UI shown while loaders resolve. |
| `src/pages/(group)/` | Route group — does not contribute a URL segment, but layout/error files inside still apply. |
| `src/*.island.tsx` | A client-side hydration island (split chunk). Used in `interactive: 'islands'` pages. |

Each page can opt into a render strategy via two exports:

```tsx
// src/pages/blog/[slug]/page.tsx
export const renderMode = 'isr' as const           // 'static' | 'spa' | 'ssr' | 'isr'
export const interactive = 'islands' as const      // 'none' | 'islands' | 'full'
```

- `renderMode` controls when the HTML is produced (build vs. request).
- `interactive` controls how much JS ships (`'none'` strips it all, `'full'` hydrates the page, `'islands'` hydrates only `.island.tsx` files).

A page can also declare its query-string contract as a page export:

```tsx
export const searchParams = Schema.Struct({
  page: Schema.optionalWith(Schema.NumberFromString, { default: () => 1 }),
})
```

- `searchParams` (an `effect/Schema` struct — every field optional or with a default) types the page's query string: `useSearchParams(searchParams)` returns the decoded shape, and links built with `withQuery` type-check against it. Details: [Pages → Query strings](/docs/routing/pages#query-strings).

## Discovery in practice

```text
apps/acme/api/
├── queries/
│   ├── notes.list.query.ts
│   └── notes.list.query.server.ts       -> notes.list query
├── mutations/
│   ├── notes.create.mutation.ts
│   ├── notes.create.mutation.server.ts  -> notes.create mutation
│   ├── notes.update.mutation.ts
│   └── notes.update.mutation.server.ts  -> notes.update mutation
├── streams/
│   ├── ticker.stream.ts
│   └── ticker.stream.server.ts          -> ticker stream
├── workflows/
│   └── notes.summarise.workflow.tsx     -> notes.summarise workflow
└── database/
    ├── users.entity.ts                  -> users table
    ├── todos.entity.ts                  -> todos table
    └── index.ts                         -> databaseHandle + re-exports
```

`voltro dev` writes `rpcGroup.generated.ts` beside the api source. It imports descriptor files only, lifts them into an `RpcGroup`, and exports descriptor metadata for the client. Web apps consume the mounted api with `useSubscription`, `useMutation`, `useAction`, or `useAgentStream` from `@voltro/client`.

## Anti-patterns

- **Don't put two procedure descriptors with the same `name`.** The CLI fails the boot — fix the name collision.
- **Don't import `.server.ts` files from your web app.** Use the generated client. Importing the server module straight into the browser bundle leaks server-only deps (Postgres driver, secret keys).
- **Don't move generated files in `.framework/` into the user source tree.** They're disposable; the CLI rewrites them on every boot.



---

<!-- source: en/intro/file-taxonomy.md -->
## The web file taxonomy

_The contract suffixes for web code — component, component.ui, hook, types, internal, fixture, tracking — what each one promises and which rule enforces it._

Every suffix on this page is a **contract**, not a label. Something else in the codebase depends on the promise, and `voltro doctor` enforces it. That is the whole admission test, and it is why the list is short:

> Does another file's correctness depend on this file keeping its promise?

If yes, the promise belongs in the name — you cannot see a contract before you break it otherwise. If no, it is a category, and categories are read out of the file.

## The catalogue

| Suffix | Promise | Enforced by |
|---|---|---|
| `*.component.tsx` | exactly one component | `component/one-per-file`, `component/no-hook-export` |
| `*.component.ui.tsx` | one component, **reads only** | `ui/no-write`, `ui/orphaned`, `ui/unlinked` |
| `*.hook.ts` | exactly one `use*` hook (+ types) | `hook/one-per-file`, `hook/no-component-export` |
| `*.types.ts` | zero runtime exports | `types/runtime-export` |
| `*.internal.ts` | only its own subtree imports it | `internal/foreign-import` |
| `*.fixture.ts` | no production path reaches it | `fixture/production-import` |
| `*.tracking.ts` | analytics happens nowhere else | `tracking/outside-tracking-file` |
| `*.client.ts` | it and its imports are browser-safe | boot-time import walk, `client/not-browser-safe` |
| `*.store.ts` | exactly one `defineStore`, no server state | `store/one-per-file`, `store/mirrors-server-state` |

A `*.component.tsx` promises exactly ONE component. It does not promise to export nothing else: types, and plain module-local values a `const COLUMNS = […]` beside the table that renders them, are fine and always were. What the rule counts is components — a declaration that renders — so an object, an array, a string or a `new` beside your component is not a second one, and neither is `export default Card` next to `export const Card`.

The BOUNDARY rules (`internal/foreign-import`, `fixture/production-import`, `ui/unlinked`) are assertions about your import graph, so it is worth knowing which edges they follow: relative specifiers, your tsconfig `paths` aliases (read from the nearest `tsconfig.json`, so a per-app `@/*` works when you run `voltro doctor` at the repo root), `export … from` re-exports, and dynamic `import()`. A package import is a leaf — the walk stops at the edge of your app.

## `*.component.ui.tsx` — reads, never writes

```tsx
// OrderRow.component.ui.tsx
import { useCan } from '@voltro/client'
import { useT } from '@voltro/i18n'

export const OrderRow = (props: { order: Order; onCancel: () => void }) => {
  const cancelLabel = useT('orders.cancel')
  const mayCancel = useCan('orders:write')
  return <tr>{/* … */}</tr>
}
```

Reading is allowed on purpose. Threading translations and permissions through props is prop-drilling — it makes every call site worse without making the component more portable.

**Writing** is what breaks the contract. A component that can mutate cannot be rendered ten thousand times in a list, reused across features, or prerendered without first reading what it does — and that property is exactly what its callers rely on. Lift the mutation into the `*.component.tsx` that owns it and pass a handler down.

The same file must also be *reached* from a `*.component.tsx`, another `*.component.ui.tsx`, or a page. An unrendered presentational component is carried, reviewed and refactored forever without ever reaching a user; that is how a design system quietly doubles in size.

## `*.internal.ts` — the feature boundary

```text
src/features/orders/
├── index.ts                 # the public surface
├── orderState.internal.ts   # only this directory may import it
└── OrderList.component.tsx
```

`.internal` is the promise that refactoring inside that directory breaks nobody. An import from another feature revokes it — silently, and without a single review comment, which is how a boundary rots.

## `*.types.ts` — provably free to import

No runtime export at all. That is not tidiness: it is what makes importing the module cost nothing in the bundle **and** makes it impossible for it to participate in a runtime import cycle. In a large codebase the second guarantee is the valuable one — an import cycle is only visible when it finally throws.

## `*.tracking.ts` — analytics is confined

```ts
// checkout.tracking.ts
import { defineTracking } from '@voltro/client'

export const checkoutTracking = defineTracking('CheckoutButton', {
  onMount: (props) => ({ event: 'checkout_started', orderId: props.orderId }),
  onClick: 'checkout.confirmed',
})
```

A component then wires it up with `useTracking(checkoutTracking, props, sink)` — it names a spec, it does not author one. Event names, property bags and the decisions about which fields leave the building all live in one place.

`useTracking` itself is a hook, so it is *not* confined — a rule nobody could satisfy is a rule everybody disables. What is confined is `defineTracking`, the declaration.

The payoff is not tidiness. "What do we send to third parties" becomes a file listing instead of an archaeology project — which is the only form in which that question can be answered on demand when someone asks about personal data.

## `convention/missing-test` — why a shallow test is still worth writing

Every suffix that declares a runtime contract also expects a test beside it, named mechanically: `Card.component.tsx` → `Card.component.test.tsx`. It is an advisory, not an error.

The usual objection is that a per-component test at any real size is low value, and for *assertions* that is often true. That is not what the rule buys. What it buys is that something **mounts** the component — and a render loop, a crashing effect, a missing provider or a broken context is invisible until something does.

That is not hypothetical. One app adopting the taxonomy wrote 251 of these, deliberately shallow (it mounts, it performs no domain write, it renders no raw catalogue key). The first run found a page whose breadcrumb effect rebuilt a fresh array literal on every render — effect → context state → re-render → new literal, without end. That one test took 423 seconds and exhausted the heap. Ten sibling pages memoised; exactly one did not, and in a browser the screen had looked usable. After the fix the whole web suite went from 645 s to 57 s.

So write them shallow if you like. The mount is the point.

## What deliberately has NO suffix

A generic "one component per file" rule would be worth enforcing everywhere, so tying it to a rename would make it opt-in — less coverage for more cost. The shape rules above fire only on files that *declared* the contract, because declaring it is what makes the promise mean something.

`*.store.ts` was in this section until `defineStore` shipped, and the reason it moved out is the rule itself: **suffixes follow primitives, never the reverse.** While client state was something you brought yourself, a suffix for it would have promised nobody anything. Now `voltro check` reads those files — one store per file, and no store mirroring server state — so the name carries a contract.

There is no `*.form.tsx`, and it is the most-requested one. A form is a component; what makes it a form is the schema it validates against, which is already declared and already typed. A suffix would add a rename without adding a checkable promise — "contains a `<form>`" is not something another file's correctness depends on. If forms ever gain a framework primitive that other code binds to, the rule above will produce the suffix on its own.

## Code you did not write

Our rules are **ours**. A shadcn component arrives via `npx shadcn add`, follows shadcn's conventions (many exports per file, a hook beside the component), and is overwritten by the next `add`. Renaming it would break their convention, be undone on the next generator run, and leave the directory half-migrated the moment one file fails to classify.

So the taxonomy applies to what you write, always — and to nothing else. Two signals mark a directory as not-yours:

1. **`components.json`** at the app root. Its `aliases.ui` names the directory shadcn owns, so a shadcn project needs no configuration at all. Only `aliases.ui` is honoured — `aliases.components` is where your own components live too, and exempting it would silence the taxonomy across most of a codebase.
2. **A `.voltro-vendored` file** in any directory, whose first line names the source:

```text
src/vendor/.voltro-vendored     # "copied from acme-design-system v3"
```

The marker is a file with a reason in it rather than a config list on purpose. A config list is invisible from the directory it exempts and quietly becomes where people put their own code to silence a rule. `voltro doctor` prints every exemption it honoured, so the escape hatch is never silent.

## Migrating

`voltro update` renames what it can decide from the exports alone: one component → `*.component.tsx`, one hook → `*.hook.ts`, no runtime exports → `*.types.ts`. Imports travel with the file.

Two things it will NOT do:

- A file exporting a component **and** a hook is left alone and reported. That is the file the taxonomy most wants split, and no codemod can decide which half keeps the name.
- `*.component.ui.tsx` is never inferred. "Presentational" is a promise about what a component *may* do; one that merely happens not to write today has not made it.
