# database.hosting

> Run Voltro on any hosted Postgres, MySQL, MariaDB, or SQL Server — Supabase, Neon, Vercel Postgres, Railway, Render, Fly.io, AWS RDS, PlanetScale, Azure SQL, and more. Connection strings, pooling, gotchas.



---

<!-- source: en/database/providers/index.md -->
## Database hosting

_Run Voltro on any hosted Postgres, MySQL, MariaDB, or SQL Server — Supabase, Neon, Vercel Postgres, Railway, Render, Fly.io, AWS RDS, PlanetScale, Azure SQL, and more. Connection strings, pooling, gotchas._

Voltro runs on five SQL backends — postgres, mysql 8+, mariadb 10.6+, mssql 2019+, and sqlite (local file) — plus the beta **Turso** dialect (the Rust SQLite rewrite, local file or `:memory:`). Any managed database that speaks one of those dialects works. You set two environment variables — `DB_DIALECT` and `DB_URL` — and the framework's schema, queries, workflows, and reactive engine run unchanged.

This page maps the common hosted-database providers to the Voltro dialect they back, the connection shape, and the gotchas worth knowing before you deploy. Each provider has its own page below.

## The two-variable contract

```sh
DB_DIALECT=postgres                                   # postgres | mysql | mariadb | mssql | sqlite
DB_URL=postgresql://user:pass@host:5432/db?sslmode=require
```

`DB_DIALECT` picks the driver; `DB_URL` is the connection string for the provider. When a single URL doesn't fit, the discrete `DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASSWORD` / `DB_DATABASE` fields are the dialect-agnostic alternative (and `PG_HOST` / `PG_PORT` / `PG_USER` / `PG_PASSWORD` / `PG_DATABASE` are accepted as a fallback). Env always wins over `app.config.ts`'s `store:` field.

## Declare your dialect's driver

Each app declares the driver package for its DB dialect as a dependency — the CLI does not bundle every driver, so a production image only ships the one your app actually uses (a memory-store app ships none, and never pulls e.g. turso's ~90 MB native binary):

| Dialect | Driver package |
| --- | --- |
| `postgres` | `@voltro/sql-postgres` |
| `mysql` / `mariadb` | `@voltro/sql-mysql` |
| `mssql` | `@voltro/sql-mssql` |
| `sqlite` | `@voltro/sql-sqlite` |
| `turso` | `@voltro/sql-turso` |

```sh
pnpm add @voltro/sql-postgres      # in the app that uses store: 'postgres'
```

Scaffolded projects already declare the right driver, and `voltro add mssql` adds `@voltro/sql-mssql` for you. If a driver is missing at boot, `voltro serve` fails with a message naming the exact package to install — it never silently falls back. A `store: 'memory'` app needs no driver.

## Providers at a glance

| Provider | Voltro dialect | Pooling | Notes |
|----------|----------------|---------|-------|
| [Supabase](./supabase) | postgres | Supavisor (`:6543` txn / `:5432` session) | LISTEN/NOTIFY needs the **session** port or a direct connection; the transaction pooler drops it. |
| [Neon](./neon) | postgres | PgBouncer (`-pooler` endpoint) | Serverless functions use the pooled endpoint; CDC listener uses the direct endpoint. Branching for preview DBs. |
| [Vercel Postgres](./vercel-postgres) | postgres | Neon-backed pooler | Neon under the hood. Same pooled-vs-direct split as Neon. |
| [Railway](./railway) | postgres / mysql | none by default | Plain managed Postgres or MySQL. Use the private network URL in-cluster. |
| [Render](./render) | postgres | none by default | Internal vs external connection strings; internal avoids egress + SSL friction. |
| [Fly.io Postgres](./fly-io) | postgres | none (or pgbouncer app) | Unmanaged Postgres app — you own backups + HA. Flycast for internal routing. |
| [AWS RDS / Aurora](./aws-rds) | postgres / mysql / mariadb | RDS Proxy (optional) | Full LISTEN/NOTIFY on Postgres (direct endpoint for the CDC listener). MariaDB adds binlog CDC; needs a binlog-enabled parameter group. |
| [DigitalOcean](./digitalocean) | postgres / mysql | built-in connection pool | Pooler in `transaction` mode drops LISTEN/NOTIFY — add a direct connection for CDC. |
| [Timescale](./timescale) | postgres | none by default | Postgres + the TimescaleDB extension. Full reactive surface; hypertables are opt-in per table. |
| [CockroachDB](./cockroachdb) | postgres (wire) | built-in | **Caveat:** wire-compatible, NOT feature-compatible. **No `LISTEN/NOTIFY` → no native CDC.** Cross-instance reactivity still works via [`@voltro/plugin-broadcast`](../multi-replica) (Redis / NATS fan-out of app-mutation changes). May not pass all of Voltro's pg DDL. SQL surface only. |
| [PlanetScale](./planetscale) | mysql | built-in (Vitess) | **Caveats:** no native CDC (mysql is inline-only; no binlog access on Vitess) → use [`@voltro/plugin-broadcast`](../multi-replica) for cross-instance reactivity. Vitess also disables foreign keys by default — enable FK support on the branch or `reference()` cascades won't apply. |
| [Azure SQL](./azure-sql) | mssql | none by default | SQL Server 2019+ surface. **CDC is inline-only (no native cross-instance feed) — add [`@voltro/plugin-broadcast`](../multi-replica) for cross-instance reactivity.** `OUTPUT INSERTED` instead of RETURNING; encrypted connection required. |
| Turso | turso (local file) | in-process MVCC pool | **Supported (beta)** via [@voltro/sql-turso](../dialects/turso) — the Rust SQLite rewrite with MVCC concurrent writes (`BEGIN CONCURRENT`). Embedded engine: a local file or `:memory:`, NOT a hosted provider. Remote **libSQL / Turso Cloud** (`libsql://…`) is still **rejected** — that's a libSQL-client concern, not this dialect. |

Legend: the dialect column is the value you set for `DB_DIALECT`. "Pooling" is the connection-pooler the provider ships; it matters because the native Postgres reactive path (LISTEN/NOTIFY CDC) needs a long-lived session-mode connection — transaction-mode poolers silently break it (the [broadcast plugin](../multi-replica) sidesteps that, see below).

## The one cross-cutting gotcha: pooling vs LISTEN/NOTIFY

On Postgres, Voltro's low-latency **native** reactive path uses `LISTEN/NOTIFY`. That needs a **long-lived, session-mode** connection. A transaction-mode pooler (Supabase Supavisor `:6543`, Neon's pooled endpoint, DigitalOcean's `transaction` pool, RDS Proxy without pinning) hands you a fresh backend per transaction — the `LISTEN` is registered on a connection you never see again, so change events never arrive.

You have **two ways to fix it** — pick by what you need:

1. **Keep native CDC** — query the CDC listener to a **direct / session-mode** connection while application queries keep going through the pool. Each provider page below spells out the exact endpoints. This preserves the full Postgres path: low-latency fan-out *and* capture of out-of-band writes (a `psql` session, another service touching the DB).

2. **Skip the listener with [`@voltro/plugin-broadcast`](../multi-replica)** — a Redis / NATS pub/sub bus that fans every committed `ctx.store` mutation out to all replicas. It opens no `LISTEN` connection, so it works *straight through* the transaction pooler — and it's the same plugin you'd add for cross-replica reactivity on a multi-pod deployment anyway. Trade-off: the bus carries app-mutation changes, **not** out-of-band DB writes — only the native `LISTEN/NOTIFY` path observes those.

MySQL / MSSQL don't have the pooling concern at all — their change path is inline (single-instance) and, for cross-replica fan-out, rides the broadcast plugin. MariaDB adds native binlog CDC (not connection-pinned). SQLite is in-process. See [Multi-replica reactivity](../multi-replica) for the full tier breakdown.

## What the dialect page covers vs what these pages cover

These provider pages are about **getting connected** — the connection string, SSL, pooling, and provider-specific quirks. For the deep dialect surface — RETURNING gaps, CDC mechanics, cluster workflow locks, read-replica routing, JSON handling, identifier quoting — see the underlying [SQL dialects](../dialects) section. Every provider page links to its dialect page.



---

<!-- source: en/database/providers/supabase.md -->
## Voltro + Supabase

_Connect Voltro to Supabase Postgres — Supavisor pooler vs direct connection, LISTEN/NOTIFY for the reactive engine, sslmode=require, and the transaction-pooler gotcha._

Supabase is managed Postgres, so it uses Voltro's **postgres** dialect — the reference dialect with the full reactive surface: `LISTEN/NOTIFY` CDC, advisory-lock cluster workflows, streaming-replication read replicas, and `JSONB`. Nothing about the schema DSL, queries, or workflows changes.

The one thing to get right is **which connection you point CDC at**. Supabase ships the Supavisor pooler, and its transaction mode breaks `LISTEN/NOTIFY`.

## Connect

Supabase gives you three connection strings under **Project Settings → Database**. Pick by use case:

```sh
# Direct connection (port 5432) — session-mode, supports LISTEN/NOTIFY.
# Use this when your deployment has a stable, bounded connection count
# (a long-running container, not per-request serverless).
DB_DIALECT=postgres
DB_URL=postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres?sslmode=require
```

```sh
# Supavisor — session mode (port 5432 on the pooler host).
# Pooled BUT keeps one backend per client connection, so LISTEN/NOTIFY works.
DB_DIALECT=postgres
DB_URL=postgresql://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres?sslmode=require
```

```sh
# Supavisor — transaction mode (port 6543). For serverless / high
# connection churn. ⚠ Does NOT support LISTEN/NOTIFY — see below.
DB_DIALECT=postgres
DB_URL=postgresql://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres?sslmode=require
```

`sslmode=require` is mandatory — Supabase rejects unencrypted connections.

## Enabling CDC

On Postgres, Voltro's change-data-capture path is `LISTEN/NOTIFY` and it's **on by default** (`CDC=1`). There's no extension to install and no flag to flip — the only requirement is that the framework's listener runs over a **session-mode / direct connection**. Concretely: point `DB_URL` (or at least the CDC listener) at the direct `:5432` connection or Supavisor **session mode**, and reactivity works immediately. The transaction pooler is the one thing that silently breaks it (below).

Voltro's reactive engine (the thing that pushes a fresh snapshot to subscribers with very low latency) uses Postgres `LISTEN/NOTIFY`. That requires a **long-lived, session-mode** connection: the framework opens one dedicated `LISTEN` connection per process and keeps it open for the process lifetime.

Supavisor **transaction mode (`:6543`)** hands each transaction a different backend from the pool. The `LISTEN` registers on a backend you never see again — so no change events arrive, and subscriptions go silent (no error, just no updates).

Decision rubric:

- **Long-running container / VM (k8s, Fly, Railway, a PM2 box)** → use the **direct connection (`:5432`)** or **Supavisor session mode**. Reactivity works out of the box.
- **Serverless functions (per-request lifecycle)** → use **transaction mode (`:6543`)** for the app's queries, but you lose the listener — so run the framework with `CDC=0` to force the inline-emit path (single-process change emission, no cross-process fan-out), or split: point app queries at the pooler and the CDC listener at a direct connection. For a push-reactive Voltro app, serverless is the wrong shape — prefer a long-running process.

## SSL + connection limits

- **SSL**: always `sslmode=require`. Supabase's CA is widely trusted; you rarely need a custom `sslrootcert`.
- **Connection limits**: the direct connection caps at the instance's `max_connections` (small on the free tier — ~60). Move app traffic to the Supavisor pooler and keep only the single CDC listener on the direct connection, or raise the instance's `max_connections` in the Supabase dashboard. (The framework opens one pool per process; cap its size per process with `DB_MAX_CONNECTIONS` — maps to the postgres `max`, mysql `connectionLimit`, and mssql `pool.max`.)

## Tables in a non-`public` schema — `DB_SCHEMA`

If your app's tables live in a schema other than `public` (e.g. `voltro`), set **`DB_SCHEMA`**:

```bash
DB_SCHEMA=voltro
```

The framework then opens every pooled connection with `search_path = voltro` (the Postgres connection-startup `options` parameter, applied server-side before the connection is usable). That one setting makes everything consistent: schema introspection (`voltro db plan` / auto-migrate use `current_schema()`), the unqualified DDL the migrator emits, and runtime queries all target `voltro`.

Without it the connection's `search_path` stays at the Supabase default (`public`), so the planner introspects an *empty* `public` schema, decides your entire schema is missing, and tries to **recreate every table** — the classic "the differ wants to create 800 tables that already exist" symptom. `DB_SCHEMA` is the fix; leave it unset for the normal `public`-schema case. (Postgres-only — other dialects ignore it.)

## `db plan` / `migrate` over the pooler — large schemas

`voltro db plan` / `apply` / `drift` / `migrate` introspect the live schema (every column, index and constraint of every table). On a **large schema** (hundreds of tables) that introspection is big, and the **Supavisor pooler can mis-frame a single huge response** — node-postgres' parser reads a garbage field length and the command dies with a raw `RangeError [ERR_OUT_OF_RANGE]`, or you get a bare `Connection error` / a multi-minute hang at zero output. This is **not** the LISTEN/NOTIFY issue above: it crashes on **session mode too**, independent of `:5432` vs `:6543`, because the trigger is response *size*, not transaction multiplexing. (Small schemas — a few dozen tables — never hit it.)

**The introspection is batched by table** — `VOLTRO_INTROSPECT_BATCH` tables per detail query (default 20), so no single response is large enough to trip the pooler. `db plan` runs over the **normal pooler**, exactly like the runtime — no direct connection required. If a batch still mis-frames on a very wide schema, **lower it**:

```bash
VOLTRO_INTROSPECT_BATCH=5 voltro db plan      # or 1 — smaller responses, more round-trips
```

### Escape hatch: `DB_DIRECT_URL` (bypass the pooler)

You can instead point only the migration path at a direct (non-pooler) connection:

```bash
DB_URL=postgresql://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres?sslmode=require
# Migration-path-only override — db plan / apply / drift / migrate use this instead:
DB_DIRECT_URL=postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres?sslmode=require
```

`DB_DIRECT_URL` (alias `DB_MIGRATE_URL`) overrides `DB_URL` for the db-command path **only** — the runtime keeps `DB_URL`, and `DB_SCHEMA` stays pinned on both. **Caveat:** the Supabase direct host `db.<project-ref>.supabase.co` is **IPv6-only** on newer projects, so on an IPv4-only network it needs the paid IPv4 add-on — which is why the batched-pooler path above is usually the practical one. If you do hit the crash, the CLI prints this guidance (lower the batch / use a direct URL) instead of a raw stacktrace.

## Verify

After `voltro dev` (or `voltro start`) connects, the boot log prints the resolved dialect:

```
[voltro:dev] sql dialect resolved: postgres — CDC: LISTEN/NOTIFY, RETURNING: native
[voltro:dev] read replicas: 0 configured (DB_REPLICA_URLS empty) — all queries → primary
[voltro:dev] workflow engine: cluster-sql, dialect=postgres
```

Confirm with `voltro logs --tail 50`. If you connected through the transaction pooler, subscriptions will deliver their first snapshot but never update — that's the LISTEN/NOTIFY symptom; switch to a session-mode connection.

## See also

- [Postgres dialect](../dialects/postgres) — the full reactive surface: LISTEN/NOTIFY internals, advisory-lock workflows, read replicas, JSONB.
- [Neon](./neon) — the other major serverless Postgres, same pooled-vs-direct split.



---

<!-- source: en/database/providers/neon.md -->
## Voltro + Neon

_Connect Voltro to Neon serverless Postgres — the -pooler endpoint vs direct endpoint, sslmode=require, LISTEN/NOTIFY for reactivity, and using Neon branches for preview databases._

Neon is serverless Postgres, so it uses Voltro's **postgres** dialect — the reference dialect with the full reactive surface: `LISTEN/NOTIFY` CDC, advisory-lock cluster workflows, read replicas, and `JSONB`. The schema DSL, queries, and workflows are unchanged.

Neon's two things to get right: the **pooled vs direct endpoint** split (it affects reactivity exactly like Supabase's pooler does), and **branching**, which is a genuinely nice fit for Voltro preview deploys.

## Connect

Neon gives you two host variants for the same database. The pooled host has `-pooler` in the subdomain:

```sh
# Pooled endpoint (PgBouncer, transaction mode) — for serverless /
# high connection churn. ⚠ Does NOT support LISTEN/NOTIFY.
DB_DIALECT=postgres
DB_URL=postgresql://[USER]:[PASSWORD]@[ENDPOINT]-pooler.[REGION].aws.neon.tech/[DB]?sslmode=require
```

```sh
# Direct endpoint (no -pooler) — session connection, supports
# LISTEN/NOTIFY. Use this for long-running processes that need the
# reactive engine, OR for the CDC listener specifically.
DB_DIALECT=postgres
DB_URL=postgresql://[USER]:[PASSWORD]@[ENDPOINT].[REGION].aws.neon.tech/[DB]?sslmode=require
```

`sslmode=require` is mandatory — Neon only accepts encrypted connections.

## Enabling CDC

Voltro's change-data-capture is `LISTEN/NOTIFY`, **on by default** (`CDC=1`) — no extension, no flag. The single requirement on Neon is to point `DB_URL` (or at least the CDC listener) at the **direct (non-`-pooler`) endpoint**, which is session-mode. The pooled `-pooler` endpoint is transaction-mode PgBouncer and breaks `LISTEN/NOTIFY` (below).

Voltro's reactive engine uses Postgres `LISTEN/NOTIFY`, which needs a **long-lived, session-mode** connection. Neon's **pooled `-pooler` endpoint** runs PgBouncer in transaction mode — each transaction gets a different backend, so a `LISTEN` registered on one is invisible to the next. Subscriptions then deliver their first snapshot but never update.

Decision rubric:

- **Long-running container / VM** → use the **direct (non-pooler) endpoint**. Reactivity works out of the box. Watch your connection count against the compute's limit.
- **Serverless functions** → use the **pooled endpoint** for app queries (PgBouncer is what makes high connection churn survivable), but the listener won't work there. Either run with `CDC=0` (inline-emit, single-process, no cross-process fan-out) or split the listener onto a direct endpoint. A push-reactive Voltro app is happiest as a long-running process — prefer that over serverless if you want live subscriptions.

### Autosuspend vs the long-lived listener

Neon's compute **autosuspends when idle** (scale-to-zero). The framework's CDC listener is a single long-lived connection that holds the `LISTEN`, so on a reactive Voltro app the listener is exactly what keeps the compute warm — useful if you want zero cold-start latency on writes, but it also means the compute never scales to zero while a Voltro process is connected (factor that into the cost model). When the compute *is* suspended (e.g. the app was down and Neon idled out), the listener connection is dropped on suspend; the framework reconnects on the next attempt and re-registers the `LISTEN`. Keep an eye on the connect/reconnect lines in `voltro logs` after an idle window — a missed reconnect shows up as subscriptions that snapshot but never update, same symptom as the pooler gotcha.

## Branching → preview databases

Neon branches are copy-on-write database clones created in seconds. They map cleanly onto Voltro's auto-migrate boot flow: point a preview deploy at a fresh branch's connection string and `voltro dev` / `voltro start` auto-applies the schema on first boot.

```sh
# Per-preview-environment: a Neon branch URL drives an isolated DB.
DB_DIALECT=postgres
DB_URL=postgresql://[USER]:[PASSWORD]@[BRANCH-ENDPOINT].[REGION].aws.neon.tech/[DB]?sslmode=require
```

Each branch carries its own schema state, so previews never collide with production data. Tear the branch down when the preview is gone.

## SSL + connection limits

- **SSL**: always `sslmode=require`. Neon also supports the `endpoint=` SNI workaround for older drivers, but `@effect/sql-pg` (node-postgres) handles SNI natively — you don't need it.
- **Connection limits**: the direct endpoint is bounded by the compute size; the pooled endpoint absorbs far more concurrent clients. Keep the CDC listener (one connection per process) on the direct endpoint and query bulk app traffic through the pooler when connection count is a concern.

## Verify

After connecting, the boot log prints the resolved dialect:

```
[voltro:dev] sql dialect resolved: postgres — CDC: LISTEN/NOTIFY, RETURNING: native
[voltro:dev] read replicas: 0 configured (DB_REPLICA_URLS empty) — all queries → primary
[voltro:dev] workflow engine: cluster-sql, dialect=postgres
```

Confirm with `voltro logs --tail 50`. If subscriptions deliver an initial snapshot but never update, you're on the `-pooler` endpoint — move the listener to the direct endpoint.

## See also

- [Postgres dialect](../dialects/postgres) — LISTEN/NOTIFY internals, advisory-lock workflows, read replicas, JSONB.
- [Vercel Postgres](./vercel-postgres) — Neon-backed; same connection model.
- [Supabase](./supabase) — the other major managed Postgres, same pooled-vs-direct split.



---

<!-- source: en/database/providers/vercel-postgres.md -->
## Voltro + Vercel Postgres

_Connect Voltro to Vercel Postgres (Neon-backed) — POSTGRES_URL vs the pooled URL, sslmode=require, LISTEN/NOTIFY reactivity, and the serverless pooling caveat._

Vercel Postgres is **Neon under the hood**, so it uses Voltro's **postgres** dialect with the full reactive surface — `LISTEN/NOTIFY` CDC, advisory-lock cluster workflows, read replicas, `JSONB`. Everything that's true of [Neon](./neon) applies here; Vercel just rebrands the connection strings as env vars.

## Connect

Vercel's integration injects a set of `POSTGRES_*` env vars. Two matter for Voltro:

```sh
# Pooled connection (the default POSTGRES_URL — PgBouncer, transaction
# mode). For serverless functions. ⚠ Does NOT support LISTEN/NOTIFY.
DB_DIALECT=postgres
DB_URL=postgresql://[USER]:[PASSWORD]@[ENDPOINT]-pooler.[REGION].postgres.vercel-storage.com/[DB]?sslmode=require
```

```sh
# Direct connection (POSTGRES_URL_NON_POOLING) — session connection,
# supports LISTEN/NOTIFY. Use for long-running processes / the CDC listener.
DB_DIALECT=postgres
DB_URL=postgresql://[USER]:[PASSWORD]@[ENDPOINT].[REGION].postgres.vercel-storage.com/[DB]?sslmode=require
```

Map `POSTGRES_URL_NON_POOLING` → `DB_URL` when you want reactivity; map the pooled `POSTGRES_URL` only for serverless query traffic. `sslmode=require` is mandatory.

## Enabling CDC

Voltro's change-data-capture is `LISTEN/NOTIFY`, **on by default** (`CDC=1`). On Vercel Postgres the one requirement is to map `POSTGRES_URL_NON_POOLING` → `DB_URL` (or at least to query the CDC listener there) — that's the direct, session-mode endpoint. The default `POSTGRES_URL` is the pooled, transaction-mode endpoint and breaks `LISTEN/NOTIFY` (below).

Identical to Neon: the pooled (`-pooler`) endpoint runs transaction-mode PgBouncer and **breaks `LISTEN/NOTIFY`**. The reactive engine needs a long-lived session connection.

- **Long-running container** → use `POSTGRES_URL_NON_POOLING` as `DB_URL`. Reactivity works.
- **Vercel serverless functions** → the function lifecycle is per-request; a push-reactive Voltro process wants to stay alive. If you deploy the API as a long-running service (not a serverless function), point it at the non-pooling URL. If you must run serverless, use the pooled URL with `CDC=0` (inline-emit only).

Because Vercel Postgres is Neon under the hood, its compute **autosuspends when idle** — the same warm-keeping + reconnect behavior described on the [Neon](./neon) page applies to the long-lived CDC listener here.

## Verify

```
[voltro:dev] sql dialect resolved: postgres — CDC: LISTEN/NOTIFY, RETURNING: native
[voltro:dev] read replicas: 0 configured (DB_REPLICA_URLS empty) — all queries → primary
[voltro:dev] workflow engine: cluster-sql, dialect=postgres
```

Confirm with `voltro logs --tail 50`. Subscriptions that snapshot but never update mean you're on the pooled URL — switch to `POSTGRES_URL_NON_POOLING`.

## See also

- [Neon](./neon) — the engine behind Vercel Postgres; branching + the same pooled/direct split.
- [Postgres dialect](../dialects/postgres) — LISTEN/NOTIFY internals, workflows, replicas, JSONB.



---

<!-- source: en/database/providers/railway.md -->
## Voltro + Railway

_Deploy Voltro on Railway with managed Postgres or MySQL — public vs private network connection strings, the DATABASE_URL variable, SSL, and full LISTEN/NOTIFY reactivity._

Railway offers managed **Postgres** and **MySQL** plugins, mapping to Voltro's `postgres` and `mysql` dialects respectively. Railway provisions a plain, single-instance database with no transaction pooler in front of it — so on Postgres you get the **full reactive surface** (`LISTEN/NOTIFY` CDC, advisory-lock workflows, `JSONB`) with no pooling caveat.

## Connect

Railway injects a `DATABASE_URL` variable into services in the same project. Prefer the **private network** URL (`*.railway.internal`) for service-to-database traffic — it avoids public egress and the associated SSL requirement.

```sh
# Postgres — private network (recommended in-project)
DB_DIALECT=postgres
DB_URL=postgresql://postgres:[PASSWORD]@[SERVICE].railway.internal:5432/railway
```

```sh
# Postgres — public proxy (for connecting from outside Railway)
DB_DIALECT=postgres
DB_URL=postgresql://postgres:[PASSWORD]@[HOST].proxy.rlwy.net:[PORT]/railway?sslmode=require
```

```sh
# MySQL plugin
DB_DIALECT=mysql
DB_URL=mysql://root:[PASSWORD]@[SERVICE].railway.internal:3306/railway
```

For the MySQL dialect, note the cross-dialect differences the framework handles for you: no `RETURNING` (INSERT-then-SELECT), inline-only CDC (single-instance reactivity, not the native cross-instance LISTEN/NOTIFY), and `0/1` booleans. See the [MySQL dialect](../dialects/mysql) page.

## Enabling CDC

On Postgres, Voltro's change-data-capture is `LISTEN/NOTIFY`, **on by default** (`CDC=1`) — no extension, no flag. Railway runs a plain single-instance Postgres with **no transaction pooler in front**, so `DB_URL` already points at a session-mode connection and reactivity works the moment you connect. Nothing to configure. (On the MySQL plugin, CDC is inline-only / single-instance — see the dialect note below.)

## Pooling / SSL

- **No pooler by default.** Postgres reactivity works directly — nothing to configure for `LISTEN/NOTIFY`.
- **SSL**: the private network URL runs over Railway's internal network and typically doesn't need `sslmode=require`. The public proxy URL does — append `?sslmode=require`.
- **Connection limits**: bounded by the plan's Postgres instance size; scale the instance up if you outgrow it. (The framework opens one pool per process; cap its size per process with `DB_MAX_CONNECTIONS` — maps to the postgres `max`, mysql `connectionLimit`, and mssql `pool.max`.)

## Verify

Postgres:

```
[voltro:dev] sql dialect resolved: postgres — CDC: LISTEN/NOTIFY, RETURNING: native
[voltro:dev] workflow engine: cluster-sql, dialect=postgres
```

MySQL:

```
[voltro:dev] sql dialect resolved: mysql — CDC: inline only (no binlog CDC), RETURNING: INSERT/UPDATE/DELETE then SELECT (no RETURNING)
[voltro:dev] workflow engine: cluster-sql, dialect=mysql, runnerStorage=sql
```

Confirm with `voltro logs --tail 50`.

## See also

- [Postgres dialect](../dialects/postgres) · [MySQL dialect](../dialects/mysql)



---

<!-- source: en/database/providers/render.md -->
## Voltro + Render

_Run Voltro on Render with managed Postgres — internal vs external connection strings, sslmode, full LISTEN/NOTIFY reactivity, and connection limits per plan._

Render offers managed **Postgres**, mapping to Voltro's **postgres** dialect with the full reactive surface — `LISTEN/NOTIFY` CDC, advisory-lock cluster workflows, read replicas, `JSONB`. Render runs a plain Postgres instance with no transaction pooler in front, so there's no LISTEN/NOTIFY caveat.

## Connect

Render gives you two connection strings: an **internal** one (only reachable from other Render services in the same region) and an **external** one (reachable from anywhere). Prefer internal for service-to-database traffic — it skips public egress and is lower latency.

```sh
# Internal connection (recommended for Render-hosted services)
DB_DIALECT=postgres
DB_URL=postgresql://[USER]:[PASSWORD]@[HOST]-a/[DB]
```

```sh
# External connection (from outside Render, e.g. local dev / CI)
DB_DIALECT=postgres
DB_URL=postgresql://[USER]:[PASSWORD]@[HOST]-a.[REGION]-postgres.render.com/[DB]?sslmode=require
```

## Enabling CDC

Voltro's change-data-capture is `LISTEN/NOTIFY`, **on by default** (`CDC=1`) — no extension, no flag. Render runs a plain Postgres instance with **no transaction pooler in front**, so whichever connection string you map to `DB_URL` (internal or external) is session-mode and reactivity works immediately. If you later add a `pgbouncer` to scale connections, keep it in **session mode** or pin the single CDC listener to a direct connection so `LISTEN/NOTIFY` survives.

## Pooling / SSL

- **No pooler by default** — Postgres reactivity works directly, no config needed for `LISTEN/NOTIFY`.
- **SSL**: the external URL requires `sslmode=require`. The internal URL runs over Render's private network and doesn't.
- **Connection limits**: each plan caps `max_connections` (low on the starter tiers). Add a `pgbouncer` (in session mode) if you outgrow the cap — keep the single CDC listener on a direct connection if you do. (The framework opens one pool per process; cap its size per process with `DB_MAX_CONNECTIONS` — maps to the postgres `max`, mysql `connectionLimit`, and mssql `pool.max`.)

## Verify

```
[voltro:dev] sql dialect resolved: postgres — CDC: LISTEN/NOTIFY, RETURNING: native
[voltro:dev] read replicas: 0 configured (DB_REPLICA_URLS empty) — all queries → primary
[voltro:dev] workflow engine: cluster-sql, dialect=postgres
```

Confirm with `voltro logs --tail 50`.

## See also

- [Postgres dialect](../dialects/postgres) — LISTEN/NOTIFY internals, workflows, read replicas, JSONB.



---

<!-- source: en/database/providers/fly-io.md -->
## Voltro + Fly.io Postgres

_Run Voltro on Fly.io with a Postgres app — Flycast internal routing, the connection string, full LISTEN/NOTIFY reactivity, and the unmanaged-database tradeoff._

Fly Postgres is a **Fly app running Postgres**, not a fully managed database — you own backups, failover, and scaling. It maps to Voltro's **postgres** dialect with the full reactive surface: `LISTEN/NOTIFY` CDC, advisory-lock cluster workflows, read replicas, `JSONB`. No transaction pooler sits in front by default, so reactivity works directly.

## Connect

Inside the Fly private network, connect over the app's `.internal` / Flycast address:

```sh
# Fly private network (6PN) — app-to-database within your org
DB_DIALECT=postgres
DB_URL=postgresql://postgres:[PASSWORD]@[PG-APP-NAME].internal:5432/[DB]
```

```sh
# Flycast (internal load-balanced address — preferred for HA clusters)
DB_DIALECT=postgres
DB_URL=postgresql://postgres:[PASSWORD]@[PG-APP-NAME].flycast:5432/[DB]
```

When you `fly postgres attach`, Fly sets a `DATABASE_URL` secret on the consuming app — map it straight to `DB_URL`.

## Enabling CDC

Voltro's change-data-capture is `LISTEN/NOTIFY`, **on by default** (`CDC=1`) — no extension, no flag. A Fly Postgres app has **no transaction pooler in front by default**, so the `.internal` / Flycast connection you map to `DB_URL` is session-mode and reactivity works immediately. If you front it with a `pgbouncer` app for connection scaling, keep that pooler in **session mode** or pin the single CDC listener to a direct connection so `LISTEN/NOTIFY` survives.

## Pooling / SSL / region

- **No pooler by default.** Postgres reactivity works directly. If you front it with a `pgbouncer` app for connection scaling, keep it in **session mode** (or run the CDC listener on a direct connection) so `LISTEN/NOTIFY` survives.
- **SSL**: traffic over the Fly private network (6PN / Flycast) is already isolated; you typically don't need `sslmode=require` internally. Public access does.
- **Region locality**: if you run Postgres read replicas across regions, set `DB_REPLICA_URLS` + `DB_REPLICA_REGIONS` and let the framework's `FLY_REGION` auto-detection prefer same-region replicas. See [read replicas](../read-replicas).

## Unmanaged tradeoff

Fly Postgres is closer to "Postgres on a VM" than to RDS — there's no automated point-in-time recovery unless you wire it up. Fine for the reactive workload; just own the backup story.

## Verify

```
[voltro:dev] sql dialect resolved: postgres — CDC: LISTEN/NOTIFY, RETURNING: native
[voltro:dev] read replicas: 0 configured (DB_REPLICA_URLS empty) — all queries → primary
[voltro:dev] workflow engine: cluster-sql, dialect=postgres
```

Confirm with `voltro logs --tail 50`.

## See also

- [Postgres dialect](../dialects/postgres) · [Read replicas](../read-replicas) — region-aware replica routing.



---

<!-- source: en/database/providers/aws-rds.md -->
## Voltro + AWS RDS / Aurora

_Run Voltro on AWS RDS or Aurora — Postgres and MySQL connection strings, RDS Proxy and the session-mode requirement for LISTEN/NOTIFY, SSL, and read replicas._

AWS RDS and Aurora offer managed **Postgres**, **MySQL**, and **MariaDB**, mapping to Voltro's `postgres`, `mysql`, and `mariadb` dialects. RDS/Aurora Postgres is the production-grade home for Voltro's full reactive surface — `LISTEN/NOTIFY` CDC, advisory-lock cluster workflows, streaming-replication read replicas, `JSONB`.

## Connect

```sh
# RDS / Aurora Postgres
DB_DIALECT=postgres
DB_URL=postgresql://[USER]:[PASSWORD]@[INSTANCE].[ID].[REGION].rds.amazonaws.com:5432/[DB]?sslmode=require
```

```sh
# RDS / Aurora MySQL
DB_DIALECT=mysql
DB_URL=mysql://[USER]:[PASSWORD]@[INSTANCE].[ID].[REGION].rds.amazonaws.com:3306/[DB]?ssl-mode=REQUIRED
```

```sh
# RDS MariaDB
DB_DIALECT=mariadb
DB_URL=mysql://[USER]:[PASSWORD]@[INSTANCE].[ID].[REGION].rds.amazonaws.com:3306/[DB]?ssl-mode=REQUIRED
```

For the MySQL dialect, the framework handles the cross-dialect gaps for you (no `RETURNING`, inline CDC, `0/1` booleans) — see the [MySQL dialect](../dialects/mysql) page. The MariaDB dialect adds binlog-based CDC; for cross-instance reactivity on MySQL, add [`@voltro/plugin-broadcast`](../multi-replica). See the CDC section below.

## Enabling CDC

How you enable change-data-capture depends on the dialect:

- **Postgres** — CDC is `LISTEN/NOTIFY`, **on by default** (`CDC=1`), no extension or flag. The only requirement is that the framework's listener runs over a **session-mode connection**: point `DB_URL` (or at least the listener) at the **direct RDS instance/cluster endpoint**, not RDS Proxy in its default transaction-multiplexing mode (which breaks it — see below).
- **MySQL** — inline CDC by default (single-instance reactivity). No binlog path in Voltro's mysql dialect. For cross-instance reactivity add [`@voltro/plugin-broadcast`](../multi-replica) (Redis / NATS) — it fans out app-mutation change events to every replica. (For native, binlog-driven CDC on RDS MySQL-family, use the MariaDB dialect instead.)
- **MariaDB** — CDC is **binlog-based (ROW format)**, giving cross-instance change events. RDS MariaDB needs a binlog-friendly parameter group: `binlog_format=ROW`, `binlog_row_image=FULL`, `gtid_strict_mode=ON`, a replication user granted `REPLICATION SLAVE, REPLICATION CLIENT`, and a unique `server_id` per instance. Note that some managed tiers restrict direct binlog access — confirm your RDS configuration exposes it before relying on cross-instance reactivity. See the [MariaDB dialect](../dialects/mariadb) page.

### RDS Proxy + the LISTEN/NOTIFY gotcha (Postgres)

RDS Proxy multiplexes connections, which is great for connection-churn-heavy workloads — but its default behavior reuses backend connections across clients (transaction-level multiplexing). On Postgres that **breaks `LISTEN/NOTIFY`**: the framework's dedicated listener registers on a connection the proxy may hand to someone else, so change events never arrive.

- **App queries through RDS Proxy** → fine, as long as you don't depend on the listener on that connection.
- **CDC listener** → must use a **direct RDS endpoint** (the instance/cluster endpoint, not the proxy) so the `LISTEN` connection is pinned. RDS Proxy "pinning" can also keep a session sticky, but the simplest robust setup is: direct endpoint for the framework process.
- **Serverless (Lambda)** → a per-request lifecycle is the wrong shape for a push-reactive Voltro process. Prefer a long-running ECS/EC2 service for the API.

## SSL

RDS requires TLS for most parameter groups. Append `sslmode=require` (Postgres) / `ssl-mode=REQUIRED` (MySQL). For full chain verification, download the AWS RDS CA bundle and point the driver at it via `sslmode=verify-full` + the cert path; `require` (encrypt, don't verify hostname) is the common baseline.

## Read replicas

RDS/Aurora Postgres read replicas plug straight into Voltro's replica routing — set `DB_REPLICA_URLS` to the reader endpoints. Aurora's single reader endpoint load-balances for you; with RDS you list each replica. Add `DB_REPLICA_REGIONS` + `AWS_REGION` for cross-region locality. See [read replicas](../read-replicas).

## Verify

```
[voltro:dev] sql dialect resolved: postgres — CDC: LISTEN/NOTIFY, RETURNING: native
[voltro:dev] read replicas: 0 configured (DB_REPLICA_URLS empty) — all queries → primary
[voltro:dev] workflow engine: cluster-sql, dialect=postgres
```

Confirm with `voltro logs --tail 50`.

## See also

- [Postgres dialect](../dialects/postgres) · [MySQL dialect](../dialects/mysql) · [Read replicas](../read-replicas) · [Multi-replica reactivity](../multi-replica)



---

<!-- source: en/database/providers/digitalocean.md -->
## Voltro + DigitalOcean Managed Databases

_Run Voltro on DigitalOcean Managed Postgres or MySQL — the connection pool gotcha for LISTEN/NOTIFY, sslmode=require, trusted sources, and connection strings._

DigitalOcean Managed Databases offer **Postgres** and **MySQL**, mapping to Voltro's `postgres` and `mysql` dialects. Postgres gives you the full reactive surface — `LISTEN/NOTIFY` CDC, advisory-lock workflows, read replicas, `JSONB` — with one pooling caveat.

## Connect

DigitalOcean gives you a direct connection (port 25060) and, if you create one, a **connection pool** (a separate host/port).

```sh
# Direct connection — session-mode, supports LISTEN/NOTIFY.
DB_DIALECT=postgres
DB_URL=postgresql://[USER]:[PASSWORD]@[CLUSTER]-do-user-[ID].[REGION].db.ondigitalocean.com:25060/[DB]?sslmode=require
```

```sh
# MySQL cluster
DB_DIALECT=mysql
DB_URL=mysql://[USER]:[PASSWORD]@[CLUSTER]-do-user-[ID].[REGION].db.ondigitalocean.com:25060/[DB]?ssl-mode=REQUIRED
```

`sslmode=require` (Postgres) / `ssl-mode=REQUIRED` (MySQL) is mandatory — DO clusters reject unencrypted connections.

## Enabling CDC

On Postgres, Voltro's change-data-capture is `LISTEN/NOTIFY`, **on by default** (`CDC=1`) — no extension, no flag. The one requirement on DigitalOcean is to point `DB_URL` (or at least the CDC listener) at the **direct connection (port 25060)**, which is session-mode — *not* the built-in connection pool in its default `transaction` mode, which breaks `LISTEN/NOTIFY` (below). On the MySQL cluster, CDC is inline-only / single-instance — nothing to enable, no cross-instance fan-out.

## Connection pool gotcha (Postgres)

DigitalOcean's built-in connection pool can run in `transaction`, `session`, or `statement` mode. The default `transaction` mode **breaks `LISTEN/NOTIFY`** — same mechanism as every other transaction-mode pooler: the framework's listener registers on a connection that gets recycled.

- **Need reactivity** → connect the framework to the **direct connection (25060)**, or create the pool in **`session` mode** and use that.
- **High connection churn** → query bulk app queries through a `transaction`-mode pool and keep the single CDC listener on a direct connection.

## Trusted sources / firewall

DO clusters default to a firewall. Add your app's droplet / k8s cluster / app-platform component to the cluster's **trusted sources**, or connections time out before SSL even negotiates.

## Verify

```
[voltro:dev] sql dialect resolved: postgres — CDC: LISTEN/NOTIFY, RETURNING: native
[voltro:dev] read replicas: 0 configured (DB_REPLICA_URLS empty) — all queries → primary
[voltro:dev] workflow engine: cluster-sql, dialect=postgres
```

Confirm with `voltro logs --tail 50`. Snapshot-but-no-updates means you're on the transaction-mode pool — move the listener to the direct connection.

## See also

- [Postgres dialect](../dialects/postgres) · [MySQL dialect](../dialects/mysql)



---

<!-- source: en/database/providers/timescale.md -->
## Voltro + Timescale

_Run Voltro on Timescale (TimescaleDB) — it's Postgres plus the time-series extension, so the full reactive surface works; hypertables are an opt-in per table._

Timescale (Timescale Cloud / TimescaleDB) is **Postgres with the TimescaleDB extension** — so it uses Voltro's **postgres** dialect with the complete reactive surface: `LISTEN/NOTIFY` CDC, advisory-lock cluster workflows, read replicas, `JSONB`. Nothing in the framework changes; you simply also have hypertables and continuous aggregates available when you want them.

## Connect

```sh
DB_DIALECT=postgres
DB_URL=postgresql://tsdbadmin:[PASSWORD]@[SERVICE].[PROJECT].tsdb.cloud.timescale.com:[PORT]/tsdb?sslmode=require
```

`sslmode=require` is mandatory on Timescale Cloud.

## Hypertables are opt-in

Voltro's schema DSL emits ordinary Postgres tables. To turn one into a TimescaleDB hypertable (chunked by time for fast time-series queries), run `create_hypertable(...)` yourself after the table exists — via a custom migration or a `*.seed.ts` that issues the SQL. The framework's `voltro migrate` doesn't generate hypertable DDL; it's a deliberate, per-table decision.

A table you convert to a hypertable still works with the reactive engine and `ctx.store` exactly as before — the conversion is transparent to Voltro's read/write path. Heavy time-series ingestion tables are usually the ones you mark `.nonReactive()` (you don't want a notification firing on every metric insert), so the two concerns rarely collide.

## Enabling CDC

Timescale is real Postgres, so Voltro's change-data-capture is `LISTEN/NOTIFY` and **on by default** (`CDC=1`) — no extension, no flag, and TimescaleDB's presence changes nothing here. Timescale Cloud puts **no transaction pooler in front by default**, so the `DB_URL` you connect with is session-mode and reactivity works immediately. (Every table is reactive by default, so on a hypertable every chunk insert fires a notification — usually not what you want on high-ingest time-series tables. Mark it `.nonReactive()`; see the hypertable note above.)

## Pooling / SSL

- **No transaction pooler by default** — reactivity works directly.
- **SSL**: `sslmode=require` always.

## Verify

```
[voltro:dev] sql dialect resolved: postgres — CDC: LISTEN/NOTIFY, RETURNING: native
[voltro:dev] read replicas: 0 configured (DB_REPLICA_URLS empty) — all queries → primary
[voltro:dev] workflow engine: cluster-sql, dialect=postgres
```

Confirm with `voltro logs --tail 50`.

## See also

- [Postgres dialect](../dialects/postgres) — the full reactive surface Timescale inherits.



---

<!-- source: en/database/providers/cockroachdb.md -->
## Voltro + CockroachDB

_CockroachDB is Postgres-wire-compatible but not 100% feature-compatible — what works, what may not pass Voltro's pg DDL, and how to test before you commit._

CockroachDB speaks the **Postgres wire protocol**, so you set `DB_DIALECT=postgres` and the driver connects. But wire-compatible is **not** feature-compatible — Cockroach implements a subset of Postgres semantics with some deliberate differences. Treat this as **supported with caveats**, not a drop-in like RDS or Supabase.

## Connect

```sh
DB_DIALECT=postgres
DB_URL=postgresql://[USER]:[PASSWORD]@[CLUSTER].[REGION].cockroachlabs.cloud:26257/[DB]?sslmode=verify-full
```

CockroachDB Cloud uses port `26257` and requires TLS (`sslmode=verify-full` with the cluster CA, or `require` as a weaker baseline).

## Reactivity — no native CDC, cross-instance via the broadcast bus

Be blunt up front: **CockroachDB does not support `LISTEN/NOTIFY` at all**, and Voltro's native Postgres CDC is built on `LISTEN/NOTIFY`. So there is **no native cross-instance path** on CockroachDB — you can't "enable" the LISTEN/NOTIFY CDC here, because the mechanism doesn't exist on this backend.

What this means concretely:

- The SQL surface (schema DSL, queries, mutations, `ctx.store`) works against Cockroach's Postgres-wire compatibility, modulo the DDL caveats below.
- **Within a single instance, reactivity works** via inline emit: the writing process pushes its own writes to its subscribers.
- **For cross-instance reactivity, add [`@voltro/plugin-broadcast`](../multi-replica)** (Redis / NATS). The bus fans out each app-mutation change event to every replica — closing the gap LISTEN/NOTIFY can't fill on Cockroach. This is the right path when you want CockroachDB's distributed SQL AND multi-replica reactivity.
- CockroachDB *does* have its own change feed (`CHANGEFEED`), but **Voltro does not consume it** — there is no adapter wiring Cockroach CHANGEFEEDs into the dispatcher. The broadcast bus is the supported cross-instance mechanism.

```ts
// app.config.ts
import { broadcastPlugin } from '@voltro/plugin-broadcast'
export default { type: 'api' as const, name: 'api', plugins: [broadcastPlugin()] }
// BROADCAST_URL=redis://… (or nats://…)
```

Caveat: the bus carries **app-mutation** changes (writes through `ctx.store`), not out-of-band DB writes — see [Multi-replica reactivity](../multi-replica). If you need out-of-band-write reactivity, a stock-Postgres provider ([Supabase](./supabase), [Neon](./neon), [AWS RDS](./aws-rds)) with native `LISTEN/NOTIFY` is the right choice.

## What to verify before committing

Cockroach diverges from stock Postgres in ways that can affect Voltro's emitted DDL and runtime path. Test a full `voltro migrate` + a few subscriptions against a real cluster before you build on it:

- **`LISTEN/NOTIFY` is NOT supported** (covered above) — the single biggest reason Cockroach is "supported with caveats" rather than a drop-in.
- **DDL differences.** Cockroach's `CREATE TABLE` / `CREATE INDEX` / FK handling, sequence behavior (`BIGSERIAL`), and some constraint forms differ. Voltro's migrator targets stock Postgres DDL; some statements may need adjustment or may not apply cleanly.
- **Advisory locks.** Cockroach's advisory-lock support differs from Postgres, which affects the cluster workflow runner's shard-ownership primitive.
- **Transaction semantics.** Cockroach is serializable-by-default with retry-on-contention; the framework's `transactional()` retry filter is tuned for Postgres error codes (`40001` / `40P01`) — Cockroach uses `40001` too, so that part overlaps, but verify under load.

## Recommendation

If you need CockroachDB's distributed/multi-region story specifically, run a spike: `voltro migrate` against a cluster, exercise your mutations + subscriptions, and watch `voltro logs` for DDL or CDC errors. If you primarily want a managed Postgres with Voltro's full reactive surface, a true Postgres provider ([Supabase](./supabase), [Neon](./neon), [AWS RDS](./aws-rds)) is the lower-risk choice.

## Verify

A successful connect logs the postgres dialect — but note CDC:

```
[voltro:dev] sql dialect resolved: postgres — CDC: LISTEN/NOTIFY, RETURNING: native
```

If subscriptions never deliver updates, LISTEN/NOTIFY isn't available on this backend (expected on Cockroach) — run with `CDC=0` for single-process inline emit, add [`@voltro/plugin-broadcast`](../multi-replica) for cross-instance reactivity, or move to a stock-Postgres provider for native `LISTEN/NOTIFY`.

## See also

- [Postgres dialect](../dialects/postgres) — what "full Postgres" actually requires.
- [Multi-replica reactivity](../multi-replica) — cross-instance change fan-out via `@voltro/plugin-broadcast`.



---

<!-- source: en/database/providers/planetscale.md -->
## Voltro + PlanetScale

_Connect Voltro to PlanetScale MySQL — the Vitess foreign-key caveat that affects reference() cascades, the connection string, TLS, and branch workflows._

PlanetScale is **MySQL on Vitess**, so it uses Voltro's **mysql** dialect. It connects fine and the schema DSL, queries, and workflows work — but there's a **critical caveat about foreign keys** you must address before relying on `reference()` cascades or FK auto-indexes.

## The foreign-key caveat (read this first)

**Vitess disables foreign keys by default.** Voltro's `reference()` column emits a real SQL `FOREIGN KEY` constraint, and several framework behaviors hang off that constraint:

- **`onDelete: 'cascade'` / `'restrict'` / `'setNull'`** — enforced by the FK. With FKs disabled, deleting a parent row does NOT cascade or get blocked; orphaned children just stay.
- **FK auto-index** — Voltro creates a B-tree index on every FK column by default. That part (the index) still works, but the referential integrity it pairs with does not.

You have two options:

1. **Enable foreign-key support on the PlanetScale branch** (PlanetScale supports FKs when the keyspace is configured for it — turn it on in the branch settings). Then `reference()` behaves as documented. This is the recommended path.
2. **Accept application-level integrity** — if you can't enable FKs, treat `reference()` as a plain indexed column and enforce cascade/restrict semantics in your mutation handlers yourself. Don't rely on `onDelete:` doing anything.

Decide this up front — a schema built assuming cascades that silently don't fire is a data-integrity bug waiting to surface.

## Connect

```sh
DB_DIALECT=mysql
DB_URL=mysql://[USER]:[PASSWORD]@[HOST].[REGION].psdb.cloud:3306/[DB]?ssl-mode=REQUIRED
```

PlanetScale **requires TLS** — `ssl-mode=REQUIRED`. The `mysql2` driver Voltro uses honors it. PlanetScale's host is region-specific (`*.psdb.cloud`).

## Reactivity — inline by default, cross-instance via the broadcast bus

Voltro's mysql dialect emits change events **inline**: the writing process emits them in-process. There is no binlog consumer in the mysql dialect, and **PlanetScale gives no binlog access anyway** (Vitess hides it behind the proxy). So:

- **Reactivity works within a single instance out of the box.** One Voltro process sees its own writes and pushes them to its subscribers. Nothing to enable — it's the default.
- **Cross-instance reactivity needs a pub/sub bus.** A write on instance A is invisible to subscriptions held by instance B *unless* you add [`@voltro/plugin-broadcast`](../multi-replica) (Redis / NATS). The bus fans out each app-mutation change event to every replica, closing the gap binlog CDC can't reach on Vitess.

```ts
// app.config.ts
import { broadcastPlugin } from '@voltro/plugin-broadcast'
export default { type: 'api' as const, name: 'api', plugins: [broadcastPlugin()] }
// BROADCAST_URL=redis://… (or nats://…)
```

Caveat: the bus carries **app-mutation** changes (writes through `ctx.store`), not out-of-band DB writes — see [Multi-replica reactivity](../multi-replica). For PlanetScale (no binlog), the bus is the only cross-instance path; if you need out-of-band-write reactivity too, postgres is the right backend.

## MySQL dialect behavior

Beyond the FK caveat and the cross-instance-reactivity note above, the standard MySQL-dialect differences apply (the framework handles them for you):

- **No `RETURNING`** — INSERT-then-SELECT under the hood.
- **No foreign keys by default** — see the [foreign-key caveat](#the-foreign-key-caveat-read-this-first) above; this is the one you must address explicitly.
- **`0/1` booleans**, `JSON` column type, backtick identifier quoting.

See the [MySQL dialect](../dialects/mysql) page for the full surface.

## Branching

PlanetScale branches are schema-isolated database copies — a good fit for Voltro preview deploys. Point a preview at a branch's connection string; `voltro dev` / `voltro start` auto-migrates the schema on first boot. Note the FK setting is per-branch — enable it on each branch that needs cascades.

## Verify

```
[voltro:dev] sql dialect resolved: mysql — CDC: inline only (no binlog CDC), RETURNING: INSERT/UPDATE/DELETE then SELECT (no RETURNING)
[voltro:dev] workflow engine: cluster-sql, dialect=mysql, runnerStorage=sql
```

Confirm with `voltro logs --tail 50`. To verify FK behavior, delete a parent row and check whether the configured `onDelete` actually fired — if it didn't, foreign keys are off on the branch.

## See also

- [MySQL dialect](../dialects/mysql) — RETURNING gaps, inline CDC, mysql2 quirks.
- [Multi-replica reactivity](../multi-replica) — cross-instance change fan-out via `@voltro/plugin-broadcast`.
- [Mixins](../mixins) and the `reference()` docs for what cascades the FK is supposed to enforce.



---

<!-- source: en/database/providers/azure-sql.md -->
## Voltro + Azure SQL

_Connect Voltro to Azure SQL Database — the mssql dialect, encrypted connection string, OUTPUT INSERTED instead of RETURNING, cross-instance reactivity via @voltro/plugin-broadcast, and firewall rules._

Azure SQL Database is **SQL Server in the cloud**, so it uses Voltro's **mssql** dialect (SQL Server 2019+ surface). The schema DSL, queries, and workflows work; the framework handles the SQL-Server-specific idioms for you (`OUTPUT INSERTED` instead of `RETURNING`, `OFFSET … FETCH NEXT` instead of `LIMIT`, `BIT` booleans, `[bracket]` identifier quoting).

## Connect

Azure SQL connections must be encrypted. Set the dialect and a single `DB_URL` — the mssql layer parses `mssql://` (and `sqlserver://`) connection strings:

```sh
DB_DIALECT=mssql
DB_URL=mssql://[USER]:[PASSWORD]@[SERVER].database.windows.net:1433/[DB]
```

Azure requires the username in `user@server` form, so URL-encode the `@` in the user segment (`%40`) — e.g. `mssql://app%40myserver:pw@myserver.database.windows.net:1433/mydb`. TLS on port `1433` is mandatory; the `@effect/sql-mssql` driver encrypts by default, which matches Azure's requirement.

If a single URL doesn't fit, use the discrete `DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASSWORD` / `DB_DATABASE` env vars instead — these are the dialect-agnostic connection fields the CLI reads. (There are no `MSSQL_*` env vars.)

## Reactivity — inline by default, cross-instance via the broadcast bus

SQL Server has **no `LISTEN/NOTIFY` equivalent**, so the mssql dialect emits change events **inline**: the writing instance emits its own change events in-process (no database-level notify channel). The practical shape:

- **Reactivity works within a single instance out of the box** — one Voltro process pushes its own writes to its subscribers. Nothing to enable; it's the default.
- **Cross-instance reactivity needs a pub/sub bus.** A write on instance A doesn't wake subscriptions on instance B *unless* you add [`@voltro/plugin-broadcast`](../multi-replica) (Redis / NATS). The bus fans out each app-mutation change event to every replica — the cross-instance path mssql lacks natively.

```ts
// app.config.ts
import { broadcastPlugin } from '@voltro/plugin-broadcast'
export default { type: 'api' as const, name: 'api', plugins: [broadcastPlugin()] }
// BROADCAST_URL=redis://… (or nats://…)
```

Caveat: the bus carries **app-mutation** changes (writes through `ctx.store`), not out-of-band DB writes — see [Multi-replica reactivity](../multi-replica). If you need out-of-band-write reactivity too, postgres (`LISTEN/NOTIFY`) is the right dialect.

## Dialect behavior

The MSSQL dialect differs from Postgres in ways the framework abstracts:

- **No `RETURNING`** — the framework uses `OUTPUT INSERTED.*` / `OUTPUT DELETED.*`.
- **No `LIMIT N OFFSET N`** — compiled to `OFFSET N ROWS FETCH NEXT M ROWS ONLY`.
- **JSON columns** stored as `NVARCHAR(MAX)`, returned as strings — the framework auto-parses them.
- **Cluster workflows** use `sp_getapplock` for shard ownership.

See the [MSSQL dialect](../dialects/mssql) page for the complete surface, including the upstream cluster patches.

## Firewall rules

Azure SQL blocks all traffic by default. Add your app's outbound IP (or "Allow Azure services") to the server's **firewall rules**, or connections fail before authentication.

## Verify

```
[voltro:dev] sql dialect resolved: mssql — CDC: inline only, RETURNING: OUTPUT INSERTED/DELETED
[voltro:dev] workflow engine: cluster-sql, dialect=mssql, runnerStorage=sql
```

Confirm with `voltro logs --tail 50`.

## See also

- [MSSQL dialect](../dialects/mssql) — OUTPUT INSERTED, OFFSET/FETCH, cluster patches.
- [Multi-replica reactivity](../multi-replica) — cross-instance change fan-out via `@voltro/plugin-broadcast`.
