Dyrected
Deployment & OperationsInfrastructureDatabase Adapters

Overview

The database adapter connects self-hosted Dyrected to your database and defines the storage contract.

In a self-hosted Dyrected project, the database adapter is what connects your app to a real database. You pass it as the db key in dyrected.config.ts, and Dyrected uses it for every read and write. In Dyrected Cloud, this layer is managed for you.

Dyrected ships official adapters, and the same adapter contract lets you support other databases.

import { defineConfig } from "@dyrected/core";
import { postgresAdapter } from "@dyrected/db-postgres";

export default defineConfig({
  db: postgresAdapter({ url: process.env.DATABASE_URL! }),
  collections: [],
  globals: [],
});

Choosing an adapter

Most of the choice comes down to relational versus document storage. The relational adapters store your documents in tables and are the common production choice; MongoDB stores them as native documents. All of them expose the same Dyrected features, so this is mainly a fit-and-hosting decision.

Pick the adapter that matches your deployment target:

  • Postgres for most relational deployments, and the recommended production default.
  • SQLite for local development and small single-server apps.
  • MongoDB for document-oriented storage.
  • MySQL for MySQL deployments; it behaves like the Postgres adapter.

Each adapter page covers connection setup and behavior specific to that database. This page describes the contract every adapter implements.

What an adapter does

An adapter is responsible for translating Dyrected's document operations — find, create, update, delete, and paginated queries — into the underlying database. Because they all implement the same contract, you can switch databases without changing your collections, fields, or application code.

How your data is stored

You mostly don't need to think about the physical layout, but a quick mental model explains a lot of the behavior on the other pages in this section.

On the relational adapters (Postgres, SQLite, MySQL), each collection is a table named collection_<slug>. A document is stored as one JSON column, data, alongside id, created_at, and updated_at. Because the body is JSON, you can filter and sort on any field without declaring it first. Fields you mark promoted are also lifted into their own real columns for faster queries. Globals live in a small internal table.

On MongoDB, documents are stored natively, so there are no tables to create and no columns to promote.

Behaviors worth knowing

These pages cover the parts of database behavior that come up as your project grows:

  • Migrations — how schema changes are applied, and how to rename fields safely.
  • Transactions — grouping writes so they all commit or all roll back.
  • Indexes — promoting the fields you query for speed, and how uniqueness fits in.

DatabaseAdapter

The interface every database adapter must implement.

Dyrected ships adapters for PostgreSQL, MySQL, SQLite, and MongoDB. Implement this interface to connect any other database.

export interface DatabaseAdapter {
  /** Find a paginated list of documents in a collection. */
  find(args: {
    collection: string;
    where?: Record<string, unknown>;
    limit?: number;
    page?: number;
    sort?: string;
    /**
     * The collection's field definitions. Optional, so simple adapters can
     * ignore it; SQL adapters use it to sort numeric fields by magnitude.
     */
    fields?: Field[];
  }): Promise<PaginatedResult>;

  /** Find a single document by its ID. Returns `null` if not found. */
  findOne(args: { collection: string; id: string }): Promise<BaseDocument | null>;

  /** Insert a new document and return it with its generated `id`. */
  create(args: { collection: string; data: Record<string, unknown> }): Promise<BaseDocument>;

  /** Update a document by ID and return the updated document. */
  update(args: { collection: string; id: string; data: Record<string, unknown> }): Promise<BaseDocument>;

  /** Delete a document by ID. Return value is intentionally untyped — callers do not use it. */
  delete(args: { collection: string; id: string }): Promise<unknown>;

  /**
   * Compute aggregate statistics across a collection without returning documents.
   *
   * Each named key in `args.aggregates` maps to a `count`, `sum`, `avg`, `min`,
   * or `max` operation, with optional `where` filtering and `cast` conversion.
   * The result is a flat object of the same named keys mapped to `number | null`.
   */
  aggregate(args: AggregateArgs): Promise<AggregateResult>;

  /** Fetch the singleton document for a global. Returns an empty object if not yet initialised. */
  getGlobal(args: { slug: string }): Promise<Record<string, unknown>>;

  /** Create or replace the singleton document for a global. */
  updateGlobal(args: { slug: string; data: Record<string, unknown> }): Promise<Record<string, unknown>>;

  /**
   * Sync the database schema with the current collection and global configs.
   * Called on startup to create tables/collections that don't exist yet.
   * Not all adapters implement this (e.g. MongoDB is schema-less).
   */
  sync?(collections: CollectionConfig[], globals: GlobalConfig[]): Promise<void>;

  /**
   * Execute a raw SQL query or database command.
   * Optional — not all adapters support raw access.
   */
  execute?(query: string, params?: unknown[]): Promise<unknown>;

  /**
   * Run all adapter operations in `callback` as one atomic transaction.
   * Shipped adapters implement this; workflow transitions require it.
   */
  transaction?<T>(callback: (db: DatabaseAdapter) => Promise<T>): Promise<T>;

  /**
   * Close any open connection pools, sockets, or background timers.
   * Called during server teardown or build lifecycle cleanup.
   */
  disconnect?(): Promise<void>;
}
OptionDescription
find (optional)Find a paginated list of documents in a collection.
findOne (required)Find a single document by its ID. Returns `null` if not found.
create (required)Insert a new document and return it with its generated `id`.
update (required)Update a document by ID and return the updated document.
delete (required)Delete a document by ID. Return value is intentionally untyped — callers do not use it.
aggregate (required)Compute aggregate statistics across a collection without returning documents. Each named key in `args.aggregates` maps to a `count`, `sum`, `avg`, `min`, or `max` operation, with optional `where` filtering and `cast` conversion. The result is a flat object of the same named keys mapped to `number | null`.
getGlobal (required)Fetch the singleton document for a global. Returns an empty object if not yet initialised.
updateGlobal (required)Create or replace the singleton document for a global.
sync (required)Sync the database schema with the current collection and global configs. Called on startup to create tables/collections that don't exist yet. Not all adapters implement this (e.g. MongoDB is schema-less).
execute (optional)Execute a raw SQL query or database command. Optional — not all adapters support raw access.
transaction (required)Run all adapter operations in `callback` as one atomic transaction. Shipped adapters implement this; workflow transitions require it.
disconnect (required)Close any open connection pools, sockets, or background timers. Called during server teardown or build lifecycle cleanup.

PaginatedResult

The envelope returned by collection list endpoints (GET /api/collections/:slug).

export interface PaginatedResult<T = Record<string, any>> {
  /** The documents on the current page. */
  docs: T[];
  /** Total number of documents matching the query (across all pages). */
  total: number;
  /** Maximum number of documents per page as requested. */
  limit: number;
  /** The current page number (1-indexed). */
  page: number;
  /** Total number of pages given the current `limit`. */
  totalPages: number;
  /** Whether a next page exists. */
  hasNextPage: boolean;
  /** Whether a previous page exists. */
  hasPrevPage: boolean;
}
OptionDescription
docs (required)The documents on the current page.
total (required)Total number of documents matching the query (across all pages).
limit (required)Maximum number of documents per page as requested.
page (required)The current page number (1-indexed).
totalPages (required)Total number of pages given the current `limit`.
hasNextPage (required)Whether a next page exists.
hasPrevPage (required)Whether a previous page exists.

ReadonlyDatabaseAdapter

Exported type from @dyrected/core.

export type ReadonlyDatabaseAdapter = Pick<DatabaseAdapter, "find" | "findOne" | "getGlobal" | "aggregate">;

On this page

Dyrected| Cloud

Get your backend ready in minutes

Use a managed database, storage, APIs, and admin dashboard without setting up the infrastructure yourself.

Set Up My Backend