Skip to content
RELKIT
Esc
↑↓navigate↵open⌘Jpreview
On this page

Define a Drizzle service

Install compatible dependencies, configure a Drizzle client and environment, generate context types, and manage connection lifecycle.

Register your database connection and tables with defineDrizzleService in src/services/database/service.ts. Use defineDrizzleService from @relkit/drizzle in src/services/database/service.ts. Do not wrap it in an ordinary defineService, add a database member to another service, or add a database provider profile to relkit.config.ts. The specialized service is the registration.

Pre-setup

For a new database, complete PostgreSQL in local Docker before configuring the service. It supplies the running database, DATABASE_URL contract, and migration steps. The detailed manual example below uses SQLite; keep your existing dialect and migrations when following it, and use the corresponding native driver.

Install compatible packages

The minimal app does not install the optional database integration. Choose one setup path from the application root, then continue with the connection and schema below.

Scaffold the database baseline

bun run relkit add database --orm drizzle --dialect postgresql

This creates src/services/database/service.ts, an items placeholder in src/services/database/schema/index.ts, and drizzle.config.ts. It merges the environment contract and examples, installs the selected packages and declaration repair, and checks the app. Replace the placeholder schema with your tables. The command does not apply migrations.

Use this once when the application has no database service. For an existing service, edit its configuration and preserve its schema and migration history. Use --dialect sqlite for the manual file-backed example below, or --dialect mysql with a running MySQL database.

With --no-install, run bun install and bun run check before continuing. See the database command reference and complete SQLite guide.

Install the compatible owner-specific cohort before creating the files below. RELKIT packages have independent versions; keep existing manifest settings when merging the repair.

From this guide’s application/package root, install only its selected requirements:

bun add @relkit/drizzle@0.8.1 drizzle-orm@1.0.0-rc.5-169397b
bun add --dev create-relkit@0.8.4 drizzle-kit@1.0.0-rc.5-ab785fc

The selected Drizzle version also needs the release’s declaration repair. The development-only create-relkit package supplies its exact asset; a dependency cannot register Bun patches for its consumer.

mkdir -p patches
cp node_modules/create-relkit/dist/patches/drizzle-orm.patch patches/drizzle-orm@1.0.0-rc.5-169397b.patch

Merge this entry into the application’s existing patchedDependencies, preserving other patches. Keep the asset in source control:

{
  "patchedDependencies": {
    "drizzle-orm@1.0.0-rc.5-169397b": "patches/drizzle-orm@1.0.0-rc.5-169397b.patch"
  }
}

Then apply the registered repair before check/typecheck:

bun install

Keep these owner-specific versions and the lockfile together. Runtime SDKs belong in dependencies; build/type tooling belongs in devDependencies. Installing an adapter does not supply its optional SDK peers or enable the feature’s configuration.

Bun includes the SQLite driver used here. The scaffold’s PostgreSQL/MySQL clients use bun:sql; other native Drizzle clients need their own selected driver and a running database. The MySQL model entrypoint also requires its declaration peer. Do not mix older Drizzle APIs with this release candidate without checking types.

Configure environment and client creation

The data-model example uses this environment contract:

import { defineEnv, env } from "@relkit/app/config";

export default defineEnv({ DATABASE_PATH: env.string().default("./data-model.sqlite") });

Merge DATABASE_PATH into your existing defineEnv() and keep that descriptor in defineApp({ env }). The example defaults to ./data-model.sqlite; ensure its parent directory exists and is writable. Use the same path for the app and migration tooling. A test may explicitly use :memory:, which is isolated to a connection and disappears when it closes.

The complete service includes optional models and overrides:

/** Owns lazy SQLite registration and disposal, supplying users-model extensions and an inactive-user lookup override. */
import { Database } from "bun:sqlite";
import { defineDrizzleService } from "@relkit/drizzle";
import { drizzle } from "drizzle-orm/bun-sqlite";
import * as schema from "./schema/index.js";
import usersModel from "./models/users.model.js";

export default defineDrizzleService({
  schema,
  client: ({ env }) => drizzle({ client: new Database(env.DATABASE_PATH) }),
  models: { users: usersModel },
  overrides: {
    users: {
      findOne: async ({ args, base }) => {
        const user = await base(args);
        return user?.active === false ? null : user;
      },
    },
  },
  dispose: (database) => database.$client.close(),
});

schema is the flat map of table exports. client({ env }) returns the actual Drizzle database, or a promise for it. Open the connection inside this factory, not at module top level. Descriptor discovery inspects the schema without activating the factory; the generated server activates it during startup.

There are no table-creation statements to write here. The client factory only opens the connection. Define the model in Drizzle tables, then use Drizzle Kit to generate and apply its migrations before starting the app.

models and overrides are optional; omit both for base CRUD. dispose(database) closes the underlying driver or pool. RELKIT calls it during normal runtime shutdown. A process crash still requires normal database recovery; shutdown hooks are not backup or durability guarantees.

Choose and customize the driver

Dialect What changes Important integration limit
SQLite SQLite tables, Bun SQLite client, file path or memory database The portable transaction helper shares a connection; review its concurrency limits.
PostgreSQL PostgreSQL table definitions and a compatible async Drizzle client/pool Portable transactions delegate to the client’s transaction implementation.
MySQL MySQL tables and a compatible async Drizzle client/pool Returned mutation rows need a recoverable primary/unique key; see queries.

Driver construction remains native Drizzle configuration. Configure TLS, connection limits, query timeouts, logging, and pool disposal there as appropriate for that driver. RELKIT does not add these as service options or translate tables between dialects. The executable guides here verify Bun SQLite; test other drivers against their actual database before deploying.

Use secret environment fields for credentials or a credential-bearing connection URL. Do not log them or hard-code production values. RELKIT_ENV=test does not replace this custom client factory with an in-memory database: set a safe test database value explicitly.

Generate the typed application context

From the app root:

bun run check
bun run typecheck

The compiler emits .relkit/generated/context-registry.d.ts, which supplies context.database and typed application environment fields. Keep the generated declaration included by your TypeScript configuration, as generated projects do. Do not hand-edit it or add a competing global declaration.

If the database or a model method is missing from editor types, check table exports and model registration, rerun the compiler, then confirm the declaration is included. The service has an inferred identity from its domain; an explicit id is optional and must follow the same domain identity rules.

Understand activation ownership

Activation is cached by descriptor object within a process. Repeated activation shares the original client and environment; it is not a per-request or per-tenant connection factory. close() is idempotent, but currently does not evict that cached activation, so activating the same closed descriptor does not reopen it. Use fresh service objects or isolated processes for independent test lifetimes.

The service is not a managed database provisioning request. Arrange durable storage or a database server, backups, credentials, network access, and migrations separately. For Better Auth, keep the auth tables in this same Drizzle schema and follow Integrate Better Auth.

Reference and source