---
title: Database
description: Prisma against Postgres, with tenant scoping built in.
type: reference
related:
- /en/docs/apps/studio
- /en/docs/packages/authentication
---
# Database
`@kreogen/database` is [Prisma](https://prisma.io) talking to Postgres through
`@prisma/adapter-pg` — the standard node-postgres driver. That works against
any Postgres: the `docker-compose` service, a managed instance, or one running
beside the app.
There is no serverless database provider involved, and no vendor driver to
replace when the project moves. If you do want [Neon's](https://neon.tech)
serverless driver, swap the adapter for `@prisma/adapter-neon` and set
`neonConfig.webSocketConstructor`; nothing else changes.
The package imports `server-only`. It is the only package in the repository
with a `main` field, so `@kreogen/database` resolves to `index.ts` rather than
to a subpath — and it must never be reachable from a client component.
## Usage
```tsx title="page.tsx"
import { database } from '@kreogen/database';
const Page = async () => {
const users = await database.user.findMany();
};
```
That client is unscoped. For anything belonging to an organization, use the
scoped one instead — see [tenant scoping](#tenant-scoping) below.
## Connection pooling
Next runs one pool per server instance, and the driver's default of ten
connections is easy to multiply past Postgres' `max_connections` once you scale
replicas. The pool is therefore stated rather than inherited:
| Setting | Value |
| ------------------------- | ------------------------------- |
| `max` | `DATABASE_POOL_MAX`, default 10 |
| `idleTimeoutMillis` | 30,000 |
| `connectionTimeoutMillis` | 10,000 |
`DATABASE_POOL_MAX` is read straight from the environment rather than through
`keys()`, because it is an operational dial rather than a feature switch.
Lower it further behind PgBouncer, where the app's pool is in front of a pool.
The client is constructed lazily and cached on `globalThis` outside production.
Creating the adapter eagerly on every module evaluation leaked a connection
pool per hot reload in development.
## Schema
`packages/database/prisma/schema.prisma`. Nine models ship: `User`, `Session`,
`Account`, `Verification`, `Organization`, `Member` and `Invitation` belong to
[Better Auth](/en/docs/packages/authentication), plus two of the template's
own:
* **`Page`** — a stub. Delete it and add your own. It carries an
`organizationId` on purpose, because that is the shape every model you add
should have.
* **`WebhookEvent`** — the idempotency ledger for inbound webhooks. Its primary
key is the provider's event id, so recording an event before handling it
makes a redelivery a no-op.
Seven of those nine are generated from the auth configuration. Running
`bun run auth:generate` rewrites `schema.prisma` and does **not** restore
hand-written directives — the `@unique` on `stripeCustomerId` is the one that
has been lost before. Review the diff in full every time.
### Adding a model
```prisma title="packages/database/prisma/schema.prisma"
model Post {
id String @id @default(cuid())
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
title String
content Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
@@map("post")
}
```
Then add it to `TENANT_SCOPED_MODELS` in `packages/database/index.ts`. Forget
that and `forOrganization` silently does nothing for the new model — which is
why a test derives the list from the schema and fails when the two drift.
## Migrations
A baseline migration is committed at
`packages/database/prisma/migrations/20260815000000_init`. A fresh clone
therefore gets a working database from `migrate deploy` alone, with no
`db push` step and no schema that exists only in someone's local Postgres.
| Command | Does |
| ------------------------ | ----------------------------------------------------------- |
| `bun run migrate` | Format, regenerate the client, create and apply a migration |
| `bun run migrate:deploy` | Apply existing migrations only. Safe in CI and production |
| `bun run db:push` | Push the schema with no migration file. Prototyping only |
`db:push` maintains no history and can drop columns without asking. It is for
the hour before you know what the model should be, not for anything with data
in it.
CI proves the two stay in step: the `migrations` job applies the committed SQL
to an empty Postgres, then runs `prisma migrate diff --exit-code`, which fails
if `schema.prisma` was edited without a migration to match.
## Seeding
```sh
bun run db:seed
```
Two organizations — Northwind and Initech — each with an owner, a member, a
pending invitation and a few rows of the stub model. It is idempotent, so
re-running against a populated database is safe.
The second organization is the point. A single one proves nothing about
scoping; two mean the tenancy example has rows that must never appear in the
other's views.
Seeded users have no credentials. Better Auth owns password hashing, and
reproducing its parameters here would couple the seed to its internals — sign
up through the UI for an account you can actually log in with.
## Tenant scoping
A query that forgets `where: { organizationId }` returns another tenant's rows,
and nothing in Prisma will stop it. `forOrganization` returns a client that
cannot make that mistake:
```ts
import { forOrganization } from '@kreogen/database';
const db = forOrganization(orgId);
await db.page.findMany(); // only this organization's rows
await db.page.create({ data: { name } }); // organizationId forced in
```
Usually you will not call it directly —
[`requireOrganization()`](/en/docs/packages/authentication) hands you the
scoped client already:
```ts
const { db, orgId } = await requireOrganization();
```
Returning the scoped client rather than a bare id is deliberate. An id that has
to be threaded into every `where` clause by hand is one somebody eventually
forgets.
### What it does
Every operation against a model in `TENANT_SCOPED_MODELS` gets
`organizationId` forced into its filter, and creates get it forced into their
data — overriding whatever the caller passed, so a request body cannot choose
its own tenant.
`findUnique` and `findUniqueOrThrow` accept only unique fields in `where`, so
adding `organizationId` to them is a validation error. They are rewritten to
`findFirst` and `findFirstOrThrow`, which keeps the same semantics — one row or
null — while allowing the filter.
Passing a falsy id throws rather than proceeding. A blank value would scope
every query to nothing, or, once a filter is dropped, to everything.
### What it does not do
`$queryRaw` and `$executeRaw` bypass Prisma extensions entirely and are your
responsibility. This is a safety net, not a licence to stop thinking.
Nor does it help with the auth tables. `User`, `Organization` and friends are
deliberately unscoped — the unscoped `database` export exists for them and for
admin work.
## Editing data
[Prisma Studio](/en/docs/apps/studio) is a visual editor for whatever is in the
database:
```sh
bun dev --filter studio
```
---
For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)
For an index of all available documentation, see [/llms.txt](/llms.txt)