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 }). |
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 }). |
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 }). |
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 }). |
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. |
*.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 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.
The web side (apps/*/web/)
| Path | What it is |
|---|---|
src/pages/*.tsx |
A page. URL is the file path; [id].tsx -> /:id, [...slug].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:
// src/pages/blog/[slug].tsx
export const renderMode = 'isr' as const // 'static' | 'spa' | 'ssr' | 'isr'
export const interactive = 'islands' as const // 'none' | 'islands' | 'full'renderModecontrols when the HTML is produced (build vs. request).interactivecontrols how much JS ships ('none'strips it all,'full'hydrates the page,'islands'hydrates only.island.tsxfiles).
Discovery in practice
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-exportsvoltro 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.tsfiles 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.