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

  • sources is 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.