Database
Persist executions, steps, and logs to Postgres, SQLite, or MySQL.
Pass a database client straight into restaq({ database }). Restaq detects
the dialect from the client itself and applies its own schema migrations —
there's no separate ORM or config to wire up.
Choose a database
- Postgres — production, multi-instance deployments
- MySQL — production, multi-instance deployments
- SQLite — local development, testing, single-instance deployments
Migrations
Outside production, migrations run automatically the first time your relay handles an event — nothing to run by hand in dev.
In production, run them explicitly before scaling up new instances:
relay migrateThis loads relay.ts and applies whatever schema your dialect needs, using
the same client you already configured — no connection string to repeat. It's
idempotent, so it's safe to run on every deploy from CI.
It creates three tables: restaq_executions, restaq_execution_steps (an
append-only audit trail — a step that fails and later succeeds keeps both
attempts), and restaq_execution_logs (interleaved system and handler logs).
Writing a custom store
database also accepts a plain ExecutionStore, for any persistence layer
Restaq doesn't support natively:
import type { ExecutionStore } from '@restaq/core';
const myStore: ExecutionStore = {
async create(execution) {},
async update(id, patch) {},
async list() {},
async get(id) {},
async findByEventId(eventId) {},
async getStep(executionId, name) {},
async saveStep(step) {},
async listSteps(executionId) {},
async saveLog(log) {},
async listLogs(executionId) {},
};
export const restaq = createRestaq({
database: myStore,
plugins: [
// ...
],
});Three invariants matter:
create()is atomic. Concurrent calls for the sameeventIdmust collapse into one execution — the winner returnstrue, everyone else returnsfalse. This is the dedup guarantee the rest of Restaq relies on.saveStep()never overwrites. Every call appends a new row, so step history stays a full audit trail, retries included.getStep()returns the latest attempt.ctx.step.run()reads the most recent row bycreatedAtto decide whether a step already succeeded.
migrate() is a no-op on a custom store — Restaq has no schema to apply for
a persistence layer it doesn't own.
Need a Kysely dialect Restaq doesn't auto-detect? Pass { dialect } directly
instead of a raw client and Restaq builds the store on top of it (though
without built-in migrations for that dialect).