API · Backend (MariaDB)

MariaDB-backed Voltro backend — binlog CDC real-time across replicas plus file storage. The dogfooded shape for K8s multi-replica apps.

The notes base wired for MariaDB: store: 'mariadb' turns on the binlog CDC reader so reactive subscriptions fan out across every replica via the database's binary log — no Redis/NATS, the binlog IS the message bus. File storage is on by default. This is the local-dev mirror of the helm MariaDB production shape. Template id: api-backend-mariadb.

Scaffold

voltro create-project acme --api=api-backend-mariadb --baseline=compose-mariadb

Pair it with the compose-mariadb baseline (or helm): MariaDB must run with binlog_format=ROW + binlog_row_image=FULL and a user holding REPLICATION SLAVE, REPLICATION CLIENT, plus MinIO/S3 for storage. The compose-mariadb baseline sets all of this up with one pnpm db:up.

What ships

apps/acme/api/                          # dir named by the app, not the template
├── app.config.ts                       # store:'mariadb' + storagePlugin()
├── .env.example                        # DB / storage / AI / auth / cluster env
├── package.json
├── tsconfig.json
├── README.md
├── database/
│   └── schema.ts                       # actors + tenants + notes (same as api-backend)
├── queries/
│   └── notes.query.ts + .query.server.ts
└── mutations/
    └── notes.create.mutation.ts + .mutation.server.ts

The wiring

// app.config.ts
import { storagePlugin } from '@voltro/plugin-storage'

export default {
  type: 'api' as const,
  name: '{{capProjectName}}{{capAppName}}',
  store: 'mariadb' as const,        // turns on binlog CDC
  plugins: [
    // Blobs in object storage (MinIO locally, S3 in prod); metadata in
    // _voltro_storage_refs. Provider resolved from STORAGE_PROVIDER.
    storagePlugin(),
  ],
}

The shipped app.config.ts also carries commented-out jwtBearerStrategy + apiKeyStrategy blocks under an auth: key — uncomment and configure them to wire your IdP / API keys.

Environment — .env.example

Copy to .env and adjust. The key vars:

# Database (MariaDB + binlog CDC)
DB_DIALECT=mariadb
DB_URL=mysql://app:app@localhost:3307/{{projectNameSnake}}
CDC=1                                   # push-based real-time across replicas (set 0 for inline-only)

# File storage — MinIO locally, S3 in prod
STORAGE_PROVIDER=minio
S3_ENDPOINT=http://localhost:9000
S3_BUCKET={{projectNameSnake}}
S3_ACCESS_KEY_ID=minioadmin
S3_SECRET_ACCESS_KEY=minioadmin
S3_FORCE_PATH_STYLE=1

# Auth — HMAC key for signed sessions (REQUIRED in production)
VOLTRO_SESSION_SECRET=dev-only-change-me

# Cluster (K8s, >1 replica) — advertise a routable host so a workflow
# can resume on another pod. Inject POD_IP via the downward API in prod.
# VOLTRO_WORKFLOW_RUNNER_HOST=

Why MariaDB CDC

Behind a load balancer each live client WebSocket lives on one replica. A mutation runs on one replica; without cross-instance fan-out, other replicas' clients go stale. MariaDB's ROW-format binlog is one of the two dialects (with postgres LISTEN/NOTIFY) that observe writes across instances natively — CDC=1 reads the binlog and injects remote change events into every replica's store. With CDC=0 it falls back to single-process inline emit.

Boot

pnpm install
cp .env.example .env                    # then adjust DB_URL / secrets
# Start MariaDB (binlog ROW) + MinIO — the compose-mariadb baseline does this:
pnpm db:up
pnpm --filter @acme/api dev

voltro dev against MariaDB auto-migrates the schema before any handler boots, and the boot log confirms CDC: binlog CDC (ROW).

Pairs well with

  • The compose-mariadb baseline — local MariaDB + MinIO.
  • The helm baseline — the K8s production target (per-pod server_id + workflow-runner host).

See also