Dyrecteddyrected
Adapters

Database Adapters

Choose, configure, migrate, and extend Dyrected database adapters.

Dyrected is database-agnostic. Choose the adapter that matches your deployment and pass it to defineConfig as db. Application code talks to the shared DatabaseAdapter contract, so database-specific concerns stay at the infrastructure boundary.

Dyrected Cloud manages the database. Cloud projects normally omit db and synchronize their configuration through the Cloud workflow. See Cloud Deployment.

Supported adapters

import { PostgresAdapter } from '@dyrected/db-postgres'

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

The current public adapter configuration accepts the connection url. Tune external poolers and hosting limits according to your provider; do not copy undocumented constructor options from older examples.

SQLite — @dyrected/db-sqlite

SQLite is convenient for local development and persistent single-server deployments.

import { SqliteAdapter } from '@dyrected/db-sqlite'

export default defineConfig({
  db: new SqliteAdapter({ filename: './dyrected.db' }),
  collections: [],
  globals: [],
})

Do not place a local SQLite file on an ephemeral serverless filesystem. Use a durable database service for horizontally scaled or short-lived runtimes.

MongoDB — @dyrected/db-mongodb

import { MongoAdapter } from '@dyrected/db-mongodb'

export default defineConfig({
  db: new MongoAdapter({
    url: process.env.MONGODB_URI!,
    dbName: process.env.MONGODB_DATABASE!,
  }),
  collections: [],
  globals: [],
})

MongoDB is schema-flexible, but content-contract migrations still matter: renamed fields, defaults, access rules, and application expectations must remain compatible with existing documents.

MySQL — @dyrected/db-mysql

import { MysqlAdapter } from '@dyrected/db-mysql'

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

The adapter also accepts individual connection properties. Prefer an environment-provided URL when your hosting provider rotates credentials or manages connection details.

Schema synchronization

Relational adapters create missing collection tables and may add promoted columns. Schema synchronization is designed to preserve document data, but it is not equivalent to “no schema changes”: promoting a field can issue ALTER TABLE statements.

Before a production schema change:

  1. Back up the database according to the provider's recovery procedure.
  2. Review new unique constraints and promoted-field types against existing values.
  3. Run synchronization in a staging environment with production-like data.
  4. Deploy code that can read both old and new shapes during a rolling migration.

MongoDB does not need relational column creation, but application-level migration rules still apply.

Field renames and promotion

Use renameTo as the old storage key while the current name is the new key:

{
  name: 'fullName',
  type: 'text',
  label: 'Full name',
  renameTo: 'name',
  defaultValue: '',
}

Keep the fallback until production documents have been migrated and verified. Removing it early can make old values appear missing.

Use promoted: true for fields that require efficient filtering, sorting, or relational constraints—not as a default for every field:

{
  name: 'slug',
  type: 'text',
  label: 'Slug',
  unique: true,
  promoted: true,
}

Writing a custom adapter

Implement the generated contract below and import its types from @dyrected/core. Workflow transitions rely on atomic transactions; an adapter intended to support workflows should implement transaction correctly rather than emulating it with independent writes.

Use parameterized queries, validate identifiers derived from collection slugs, and return the shared pagination envelope. Contract tests live in @dyrected/adapter-contract-tests and should be run against custom implementations.

Generated database contracts

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;
  }): 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>;

  /** 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>;
}
MemberSignatureDescription
findfind(args: { collection: string; where?: Record<string, unknown>; limit?: number; page?: number; sort?: string; }): Promise<PaginatedResult>Find a paginated list of documents in a collection.
findOnefindOne(args: { collection: string; id: string }): Promise<BaseDocument | null>Find a single document by its ID. Returns `null` if not found.
createcreate(args: { collection: string; data: Record<string, unknown> }): Promise<BaseDocument>Insert a new document and return it with its generated `id`.
updateupdate(args: { collection: string; id: string; data: Record<string, unknown> }): Promise<BaseDocument>Update a document by ID and return the updated document.
deletedelete(args: { collection: string; id: string }): Promise<unknown>Delete a document by ID. Return value is intentionally untyped — callers do not use it.
getGlobalgetGlobal(args: { slug: string }): Promise<Record<string, unknown>>Fetch the singleton document for a global. Returns an empty object if not yet initialised.
updateGlobalupdateGlobal(args: { slug: string; data: Record<string, unknown> }): Promise<Record<string, unknown>>Create or replace the singleton document for a global.
syncsync(collections: CollectionConfig[], globals: GlobalConfig[]): Promise<void>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).
executeexecute(query: string, params?: unknown[]): Promise<unknown>Execute a raw SQL query or database command. Optional — not all adapters support raw access.
transactiontransaction<T>(callback: (db: DatabaseAdapter) => Promise<T>): Promise<T>Run all adapter operations in `callback` as one atomic transaction. Shipped adapters implement this; workflow transitions require it.

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;
}
MemberSignatureDescription
docsdocs: T[]The documents on the current page.
totaltotal: numberTotal number of documents matching the query (across all pages).
limitlimit: numberMaximum number of documents per page as requested.
pagepage: numberThe current page number (1-indexed).
totalPagestotalPages: numberTotal number of pages given the current `limit`.
hasNextPagehasNextPage: booleanWhether a next page exists.
hasPrevPagehasPrevPage: booleanWhether a previous page exists.

ReadonlyDatabaseAdapter

Exported type from @dyrected/core.

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

On this page