@kitr/db 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Platon Podkopov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,13 @@
1
- # Temporary Holding Version
1
+ # @kitr/db
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Database.** Connect your database so every request can use it.
4
+
5
+ ```sh
6
+ pnpm kitr add db
7
+ ```
8
+
9
+ `kitr add` installs the package and wires it into your app: the provider in `kitrrc.ts`, its config file and its `AGENTS.md` section. Import it from `@kitr/db`.
10
+
11
+ Read the guide at https://kitr.dev/packages/db.
12
+
13
+ Part of [kitr](https://kitr.dev), the TypeScript framework where everything is an add-on. MIT licensed.
@@ -0,0 +1,42 @@
1
+ import { KitrApp, KitrProvider } from "@kitr/core";
2
+ /**
3
+ * kitr ships no ORM. `defineDb` binds whatever you use (Drizzle, Prisma, Kysely,
4
+ * a raw pool) as `db` in the container with connect/disconnect in the lifecycle.
5
+ *
6
+ * // config/db.ts
7
+ * export default defineDb({ connect: () => drizzle(process.env.DATABASE_URL!), disconnect: (db) => db.$client.end() })
8
+ */
9
+ export interface DbDefinition<T> {
10
+ /** Opens the connection and returns the client. Runs once during `app.boot()`; a rejection fails boot. */
11
+ connect: (app: KitrApp) => T | Promise<T>;
12
+ /** Closes the client when the server shuts down (SIGINT, SIGTERM or `server.close()`). */
13
+ disconnect?: (db: T, app: KitrApp) => void | Promise<void>;
14
+ /** Runs once the server is ready to serve, after boot. Use it to check for pending migrations or warm a pool. */
15
+ onReady?: (db: T, app: KitrApp) => void | Promise<void>;
16
+ }
17
+ declare module '@kitr/core' {
18
+ interface ConfigRegistry {
19
+ db: DbDefinition<any>;
20
+ }
21
+ interface ContainerBindings {
22
+ db: unknown;
23
+ }
24
+ }
25
+ /**
26
+ * Declares how the app connects to its database, in `config/db.ts`. Returns the definition
27
+ * unchanged; it exists so `disconnect` and `onReady` receive the client type `connect` returns.
28
+ */
29
+ export declare function defineDb<T>(definition: DbDefinition<T>): DbDefinition<T>;
30
+ /** Untyped alias of `defineDb` for the `defineConfig` convention; prefer `defineDb`, which infers the client type. */
31
+ export declare const defineConfig: <const C extends DbDefinition<any>>(config: C) => C;
32
+ /**
33
+ * The provider `kitr add db` puts in `kitrrc.ts`. It binds `db` in the container from `config/db.ts`,
34
+ * connects during boot, runs `onReady` when the server is ready and `disconnect` on shutdown.
35
+ */
36
+ export declare const dbProvider: KitrProvider;
37
+ /**
38
+ * Returns the connected client from an app's container, typed as `T`: `useDb<AppDb>(ctx.app)`.
39
+ * Throws when the app has no `db` binding, or when an async `connect` has not finished (call it after `app.boot()`).
40
+ */
41
+ export declare function useDb<T>(app: KitrApp): T;
42
+ export { dbProvider as default };
package/dist/index.js ADDED
@@ -0,0 +1,37 @@
1
+ import { createDefineConfig, defineProvider } from "@kitr/core";
2
+ // src/index.ts
3
+ /**
4
+ * Declares how the app connects to its database, in `config/db.ts`. Returns the definition
5
+ * unchanged; it exists so `disconnect` and `onReady` receive the client type `connect` returns.
6
+ */
7
+ function defineDb(definition) {
8
+ return definition;
9
+ }
10
+ /** Untyped alias of `defineDb` for the `defineConfig` convention; prefer `defineDb`, which infers the client type. */
11
+ const defineConfig = createDefineConfig();
12
+ /**
13
+ * The provider `kitr add db` puts in `kitrrc.ts`. It binds `db` in the container from `config/db.ts`,
14
+ * connects during boot, runs `onReady` when the server is ready and `disconnect` on shutdown.
15
+ */
16
+ const dbProvider = defineProvider({
17
+ name: "db",
18
+ register(app) {
19
+ const def = app.config.get("db");
20
+ app.container.singleton("db", () => def.connect(app));
21
+ },
22
+ async ready(app) {
23
+ await app.config.get("db").onReady?.(await app.container.make("db"), app);
24
+ },
25
+ async shutdown(app) {
26
+ const def = app.config.get("db");
27
+ if (app.container.has("db")) await def.disconnect?.(await app.container.make("db"), app);
28
+ }
29
+ });
30
+ /**
31
+ * Returns the connected client from an app's container, typed as `T`: `useDb<AppDb>(ctx.app)`.
32
+ * Throws when the app has no `db` binding, or when an async `connect` has not finished (call it after `app.boot()`).
33
+ */
34
+ function useDb(app) {
35
+ return app.container.use("db");
36
+ }
37
+ export { dbProvider, dbProvider as default, defineConfig, defineDb, useDb };
package/dist/kitr.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /** `kitr add db`: bind any ORM as `db` with connect/disconnect in the lifecycle. No ORM shipped. */
2
+ declare const _default: {
3
+ readonly name: "db";
4
+ readonly requires: ["server"];
5
+ readonly add: (ctx: import("@kitr/core/package").AddContext) => void;
6
+ readonly remove: (ctx: import("@kitr/core/package").AddContext) => void;
7
+ readonly vite: {
8
+ readonly serverOnlyConfig: true;
9
+ };
10
+ };
11
+ export { _default as default };
package/dist/kitr.js ADDED
@@ -0,0 +1,19 @@
1
+ import { definePackage } from "@kitr/core/package";
2
+ // src/kitr.ts
3
+ /** `kitr add db`: bind any ORM as `db` with connect/disconnect in the lifecycle. No ORM shipped. */
4
+ var kitr_default = definePackage({
5
+ name: "db",
6
+ requires: ["server"],
7
+ add(ctx) {
8
+ ctx.provider.add("@kitr/db");
9
+ ctx.env.add({ DATABASE_URL: "postgres://localhost:5432/app" }, "Database");
10
+ ctx.config("db", `import { defineDb } from '@kitr/db'\n\n// Example with Drizzle + postgres.js: \`pnpm add drizzle-orm postgres\`\n// import { drizzle } from 'drizzle-orm/postgres-js'\n// import postgres from 'postgres'\n\nexport default defineDb({\n connect: async () => {\n // const client = postgres(process.env.DATABASE_URL!)\n // return drizzle(client)\n return {} as unknown\n },\n // disconnect: (db) => db.$client.end(),\n})\n`);
11
+ ctx.agents.add("db", `- \`config/db.ts\` connects the ORM; controllers and actions read it with \`useDb<AppDb>(ctx.app)\` or \`ctx.app.container.use('db')\`. Migrations are the ORM's own tooling.`);
12
+ },
13
+ remove(ctx) {
14
+ ctx.provider.remove("@kitr/db");
15
+ ctx.agents.remove("db");
16
+ },
17
+ vite: { serverOnlyConfig: true }
18
+ });
19
+ export { kitr_default as default };
package/docs/api.md ADDED
@@ -0,0 +1,69 @@
1
+ ---
2
+ title: API reference
3
+ description: Every export of @kitr/db, with its signature.
4
+ order: 2
5
+ ---
6
+
7
+ # API reference
8
+
9
+ Every export of `@kitr/db`, read from the published type definitions. The [guide](/packages/db/guide) shows them in use.
10
+
11
+ ## @kitr/db
12
+
13
+ ### defineDb
14
+
15
+ ```ts
16
+ function defineDb<T>(definition: DbDefinition<T>): DbDefinition<T>;
17
+ ```
18
+
19
+ Declares how the app connects to its database, in `config/db.ts`. Returns the definition
20
+ unchanged; it exists so `disconnect` and `onReady` receive the client type `connect` returns.
21
+
22
+ ### useDb
23
+
24
+ ```ts
25
+ function useDb<T>(app: KitrApp): T;
26
+ ```
27
+
28
+ Returns the connected client from an app's container, typed as `T`: `useDb<AppDb>(ctx.app)`.
29
+ Throws when the app has no `db` binding, or when an async `connect` has not finished (call it after `app.boot()`).
30
+
31
+ ### dbProvider
32
+
33
+ ```ts
34
+ const dbProvider: KitrProvider;
35
+ ```
36
+
37
+ The provider `kitr add db` puts in `kitrrc.ts`. It binds `db` in the container from `config/db.ts`,
38
+ connects during boot, runs `onReady` when the server is ready and `disconnect` on shutdown.
39
+
40
+ ### defineConfig
41
+
42
+ ```ts
43
+ const defineConfig: <const C extends DbDefinition<any>>(config: C) => C;
44
+ ```
45
+
46
+ Untyped alias of `defineDb` for the `defineConfig` convention; prefer `defineDb`, which infers the client type.
47
+
48
+ ### DbDefinition
49
+
50
+ ```ts
51
+ interface DbDefinition<T> {
52
+ /** Opens the connection and returns the client. Runs once during `app.boot()`; a rejection fails boot. */
53
+ connect: (app: KitrApp) => T | Promise<T>;
54
+ /** Closes the client when the server shuts down (SIGINT, SIGTERM or `server.close()`). */
55
+ disconnect?: (db: T, app: KitrApp) => void | Promise<void>;
56
+ /** Runs once the server is ready to serve, after boot. Use it to check for pending migrations or warm a pool. */
57
+ onReady?: (db: T, app: KitrApp) => void | Promise<void>;
58
+ }
59
+ ```
60
+
61
+ kitr ships no ORM. `defineDb` binds whatever you use (Drizzle, Prisma, Kysely,
62
+ a raw pool) as `db` in the container with connect/disconnect in the lifecycle.
63
+
64
+ ```ts
65
+ // config/db.ts
66
+ export default defineDb({ connect: () => drizzle(process.env.DATABASE_URL!), disconnect: (db) => db.$client.end() })
67
+ ```
68
+
69
+ <!-- GENERATED by scripts/api-docs.mjs from the type definitions. Do not edit. -->
package/docs/index.md ADDED
@@ -0,0 +1,638 @@
1
+ ---
2
+ title: Database
3
+ description: Connect your database so every request can use it.
4
+ order: 1
5
+ ---
6
+
7
+ # Database
8
+
9
+ Almost every app keeps its data in a database. `@kitr/db` opens the connection when your app starts, hands the same client to every controller, action and job, and closes it cleanly when the server stops.
10
+
11
+ > [!SUMMARY]
12
+ > Connect your database so every request can use it. Add it with `kitr add db`; the overview is at [/packages/db](/packages/db).
13
+
14
+ It ships no ORM. Bring [Drizzle](https://orm.drizzle.team), [Prisma](https://www.prisma.io/docs), [Kysely](https://kysely.dev) or a plain [`pg`](https://node-postgres.com) pool, and kitr runs it inside the [app lifecycle](/docs/configuration). It runs on the server only; for browser or device data, use [storage](/packages/storage).
15
+
16
+ The examples use Acme's Postgres database with `orders` and `projects` tables, read through Drizzle.
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ kitr add db
22
+ ```
23
+
24
+ <details>
25
+ <summary>Everything kitr add changes</summary>
26
+
27
+ What `kitr add` does:
28
+
29
+ - Adds the [server](/packages/server) package first when missing.
30
+ - Installs `@kitr/db`.
31
+ - Adds `'@kitr/db'` to `packages` and `() => import('@kitr/db')` to `providers` in `kitrrc.ts`.
32
+ - Appends `DATABASE_URL=postgres://localhost:5432/app` under a `# Database` comment to `.env.example`, and to `.env` when it exists.
33
+ - Writes `config/db.ts` (shown under Configure). An existing file is left alone.
34
+ - Regenerates `config/index.ts`, the config list the standalone server imports.
35
+ - Adds a `## kitr: db` section to `AGENTS.md`: "`config/db.ts` connects the ORM; controllers and actions read it with `useDb<AppDb>(ctx.app)` or `ctx.app.container.use('db')`. Migrations are the ORM's own tooling."
36
+
37
+ `kitr add db` asks no questions. Pass `--dry-run` to preview or `--no-install` to skip the package manager. The `server` kit includes db.
38
+
39
+ `kitr remove db` removes the provider, the `packages` entry, the dependency, the `AGENTS.md` section and `config/db.ts`. It leaves `DATABASE_URL`, your ORM packages and schema files.
40
+
41
+ </details>
42
+
43
+ ## Configure
44
+
45
+ `kitr add db` writes this file. `connect` returns an empty object until you fill in your ORM.
46
+
47
+ ```ts
48
+ // config/db.ts
49
+ import { defineDb } from '@kitr/db'
50
+
51
+ // Example with Drizzle + postgres.js: `pnpm add drizzle-orm postgres`
52
+ // import { drizzle } from 'drizzle-orm/postgres-js'
53
+ // import postgres from 'postgres'
54
+
55
+ export default defineDb({
56
+ connect: async () => {
57
+ // const client = postgres(process.env.DATABASE_URL!)
58
+ // return drizzle(client)
59
+ return {} as unknown
60
+ },
61
+ // disconnect: (db) => db.$client.end(),
62
+ })
63
+ ```
64
+
65
+ `defineDb` takes three functions, each receiving the app.
66
+
67
+ | Option | Default | Description |
68
+ | --- | --- | --- |
69
+ | `connect(app)` | required | Opens the connection and returns the client. Runs once, during `app.boot()`. If it throws or rejects, boot fails with that error. |
70
+ | `disconnect(db, app)` | none | Closes the client. Runs when the server shuts down: on `SIGINT`, `SIGTERM` or `server.close()`. |
71
+ | `onReady(db, app)` | none | Runs once after boot, when the server starts serving. Use it to check for pending migrations. |
72
+
73
+ | Variable | Used for |
74
+ | --- | --- |
75
+ | `DATABASE_URL` | The connection string your `connect` reads. `kitr add` writes `postgres://localhost:5432/app` as the example value. |
76
+
77
+ Prefer `defineDb` to `defineConfig`: it infers the client type from `connect`, so `disconnect` and `onReady` receive a typed `db`.
78
+
79
+ ## Connect with Drizzle
80
+
81
+ Install Drizzle and the postgres.js driver:
82
+
83
+ ```sh
84
+ pnpm add drizzle-orm postgres
85
+ pnpm add -D drizzle-kit
86
+ ```
87
+
88
+ Describe the tables in plain Drizzle.
89
+
90
+ ```ts
91
+ // server/db/schema.ts
92
+ import { integer, pgTable, text, timestamp } from 'drizzle-orm/pg-core'
93
+
94
+ export const projects = pgTable('projects', {
95
+ id: text('id').primaryKey(),
96
+ teamId: text('team_id').notNull(),
97
+ name: text('name').notNull(),
98
+ })
99
+
100
+ export const orders = pgTable('orders', {
101
+ id: text('id').primaryKey(),
102
+ teamId: text('team_id').notNull(),
103
+ customerEmail: text('customer_email').notNull(),
104
+ totalCents: integer('total_cents').notNull(),
105
+ createdAt: timestamp('created_at').defaultNow().notNull(),
106
+ })
107
+ ```
108
+
109
+ Fill in `config/db.ts` and export the client type as `AppDb`.
110
+
111
+ ```ts
112
+ // config/db.ts
113
+ import { drizzle } from 'drizzle-orm/postgres-js'
114
+ import postgres from 'postgres'
115
+ import { defineDb } from '@kitr/db'
116
+ import * as schema from '#server/db/schema'
117
+
118
+ const createDb = () => drizzle(postgres(process.env.DATABASE_URL!), { schema })
119
+
120
+ /** The client every controller and action gets from useDb<AppDb>(). */
121
+ export type AppDb = ReturnType<typeof createDb>
122
+
123
+ export default defineDb({
124
+ connect: () => createDb(),
125
+ disconnect: (db) => db.$client.end(),
126
+ })
127
+ ```
128
+
129
+ Point `DATABASE_URL` in `.env` at a running database:
130
+
131
+ ```sh
132
+ DATABASE_URL=postgres://ada@localhost:5432/acme
133
+ ```
134
+
135
+ Start the server with `kitr dev`. `connect` runs during boot, before the first request. If Postgres is not running, the server does not start and the log shows the driver's error, such as `ECONNREFUSED`.
136
+
137
+ ## Query from a controller
138
+
139
+ In a controller, pass `ctx.app` to `useDb` with your client type:
140
+
141
+ ```ts
142
+ // server/controllers/orders-controller.ts
143
+ import { desc, eq } from 'drizzle-orm'
144
+ import type { HttpContext } from '@kitr/api'
145
+ import { useDb } from '@kitr/db'
146
+ import type { AppDb } from '#config/db'
147
+ import { orders } from '#server/db/schema'
148
+
149
+ export default class OrdersController {
150
+ async index({ app, params }: HttpContext<{ teamId: string }>) {
151
+ const db = useDb<AppDb>(app)
152
+ return db
153
+ .select()
154
+ .from(orders)
155
+ .where(eq(orders.teamId, params.teamId))
156
+ .orderBy(desc(orders.createdAt))
157
+ }
158
+ }
159
+ ```
160
+
161
+ Register the route as usual with [api](/packages/api):
162
+
163
+ ```ts
164
+ // server/routes.ts
165
+ import { defineRoutes } from '@kitr/api'
166
+ import OrdersController from '#server/controllers/orders-controller'
167
+
168
+ export const routes = defineRoutes((router) =>
169
+ router.get('/api/teams/:teamId/orders', [OrdersController, 'index']),
170
+ )
171
+ ```
172
+
173
+ A request returns the rows as JSON:
174
+
175
+ ```sh
176
+ curl http://localhost:3001/api/teams/team_acme/orders
177
+ ```
178
+
179
+ ```json
180
+ [{ "id": "ord_1", "teamId": "team_acme", "customerEmail": "grace@acme.com", "totalCents": 4900, "createdAt": "2026-11-02T09:14:00.000Z" }]
181
+ ```
182
+
183
+ Every request shares the same pooled client, not a new connection.
184
+
185
+ ## Query from an action
186
+
187
+ An [action](/packages/actions) handler has no `ctx`; get the request's app with `useRequestApp` from `@kitr/server`:
188
+
189
+ ```ts
190
+ // app/actions/projects/rename.ts
191
+ import { eq } from 'drizzle-orm'
192
+ import { z } from 'zod'
193
+ import { defineMutation } from '@kitr/actions'
194
+ import { useDb } from '@kitr/db'
195
+ import { useRequestApp } from '@kitr/server'
196
+ import { app } from '#app/app'
197
+ import type { AppDb } from '#config/db'
198
+ import { projects } from '#server/db/schema'
199
+
200
+ export const renameProject = defineMutation('projects.rename', {
201
+ input: z.object({ id: z.string(), name: z.string().min(2) }),
202
+ effects: (project) => ({ invalidate: [['projects', 'list']], set: [{ key: ['projects', 'detail', project.id], data: project }] }),
203
+ handler: async ({ input }) => {
204
+ const db = useDb<AppDb>(useRequestApp(app))
205
+ const [project] = await db
206
+ .update(projects)
207
+ .set({ name: input.name })
208
+ .where(eq(projects.id, input.id))
209
+ .returning()
210
+ return project
211
+ },
212
+ })
213
+ ```
214
+
215
+ > [!WARNING]
216
+ > Don't import `config/db.ts` into a component; import it from server code and actions only.
217
+
218
+ ## Query from a service or job
219
+
220
+ Outside a request, such as a scheduled [job](/packages/jobs), pass the root app to `useDb`:
221
+
222
+ ```ts
223
+ // server/services/revenue.ts
224
+ import { eq, sum } from 'drizzle-orm'
225
+ import type { KitrApp } from '@kitr/kitr/app'
226
+ import { useDb } from '@kitr/db'
227
+ import type { AppDb } from '#config/db'
228
+ import { orders } from '#server/db/schema'
229
+
230
+ /** Total revenue for one team, in cents. */
231
+ export async function teamRevenue(app: KitrApp, teamId: string): Promise<number> {
232
+ const db = useDb<AppDb>(app)
233
+ const [row] = await db
234
+ .select({ total: sum(orders.totalCents) })
235
+ .from(orders)
236
+ .where(eq(orders.teamId, teamId))
237
+ return Number(row?.total ?? 0)
238
+ }
239
+ ```
240
+
241
+ Taking `app` as a parameter keeps the function testable (see Testing).
242
+
243
+ ## Read the client without a type
244
+
245
+ `useDb<T>(app)` is a typed shortcut for `app.container.use('db')`:
246
+
247
+ ```ts
248
+ // server/controllers/health-controller.ts
249
+ import type { HttpContext } from '@kitr/api'
250
+
251
+ export default class HealthController {
252
+ async show({ app }: HttpContext) {
253
+ const db = app.container.use('db') // typed as unknown
254
+ return { ok: db !== undefined }
255
+ }
256
+ }
257
+ ```
258
+
259
+ > [!WARNING]
260
+ > `useDb` only casts. Derive `AppDb` from `connect`, as in the Drizzle example, rather than writing the type by hand.
261
+
262
+ Without the provider in `providers`, both throw `No binding named "db"`.
263
+
264
+ ## Await the client before boot finishes
265
+
266
+ The provider resolves `connect` during `app.boot()`, so `useDb` is synchronous after boot. Code that runs earlier, such as another provider's `register`, must await the binding:
267
+
268
+ ```ts
269
+ // app/providers/project_stats.ts
270
+ import { defineProvider } from '@kitr/kitr/app'
271
+ import type { AppDb } from '#config/db'
272
+ import { projects } from '#server/db/schema'
273
+
274
+ export default defineProvider({
275
+ name: 'projectStats',
276
+ register(app) {
277
+ app.container.singleton('projectStats', async (container) => {
278
+ const db = (await container.make('db')) as AppDb
279
+ return { count: async () => (await db.select().from(projects)).length }
280
+ })
281
+ },
282
+ })
283
+ ```
284
+
285
+
286
+ ## Connect other ORMs
287
+
288
+ Any library works: whatever `connect` returns is the client.
289
+
290
+ ### Prisma
291
+
292
+ ```sh
293
+ pnpm add @prisma/client
294
+ pnpm add -D prisma
295
+ ```
296
+
297
+ ```ts
298
+ // config/db.ts
299
+ import { PrismaClient } from '@prisma/client'
300
+ import { defineDb } from '@kitr/db'
301
+
302
+ const createDb = () => new PrismaClient()
303
+ export type AppDb = ReturnType<typeof createDb>
304
+
305
+ export default defineDb({
306
+ connect: async () => {
307
+ const prisma = createDb()
308
+ await prisma.$connect()
309
+ return prisma
310
+ },
311
+ disconnect: (prisma) => prisma.$disconnect(),
312
+ })
313
+ ```
314
+
315
+ Calling `$connect()` makes a bad URL fail boot instead of the first query. See the [Prisma Client docs](https://www.prisma.io/docs/orm/prisma-client).
316
+
317
+ ### Kysely
318
+
319
+ ```sh
320
+ pnpm add kysely pg
321
+ pnpm add -D @types/pg
322
+ ```
323
+
324
+ ```ts
325
+ // config/db.ts
326
+ import { Kysely, PostgresDialect } from 'kysely'
327
+ import { Pool } from 'pg'
328
+ import { defineDb } from '@kitr/db'
329
+ import type { Database } from '#server/db/types'
330
+
331
+ export type AppDb = Kysely<Database>
332
+
333
+ export default defineDb({
334
+ connect: (): AppDb =>
335
+ new Kysely<Database>({
336
+ dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL }) }),
337
+ }),
338
+ disconnect: (db) => db.destroy(),
339
+ })
340
+ ```
341
+
342
+ `Database` is your table interface, by hand or from `kysely-codegen`. See the [Kysely docs](https://kysely.dev/docs/getting-started).
343
+
344
+ ### A plain pg pool
345
+
346
+ ```sh
347
+ pnpm add pg
348
+ pnpm add -D @types/pg
349
+ ```
350
+
351
+ ```ts
352
+ // config/db.ts
353
+ import { Pool } from 'pg'
354
+ import { defineDb } from '@kitr/db'
355
+
356
+ export type AppDb = Pool
357
+
358
+ export default defineDb({
359
+ connect: async () => {
360
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 })
361
+ await pool.query('select 1')
362
+ return pool
363
+ },
364
+ disconnect: (pool) => pool.end(),
365
+ })
366
+ ```
367
+
368
+ The `select 1` checks the URL during boot, because a `Pool` opens no connection until its first query. See the [pooling docs](https://node-postgres.com/features/pooling).
369
+
370
+ ### SQLite with Drizzle
371
+
372
+ ```sh
373
+ pnpm add drizzle-orm better-sqlite3
374
+ pnpm add -D @types/better-sqlite3
375
+ ```
376
+
377
+ ```ts
378
+ // config/db.ts
379
+ import Database from 'better-sqlite3'
380
+ import { drizzle } from 'drizzle-orm/better-sqlite3'
381
+ import { defineDb } from '@kitr/db'
382
+ import * as schema from '#server/db/schema'
383
+
384
+ const createDb = () => drizzle(new Database(process.env.DATABASE_URL ?? 'acme.db'), { schema })
385
+ export type AppDb = ReturnType<typeof createDb>
386
+
387
+ export default defineDb({
388
+ connect: () => createDb(),
389
+ disconnect: (db) => db.$client.close(),
390
+ })
391
+ ```
392
+
393
+ Here `DATABASE_URL` is a file path such as `acme.db`, and tables come from `drizzle-orm/sqlite-core`. See [Drizzle with SQLite](https://orm.drizzle.team/docs/get-started-sqlite).
394
+
395
+ ## Validate DATABASE_URL at startup
396
+
397
+ A missing `process.env.DATABASE_URL!` crashes deep inside the driver. Declare it in the typed env so the app refuses to start with a clear message:
398
+
399
+ ```ts
400
+ // app/env.ts
401
+ import { createEnv, env as e } from '@kitr/kitr/env'
402
+
403
+ export const env = createEnv({
404
+ public: {},
405
+ server: {
406
+ DATABASE_URL: e.url(),
407
+ },
408
+ })
409
+ ```
410
+
411
+ ```ts
412
+ // config/db.ts
413
+ import { drizzle } from 'drizzle-orm/postgres-js'
414
+ import postgres from 'postgres'
415
+ import { defineDb } from '@kitr/db'
416
+ import { env } from '#app/env'
417
+ import * as schema from '#server/db/schema'
418
+
419
+ const createDb = () => drizzle(postgres(env.DATABASE_URL), { schema })
420
+ export type AppDb = ReturnType<typeof createDb>
421
+
422
+ export default defineDb({
423
+ connect: () => createDb(),
424
+ disconnect: (db) => db.$client.end(),
425
+ })
426
+ ```
427
+
428
+
429
+ ## Change settings per environment
430
+
431
+ Read `app.isProduction` or `app.environment` in `connect` to size the pool or turn on TLS only where it matters:
432
+
433
+ ```ts
434
+ // config/db.ts
435
+ import { drizzle } from 'drizzle-orm/postgres-js'
436
+ import postgres from 'postgres'
437
+ import { defineDb } from '@kitr/db'
438
+ import * as schema from '#server/db/schema'
439
+
440
+ const createDb = (production: boolean) =>
441
+ drizzle(
442
+ postgres(process.env.DATABASE_URL!, {
443
+ max: production ? 20 : 5,
444
+ ssl: production ? 'require' : false,
445
+ }),
446
+ { schema },
447
+ )
448
+ export type AppDb = ReturnType<typeof createDb>
449
+
450
+ export default defineDb({
451
+ connect: (app) => createDb(app.isProduction),
452
+ disconnect: (db) => db.$client.end(),
453
+ })
454
+ ```
455
+
456
+
457
+ ## Check migrations when the server starts
458
+
459
+ `onReady` runs once after boot, when the server starts serving. Use it to warn about, or refuse to run with, an outdated schema:
460
+
461
+ ```ts
462
+ // config/db.ts
463
+ import { sql } from 'drizzle-orm'
464
+ import { drizzle } from 'drizzle-orm/postgres-js'
465
+ import postgres from 'postgres'
466
+ import { defineDb } from '@kitr/db'
467
+ import * as schema from '#server/db/schema'
468
+
469
+ const createDb = () => drizzle(postgres(process.env.DATABASE_URL!), { schema })
470
+ export type AppDb = ReturnType<typeof createDb>
471
+
472
+ const EXPECTED_MIGRATIONS = 12
473
+
474
+ export default defineDb({
475
+ connect: () => createDb(),
476
+ disconnect: (db) => db.$client.end(),
477
+ onReady: async (db, app) => {
478
+ const rows = await db.execute(sql`select count(*)::int as count from drizzle.__drizzle_migrations`)
479
+ const applied = Number(rows[0]?.count ?? 0)
480
+ if (applied < EXPECTED_MIGRATIONS) {
481
+ if (app.isProduction) throw new Error(`Database is ${EXPECTED_MIGRATIONS - applied} migrations behind`)
482
+ app.container.use('logger').warn(`Run pnpm db:migrate: ${EXPECTED_MIGRATIONS - applied} pending`)
483
+ }
484
+ },
485
+ })
486
+ ```
487
+
488
+ `onReady` does not run under `createTestApp`. The logger comes from [logger](/packages/logger).
489
+
490
+ ## Run migrations
491
+
492
+ Your ORM's migration tools work unchanged. Add scripts to `package.json`:
493
+
494
+ ```json
495
+ {
496
+ "scripts": {
497
+ "db:generate": "drizzle-kit generate",
498
+ "db:migrate": "drizzle-kit migrate",
499
+ "db:studio": "drizzle-kit studio"
500
+ }
501
+ }
502
+ ```
503
+
504
+ ```ts
505
+ // drizzle.config.ts
506
+ import { defineConfig } from 'drizzle-kit'
507
+
508
+ export default defineConfig({
509
+ schema: './server/db/schema.ts',
510
+ out: './server/db/migrations',
511
+ dialect: 'postgresql',
512
+ dbCredentials: { url: process.env.DATABASE_URL! },
513
+ })
514
+ ```
515
+
516
+ Run `pnpm db:migrate` in your deploy step, before the new server starts. See [Drizzle migrations](https://orm.drizzle.team/docs/migrations).
517
+
518
+ > [!WARNING]
519
+ > Don't run migrations from `connect` or `onReady`: two instances migrating at once can leave the database half-migrated. Run them once, from the deploy pipeline.
520
+
521
+ ## Use a transaction
522
+
523
+ Transactions belong to your ORM; call its API on the client from `useDb`:
524
+
525
+ ```ts
526
+ // server/services/checkout.ts
527
+ import { eq } from 'drizzle-orm'
528
+ import type { KitrApp } from '@kitr/kitr/app'
529
+ import { useDb } from '@kitr/db'
530
+ import type { AppDb } from '#config/db'
531
+ import { orders, projects } from '#server/db/schema'
532
+
533
+ export async function placeOrder(app: KitrApp, input: { id: string; teamId: string; projectId: string }) {
534
+ const db = useDb<AppDb>(app)
535
+ return db.transaction(async (tx) => {
536
+ const [project] = await tx.select().from(projects).where(eq(projects.id, input.projectId))
537
+ if (!project) throw new Error(`Project ${input.projectId} not found`)
538
+ const [order] = await tx
539
+ .insert(orders)
540
+ .values({ id: input.id, teamId: input.teamId, customerEmail: 'linus@acme.com', totalCents: 4_900 })
541
+ .returning()
542
+ return order
543
+ })
544
+ }
545
+ ```
546
+
547
+
548
+ ## Close the connection on shutdown
549
+
550
+ On `SIGINT` or `SIGTERM` the server stops accepting requests, then calls your `disconnect`, so open queries finish before the pool closes:
551
+
552
+ ```ts
553
+ // server/main.ts
554
+ import { mountRoutes } from '@kitr/api'
555
+ import { createServer } from '@kitr/server'
556
+ import { config } from '../config'
557
+ import rc from '../kitrrc'
558
+ import { routes } from './routes'
559
+
560
+ export const server = createServer({ providers: rc.providers, config })
561
+ mountRoutes(server, routes)
562
+
563
+ await server.listen({ port: 3001 })
564
+ // Ctrl-C logs "SIGINT received, shutting down", then calls disconnect(db, app).
565
+ ```
566
+
567
+ Without `disconnect`, the database sees connections drop instead of closing.
568
+
569
+ ## Errors
570
+
571
+ Driver errors (`ECONNREFUSED`, authentication failures) pass through unchanged. These come from a wrong database binding:
572
+
573
+ | Error | When | What to do |
574
+ | --- | --- | --- |
575
+ | `[kitr] Missing config "db". Create config/db.ts or pass it to createApp().` | The provider is registered but there is no `config/db.ts`, or a test passed no `db` config. | Run `kitr add db` again, or pass `config: { db: defineDb(...) }`. |
576
+ | `[kitr] No binding named "db". Did you add the package that provides it?` | `useDb` on an app without the db provider. | Add `() => import('@kitr/db')` to `providers` in `kitrrc.ts`, or `providers: [dbProvider]` in a test. |
577
+ | `[kitr] Binding "db" has an async factory; await container.make('db') before use('db').` | `useDb` before `app.boot()` finished. | Await `app.boot()` first, or use `await app.container.make('db')`. |
578
+ | The error `connect` threw | `connect` failed, for example the database is down. | Boot rejects with it and the server does not start. Fix the URL or start the database. |
579
+
580
+ ## Testing
581
+
582
+ Build the app with `createTestApp` from [testing](/packages/testing) and give `config` a `db` whose `connect` returns a fake. Code under test calls `useDb` unchanged. This tests `teamRevenue` from "Query from a service or job":
583
+
584
+ ```ts
585
+ // server/services/revenue.test.ts
586
+ import { dbProvider, defineDb } from '@kitr/db'
587
+ import { createTestApp } from '@kitr/testing'
588
+ import { describe, expect, it, vi } from 'vitest'
589
+ import type { AppDb } from '#config/db'
590
+ import { teamRevenue } from '#server/services/revenue'
591
+
592
+ describe('teamRevenue', () => {
593
+ it('sums the team orders and closes the client on shutdown', async () => {
594
+ const where = vi.fn(async () => [{ total: '16900' }])
595
+ const fakeDb = { select: () => ({ from: () => ({ where }) }) } as unknown as AppDb
596
+ const disconnect = vi.fn()
597
+
598
+ const app = await createTestApp({
599
+ providers: [dbProvider],
600
+ config: { db: defineDb({ connect: () => fakeDb, disconnect }) },
601
+ })
602
+
603
+ expect(await teamRevenue(app, 'team_acme')).toBe(16_900)
604
+ expect(where).toHaveBeenCalledOnce()
605
+
606
+ await app.shutdown()
607
+ expect(disconnect).toHaveBeenCalledWith(fakeDb, app)
608
+ })
609
+ })
610
+ ```
611
+
612
+ `createTestApp` runs `connect` but not `onReady`; call `await app.ready()` for that. To replace the client in an app that has booted, bind a value over it:
613
+
614
+ ```ts
615
+ // test/orders.test.ts
616
+ import { dbProvider, defineDb, useDb } from '@kitr/db'
617
+ import { createTestApp } from '@kitr/testing'
618
+ import { expect, it } from 'vitest'
619
+
620
+ it('uses the stub bound after boot', async () => {
621
+ const app = await createTestApp({
622
+ providers: [dbProvider],
623
+ config: { db: defineDb({ connect: () => ({ name: 'real' }) }) },
624
+ })
625
+ app.container.value('db', { name: 'stub' })
626
+ expect(useDb<{ name: string }>(app)).toEqual({ name: 'stub' })
627
+ })
628
+ ```
629
+
630
+ Bind after boot, not before: the provider's `register` replaces anything bound under `db`.
631
+
632
+ For real queries, point `DATABASE_URL` at a throwaway database in `.env.test`, or use [PGlite](https://pglite.dev), which runs Postgres in-process through `drizzle-orm/pglite`. To test a controller end to end, call it with `createTestClient(server)` from [testing](/packages/testing).
633
+
634
+ ## Next steps
635
+
636
+ - [API reference](/packages/db/api): `defineDb`, `dbProvider`, `useDb`.
637
+ - [API routes](/packages/api): routes and controllers that read the database.
638
+ - [Actions](/packages/actions): server functions your components call.
@@ -0,0 +1,89 @@
1
+ ---
2
+ title: Overview
3
+ description: Connect your database so every request can use it.
4
+ order: 0
5
+ ---
6
+
7
+ # Database
8
+
9
+ ## What it is
10
+
11
+ `@kitr/db` connects your database to the app lifecycle. Describe the connection once with `defineDb`; the provider opens it at boot, hands it to `useDb()` everywhere on the server and closes it on shutdown.
12
+
13
+ It ships no ORM and runs on the server only. For data on the device, use [storage](/packages/storage).
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ kitr add db
19
+ ```
20
+
21
+ Adds `@kitr/db`, its provider, `config/db.ts` and `DATABASE_URL` in `.env.example`. Bring your own ORM. [Every change](/packages/db/guide#install).
22
+
23
+ ## Quick start
24
+
25
+ With Drizzle and postgres.js installed (`pnpm add drizzle-orm postgres`), describe the connection once:
26
+
27
+ ```ts
28
+ // config/db.ts
29
+ import { drizzle } from 'drizzle-orm/postgres-js'
30
+ import postgres from 'postgres'
31
+ import { defineDb } from '@kitr/db'
32
+ import * as schema from '#server/db/schema'
33
+
34
+ const createDb = () => drizzle(postgres(process.env.DATABASE_URL!), { schema })
35
+
36
+ /** The client every controller and action gets from useDb<AppDb>(). */
37
+ export type AppDb = ReturnType<typeof createDb>
38
+
39
+ export default defineDb({
40
+ connect: () => createDb(),
41
+ disconnect: (db) => db.$client.end(),
42
+ })
43
+ ```
44
+
45
+ Then read it anywhere on the server:
46
+
47
+ ```ts
48
+ // server/controllers/orders_controller.ts
49
+ import type { HttpContext } from '@kitr/api'
50
+ import { useDb } from '@kitr/db'
51
+ import type { AppDb } from '#config/db'
52
+
53
+ export default class OrdersController {
54
+ async index({ app }: HttpContext) {
55
+ return useDb<AppDb>(app).query.orders.findMany()
56
+ }
57
+ }
58
+ ```
59
+
60
+ Point `DATABASE_URL` at a running database and run `kitr dev`. The provider connects at boot and refuses to start if the database is unreachable. Next in the guide: [Connect with Drizzle](/packages/db/guide#connect-with-drizzle).
61
+
62
+ ## Built on
63
+
64
+ `@kitr/db` is built on the kitr [server](/packages/server) lifecycle and container. The database client itself is whichever you bring:
65
+
66
+ | ORM or driver | Docs |
67
+ | --- | --- |
68
+ | Drizzle (the guide's examples) | [orm.drizzle.team](https://orm.drizzle.team/docs/overview) |
69
+ | Prisma | [prisma.io/docs](https://www.prisma.io/docs) |
70
+ | Kysely | [kysely.dev](https://kysely.dev) |
71
+ | node-postgres | [node-postgres.com](https://node-postgres.com) |
72
+
73
+ kitr adds the typed definition, startup validation of `DATABASE_URL`, the migration check, `useDb()` and clean shutdown.
74
+
75
+ ## Problems it solves
76
+
77
+ - **A client created in every file.** [Connect with Drizzle](/packages/db/guide#connect-with-drizzle).
78
+ - **Database code in controllers, actions and jobs looking different.** [Query from a controller](/packages/db/guide#query-from-a-controller).
79
+ - **A bad connection string found on the first request.** [Validate DATABASE_URL at startup](/packages/db/guide#validate-database_url-at-startup).
80
+ - **Deploying code ahead of its migrations.** [Check migrations when the server starts](/packages/db/guide#check-migrations-when-the-server-starts).
81
+ - **Connections left open on shutdown.** [Close the connection on shutdown](/packages/db/guide#close-the-connection-on-shutdown).
82
+
83
+ ## Further reading
84
+
85
+ - [Guide](/packages/db/guide).
86
+ - [API reference](/packages/db/api).
87
+ - [Drizzle ORM docs](https://orm.drizzle.team/docs/overview).
88
+ - [API routes](/packages/api) and [Actions](/packages/actions).
89
+ - [Server](/packages/server).
package/package.json CHANGED
@@ -1,6 +1,61 @@
1
1
  {
2
2
  "name": "@kitr/db",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.3.0",
4
+ "description": "@kitr/db: bind any ORM (Drizzle, Prisma, Kysely) as ctx.db with lifecycle hooks. No ORM shipped.",
5
+ "kitr": {
6
+ "name": "db",
7
+ "title": "Database",
8
+ "summary": "Connect your database so every request can use it.",
9
+ "category": "backend",
10
+ "runsOn": "server",
11
+ "requires": [
12
+ "server"
13
+ ],
14
+ "docs": "docs",
15
+ "setup": "./kitr"
16
+ },
17
+ "type": "module",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/kitrjs/kitr.git",
22
+ "directory": "packages/db"
23
+ },
24
+ "homepage": "https://kitr.dev/packages/db",
25
+ "bugs": "https://github.com/kitrjs/kitr/issues",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "sideEffects": false,
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "./kitr": {
36
+ "types": "./dist/kitr.d.ts",
37
+ "default": "./dist/kitr.js"
38
+ },
39
+ "./package.json": "./package.json"
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "docs"
44
+ ],
45
+ "dependencies": {
46
+ "@kitr/core": "0.3.0"
47
+ },
48
+ "peerDependencies": {},
49
+ "devDependencies": {
50
+ "tsdown": "^0.23.7",
51
+ "typescript": "^5.9.3"
52
+ },
53
+ "keywords": [
54
+ "kitr",
55
+ "kitr-package"
56
+ ],
57
+ "scripts": {
58
+ "build": "tsdown",
59
+ "typecheck": "tsc -p tsconfig.json"
60
+ }
6
61
  }