Restaq
Database

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 migrate

This 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 same eventId must collapse into one execution — the winner returns true, everyone else returns false. 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 by createdAt to 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).

On this page