Tables compose
like CSS classes.

Drizzle and Prisma made TypeScript schemas viable. Voltro takes the next step: every cross-cutting concern your tables share — audit columns, tenant isolation, soft delete, custom versioning, AI auto-embedding — is a composable mixin you append with .with(). One line per concern, no codegen step, fully typed result.

posts.entity.ts
TypeScript
// database/posts.entity.ts
import { boolean, id, table, text, timestamp } from '@voltro/database'
import { audit } from '@voltro/plugin-audit'
import { tenant } from '@voltro/plugin-multitenancy'
import { softDelete } from '@voltro/plugin-soft-delete'

export const posts = table('posts', {
  id:       id(),                          // typed PostId (TypeID-branded)
  title:    text(),
  body:     text(),
  status:   text().oneOf(['draft', 'published']),
  pinned:   boolean().default(false),
  publishedAt: timestamp().nullable(),
})
  .with(audit(), tenant(), softDelete())   // ← composable mixins
  .index('byTenantStatus', ['tenantId', 'status'])
  .index('byPublished', ['publishedAt'], { where: `"status" = 'published'` })
  .reactive()

// You get for free:
//   - createdAt, updatedAt, createdBy, updatedBy → actors  (audit)
//   - tenantId reference + auto-scope reads + auto-fill   (tenant)
//   - deletedAt, deletedBy + delete()-turns-into-UPDATE   (softDelete)
//   - Reactive matcher hookup                              (.reactive)
//   - Partial index on published posts only                (.index)
//   - Branded PostId                                       (id)

Six features that ship with @voltro/database.

Composable mixins

.with(audit(), tenant(), softDelete()) — three lines of declaration, fifteen columns + constraints added. Mixins know their dependencies; softDelete() transitively requires audit(), and the resolver dedupes if both reach it.

Branded TypeIDs by default

Every id() returns a brand-typed value (PostId, UserId, OrgId). Sortable by creation time, URL-safe, prefix-derived from the table name. Cross-table assignment fails compilation.

Partial + expression + FTS indexes

index([cols]) for simple, index('name', cols, where) for partial, expressionIndex('name', [expr]) for computed, fullTextIndex for FTS. Boot-time audit flags redundant-prefix indexes that add write cost.

Five dialects, one schema

Postgres / MariaDB / MySQL / MSSQL / SQLite — switched at boot via DB_DIALECT env. The DSL emits per-dialect DDL automatically; you write the schema once and run it anywhere.

Relations + eager loading

Declare 1:1, 1:N, and N:M relations in a *.relations.ts file. A .with(...) call loads the tree in ONE roundtrip via dialect-native JSON aggregation. Per-parent limits via correlated subqueries.

Plugin-authored mixins

defineMixin(...) ships third-party mixins (vectorEmbedding, billing.versioned, etc.). They compose alongside the built-ins through the same .with() chain.

The cross-cutting concerns are the bug surface.

The columns every table has — createdAt, updatedAt, createdBy, deletedAt, tenantId — are the columns that, when you forget them or implement them inconsistently, become the bugs that show up in your second year. Voltro standardises them as mixins so the implementation is correct everywhere or nowhere — never half right in three places.

The cross-dialect bet:

Voltro's DSL targets five SQL dialects — Postgres, MariaDB, MySQL, MSSQL, SQLite. One schema, one query builder, one set of mixins. Switching DB_DIALECT switches the DDL emitter, the JSON-agg compiler, the FTS index syntax. Postgres remains the recommended target for native LISTEN/NOTIFY-driven reactivity; the others are there for enterprise procurement and embedded workloads.

The decision is "you write Voltro" — not "you write Postgres-specific Voltro". The DSL hides every dialect divergence; EXPLAIN on your hottest query stays portable.

The schema feeds the rest of the runtime.

Open the framework. See it for yourself.

Every primitive on this page is in the framework today. Clone the starter, run `voltro dev`, and have it on screen in two minutes.