--- 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)