Database overview
Organize durable application data, choose a database service, and keep persistence separate from business rules and public APIs.
A database keeps application records, such as accounts and orders, after a request or process ends. A database service owns how the application connects to that data and exposes data operations to business domains. It is not the business domain itself.
Start with the data your product needs, then choose the database and integration that fit it. The architecture below applies whether your model uses relational tables or documents; the concrete schema, query, and migration APIs belong to the chosen integration.
Pre-setup
For a new database service, use PostgreSQL in local Docker. That walkthrough covers the Compose file, persistent volume, CLI scaffold, credentials, environment validation, and migration. Complete it before adding queries or auth tables.
Start the database and scaffold its service
Follow the Docker setup, then run
bun run relkit add database --orm drizzle --dialect postgresql once in an app with no database
service. The CLI installs the integration and creates the service, schema, and migration config.
Connect defineEnv to defineApp
The CLI merges DATABASE_URL into src/platform/env.ts. Keep that contract imported by
relkit.config.ts. This minimal inline example shows their relationship:
/** Validate the database connection before activating the Drizzle service. */
import { defineApp, defineEnv, env } from "@relkit/app/config";
export default defineApp({
env: defineEnv({ DATABASE_URL: env.secret() }),
});Keep the actual URL in the backend’s untracked .env.local. The Drizzle service opens it
inside client({ env }); no database provider profile belongs in defineApp.
Migrate and verify
Use the migration steps to generate, review,
and apply your schema, then run bun run check, bun run typecheck, and bun run dev.
Existing SQLite/MySQL apps keep their dialect and history; the SQLite tutorial
is an explicit alternative, not a conversion step.
Keep the responsibilities separate
| Responsibility | What belongs here | Typical location |
|---|---|---|
| Database service | Connection configuration, lifecycle, and integration-specific registration | src/services/database/service.ts |
| Data model | Stored record shapes, identities, relationships, and constraints | src/services/database/schema/ in the current Drizzle integration |
| Data operations | Reusable queries and persistence-specific customization | src/services/database/models/ for Drizzle custom methods |
| Business domain | Validation, authorization, and workflows such as registering a member | src/services/users/functions/ or another owning domain |
| Operational tooling | Schema migrations, backfills, backups, and recovery | Application-root tooling, outside business handlers |
| HTTP interface | Which business operations callers can invoke | src/routes/ |
A Users function decides whether someone may join an organization. The database stores the user and membership and enforces the configured constraints. A route exposes that workflow only when the product needs an HTTP endpoint. Keeping these responsibilities separate lets you change a query without moving routes, and evolve storage without duplicating business rules across transports.
The database is a specialized service. It does not replace domain services, and table access alone never grants permission to a caller. See Service organization for where functions, tools, agents, jobs, and other domain members belong.
Choose an integration
Drizzle is the database service integration available today. It supports relational table definitions and adds typed CRUD operations, generated validation schemas, custom models, and transactions.
MongoDB and Prisma integrations are planned, not available APIs in this release. Their guides will describe their own APIs as they are introduced; Drizzle’s method names are not a universal contract for future integrations. Database concepts stay separate from the currently implemented Drizzle workflow.
For the Drizzle path, follow Save and retrieve orders for a complete
SQLite example, then use connection setup for other databases. Other database clients should remain behind your
domain’s own operations until a supported integration is available; they are not automatically
registered by naming a file service.ts.
Follow data through the application
Route, job, tool, or other entry point
→ owning domain's business function
→ database service's data operations
→ database
Schema changes → migration tooling → database
The current Drizzle integration makes its typed client available through context.database.
Business functions do not open a new connection for each call or declare dependencies.database.
This injection and its exact types are documented in the Drizzle service guide.
From an existing app’s root, check service discovery and generated context types after changing registration:
bun run check
bun run typecheck
These commands check application structure and types; they do not provision a database or migrate it.
Find the guide for your task
The following implementation guides use the current Drizzle integration:
Understand what the integration generates:
Connect and configure lifecycle:
Define tables, keys, and relationships:
Reuse and customize generated validation:
Read and change records:
Add a named query:
Change a built-in operation:
Commit related writes together:
Evolve persistent data:
Share the database with authentication:
Verify behavior safely:
Follow the complete application workflow: