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:

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

The first argument to client hooks is that API name:

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

RPC Tags

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

defineQuery({ name: 'todos.list', /* ... */ })
defineMutation({ name: 'todos.create', /* ... */ })
defineAction({ name: 'support.ping', /* ... */ })
defineStream({ name: 'agent.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.

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 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.

Reactivity

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

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.

defineQuery({ name: 'todos.list', source: 'todos', /* ... */ })
defineMutation({ name: 'todos.create', target: { table: 'todos', op: 'insert' }, /* ... */ })

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:

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:

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 Effects 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