One schema.
Every SQL backend.
Your schema, queries, migrations and the reactive engine are written once and compile to the dialect-native idiom at runtime. Postgres is the reference; MySQL, MariaDB, MSSQL and SQLite run the same code. Selecting one is a single env var — DB_DIALECT.
// database/posts.entity.ts — one schema, any dialect
import { id, table, text, timestamp } from '@voltro/database'
export const posts = table('posts', {
id: id(), // branded TypeID
body: text(), // NOT NULL by default
createdAt: timestamp(), // timestamptz, UTC
}).reactive()
// Switch backend with one env var — the DDL, query
// builder and CDC reader all adapt:
// DB_DIALECT=postgres # LISTEN/NOTIFY
// DB_DIALECT=mysql # binlog CDC
// DB_DIALECT=sqlite # in-process busWhat ships with @voltro/database.
One DSL, one query builder
table() + columns + mixins declare the schema; a single cross-dialect query builder emits per-dialect SQL. Columns are NOT NULL by default; ids default to branded TypeIDs. You write Voltro, not Postgres-specific Voltro.
Native change-data-capture
Reactivity is powered by real CDC: Postgres LISTEN/NOTIFY, MySQL/MariaDB binlog (ROW format), MSSQL Change Tracking, and an in-process bus for SQLite. Resume offsets are persisted so a restart doesn’t miss a change.
Migrations, planned & online
voltro db plan / apply with drift detection, squashing, rollback + snapshots, cross-env sync and zero-downtime online changes — emitting the right DDL for whichever dialect you target.
Rich SQL surface
Joins with eager loading via dialect-native JSON aggregation, window functions, recursive CTEs, DISTINCT ON, set operations, typed JSON/JSONB, arrays, generated columns, full-text search and pgvector similarity.
Read replicas + read-your-writes
Point DB_REPLICA_URLS at replicas and reads spread across them; per-subject read-your-writes tracks LSN/GTID so a user never reads staler than their own last write. (Postgres RYW is complete; MySQL/MariaDB wait-mode routes to primary today.)
Portable, not lowest-common-denominator
The DSL hides each dialect’s divergence — the DDL emitter, JSON-agg compiler and FTS syntax all switch with DB_DIALECT — while still reaching dialect-native features. Turso ships as a beta dialect; money is integer minor units (no DECIMAL type yet).
Six backends, one mental model.
The cross-dialect bet:
Postgres, MySQL, MariaDB, MSSQL and SQLite each get DB_DIALECT -driven emission — the query builder, migration planner and CDC reader all adapt. Postgres remains the recommended target for native LISTEN/NOTIFY reactivity; the others exist for enterprise procurement and embedded workloads.
For dialects without native cross-instance CDC (SQLite) or behind a load balancer, add @voltro/plugin-broadcast to fan changes across replicas over Redis or NATS.
How far the same code travels.
SQL dialects, exercised by parity tests against real servers rather than assumed compatible
published packages, of which the dialect drivers are five interchangeable ones
A schema change, both ways.
-- migrations/0007_add_archived.sql
ALTER TABLE posts ADD COLUMN archived boolean NOT NULL DEFAULT false;
-- migrations/0008_index_it.sql
CREATE INDEX posts_archived_idx ON posts (archived);
-- …and the same two files again, by hand, for every dialect
-- you support. Reviewing them means reading SQL to infer intent.// database/schema.ts — the desired state, not the diff
export const posts = table('posts', {
body: text(),
archived: boolean().default(false).index(),
})
// $ voltro db plan
// ADD COLUMN posts.archived boolean NOT NULL DEFAULT false
// CREATE INDEX posts_archived_idx
// 0 destructive operationsThe planner diffs the declaration against the live database, prints the operations it intends and classifies the destructive ones. What you review is intent, not an archaeology of scripts — and the same declaration produces the right statements on every supported dialect.
The database layer, in depth.
How does one codebase run on six different databases?
You describe tables, columns, indexes and relations in a TypeScript schema, and write queries through a builder rather than in SQL strings. At runtime the builder compiles to the idiom of whichever dialect is configured — Postgres, MySQL, MariaDB, SQL Server, SQLite or Turso — so the same application code produces dialect-native SQL on each one.
Selecting a backend is a single environment variable, DB_DIALECT, with the connection supplied as a URL or as discrete DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_DATABASE fields. Nothing in your handlers changes, which is what makes "develop on SQLite, deploy on Postgres" a configuration decision rather than a porting project.
The parts that genuinely differ between engines — identifier quoting, upsert syntax, JSON access, returning clauses, concurrent-index creation — are the dialect layer's job, and hand-written SQL is run against every supported engine by a shared parity harness so a Postgres-only statement fails in the test suite rather than in someone else's production MySQL.
What is a declarative migration, and why not migration files?
Your schema file is the desired state. `voltro db plan` diffs it against what the database actually contains and prints the operations that would close the gap; `voltro db apply` runs them. You do not hand-write an up and a down for every change, and there is no numbered file whose order matters.
The reason is that a hand-written migration chain encodes intent twice — once in the schema and once in the steps — and those two copies drift. A differ reads the live catalog, so it cannot be wrong about what is there. It classifies each operation, refuses lossy ones unless you say otherwise, and proves convergence: after applying, the next plan must come out empty, or the migration is not finished.
Renames are declared rather than inferred. `table('new', …).renamedFrom('old')` and the column equivalent tell the differ that this is a move, not a drop and a create — the distinction the data depends on. Above a row threshold (50,000 by default) index and column changes are planned as online, non-blocking operations instead of table-locking ones.
Where does the real-time behaviour come from?
From the database itself, not from a polling loop in the application. Each dialect has a native change feed — Postgres LISTEN/NOTIFY, the MySQL and MariaDB binlog, SQL Server Change Tracking, an in-process bus for SQLite — and the runtime consumes it. A write from anywhere, including a raw SQL client or another service, wakes the subscriptions that read the affected table.
That is also what makes cache invalidation a property of the system rather than a discipline you maintain: the fact "this table changed" is already flowing, so cached query results are dropped by the same event that pushes a delta to the browser. For dialects with no cross-instance feed, or any deployment behind a load balancer, a broadcast plugin fans the events over Redis or NATS.
What about replicas, encryption and data residency?
Read replicas are configured, not coded around: point DB_REPLICA_URLS at them and reads are distributed while writes go to the primary. Read-your-writes consistency is a setting, because the alternative — a user who saves a form and then sees the old value — is the failure that makes teams abandon replicas entirely.
Column-level encryption at rest is a marker on the column plus a key in the environment; the store encrypts on write and decrypts on read, including through the store a plugin receives. Exposure is a separate marker: a server-only column is stripped from every wire response structurally, so "encrypted" and "not sent to the browser" stay two decisions rather than one assumption.
Data residency is declarable. Configure the regions you serve and the framework opens one store per region, routing each request to its tenant's home region — or refusing it. That turns a legal requirement into a boot-time configuration instead of a review checklist someone has to remember.
Supported engines.
| Engine | Notes |
|---|---|
| PostgreSQL | The reference dialect. Native LISTEN/NOTIFY drives cross-instance reactivity without a broker. |
| MySQL 8+ | Binlog-based change capture; full migration and query support. |
| MariaDB 10.6+ | Binlog-based change capture; treated as its own dialect, not as MySQL. |
| SQL Server 2019+ | Change Tracking for reactivity; dialect-specific SQL handled by the compiler. |
| SQLite 3.38+ | In-process change bus. Single node; add the broadcast plugin for a fleet. |
| Turso (beta) | The Rust SQLite rewrite with MVCC concurrent writes. Single node; no generated columns or FTS yet. |
Frequently asked questions
Can I develop on SQLite and deploy on Postgres?
Yes — that is the intended workflow. Set DB_DIALECT to sqlite locally and postgres in production; the schema, queries, migrations and reactive engine are identical. Verify with the same test suite, which runs hand-written SQL against every dialect through a parity harness.
Do I have to write SQL migration files?
No. The schema file is the desired state and the differ computes the operations to reach it. You run `voltro db plan` to review and `voltro db apply` to execute. Renames are declared with `.renamedFrom()` so a move is never mistaken for a drop and a create.
Does real-time work with more than one instance?
On Postgres and MySQL/MariaDB the change feed is fleet-wide, so a write on any replica reaches subscribers on every replica. For SQLite, SQL Server or any deployment where you want an explicit bus, the broadcast plugin fans events over Redis or NATS without changing application code.
Can Voltro use my existing database?
Yes. The framework is a runtime that lives next to your database — your connection string, your backups, your access controls. Existing tables can be adopted into the schema, and nothing is proxied through third-party infrastructure.
How are lossy migrations prevented?
Each planned operation is classified, and a destructive one is refused unless explicitly confirmed. Applying also proves convergence: the next plan must be empty, so a migration that quietly re-proposes itself is treated as unfinished rather than as success.
The schema feeds the rest of the runtime.
Schema DSL
Compose tables with mixins — audit, tenant, soft-delete — one line per cross-cutting concern.
Reactive queries
.reactive() opts a table into the matcher engine so its writes wake subscriptions.
Real-time & fan-out
Native CDC plus plugin-broadcast keep reactivity working across a multi-replica fleet.
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.