Building blocks
definePlugin({ sources }) — a plugin that brings app-shaped primitives (procedures, tables, workflows, schedules, reactions) which every installing app discovers as its own. Shares code between apps of a project, never data.
A ledger, an audit trail, an approval flow: some pieces of an app are the SAME in several apps of a project. A building block is how that code is written once and installed wherever it is needed — as a plugin that carries a directory of ordinary app files:
// packages/audit-block/src/index.ts
import { fileURLToPath } from 'node:url'
import { definePlugin } from '@voltro/protocol'
export const auditBlock = () => definePlugin({
name: 'auditBlock',
permissions: ['store:write'],
sources: fileURLToPath(new URL('./app', import.meta.url)),
})packages/audit-block/src/app/
├── database/schema.ts # table('audit_marks', …)
├── mutations/audit.mark.mutation.ts # + .server.ts
├── queries/audit.marks.query.ts # + .server.ts
├── workflows/audit.export.workflow.tsx # + .server.tsx
└── schedules/heartbeat.cron.tsx// apps/cloud/worker/app.config.ts
import { auditBlock } from '@acme/audit-block'
export default {
type: 'api' as const,
name: 'worker',
plugins: [auditBlock()],
}Every app that installs the plugin discovers the block's files exactly as it
discovers its own: the same codegen (the block's procedures join the app's
rpcGroup.generated.ts, its service: procedures the app's
serviceGroup.generated.ts), the same browser-safety guard, the same access
gate under security.defaultDeny, the same migrations, and both boot paths —
voltro dev and voltro serve, from source or from the built serve bundle,
wherever the block lives after a deploy.
Code, never data
Each installing app gets its OWN copy of the block's tables, in its OWN
database. Two apps that install auditBlock have two audit_marks tables, and
neither sees the other's rows. That is the point: a building block shares what an
app DOES, not what it HOLDS. When one app needs another's data, it
calls that app.
The rules
sourcesis an ABSOLUTE directory —fileURLToPath(new URL('./app', import.meta.url))resolves the same from every app that installs it. A relative path or a missing directory refuses the boot.- A block is installed once per app.
- The block's procedures keep the names their descriptors declare. A name that collides with the app's own — or another block's — is refused at codegen, as any collision is.
- A block's table is a table the app declares, so the app's store reaches it; a table the block does not declare is refused there as anywhere else.
- Descriptors in a block follow the same
browser/server boundary as the app's: descriptor files are
browser-safe, executors live in
.server.ts.
Building blocks vs. ordinary plugins
An ordinary plugin contributes through hooks — routes, extendSchema,
interceptors, services — and is written against the plugin contract. A building
block is written like an APP, in the file conventions every app already uses, so
anything an app can declare, a block can bring. The two combine: a plugin may set
sources beside its hooks.