--- description: Prisma schema and database access conventions globs: prisma/**,src/app/api/**/*.ts,src/lib/**/*.ts,scripts/**/*.ts --- # Prisma Conventions ## Client import Always import the singleton: ```ts import { prisma } from "@/lib/db"; ``` `src/lib/db.ts` exposes a lazy `Proxy` over `PrismaClient` — the underlying client (and Postgres pool) is only constructed on first property access. Never `new PrismaClient()` outside of `src/lib/db.ts`. Don't call `prisma` from top-level module code; the lazy proxy depends on `DATABASE_URL` being set at access time, not import time. ## Generated client path The Prisma client is generated to `src/generated/prisma/` (custom `output` in `prisma/schema.prisma`), not `@prisma/client`. To import generated types or enums: ```ts import { Prisma, PrismaClient } from "@/generated/prisma/client"; ``` `src/generated/` is a no-go zone — regenerate via `npx prisma generate` instead of editing. ## Schema patterns (what this repo actually does) - **IDs:** `String @id @default(cuid())`. - **Timestamps:** `createdAt DateTime @default(now())` + `updatedAt DateTime @updatedAt`. - **Multi-tenancy:** every business model has `organizationId String` + `@@index([organizationId])`. Auth tables (`User`, `Account`, `Session`) and the `Organization` itself are the exceptions. - **JSON columns:** `Json?` is used for `ResponseCard.fieldData`, `ResponseCard.rawOcrResponse`, `Integration.config`, etc. Treat the shape as untrusted on read. - **Indexes:** `@@index` on FKs and the columns we sort/filter by. Add an index when you add a `where:` filter. - **No `@@map`:** Prisma defaults to PascalCase tables here; do not add `@@map(...)` to existing models. ## Multi-tenant scoping (mandatory) Every read or write against a business model MUST scope by `organizationId`: ```ts // Read const items = await prisma.responseCard.findMany({ where: { organizationId: session.user.orgId, /* ... */ }, }); // Create await prisma.responseCard.create({ data: { organizationId: session.user.orgId, /* ... */ }, }); ``` Cross-org leakage is a security bug. Returning a `404` (not `403`) on accidental cross-org IDs is the convention so we don't reveal existence. ## After schema changes ```bash npx prisma generate # regenerate client at src/generated/prisma/ npx prisma migrate dev --name descriptive_name # create + apply a migration locally npm run schema:map # refresh docs/SCHEMA_MAP.md (see prisma-schema-map.mdc) ``` For prototype-stage edits (no DB structure change you intend to keep), `npm run db:push` is also available. ## Query best practices - `findUnique` for ID / unique lookups; `findFirst` when filtering by org. - Use `select` when you only need a few fields and the model has heavy relations. - Wrap multi-step writes in `prisma.$transaction([...])` or `prisma.$transaction(async (tx) => ...)`. - Background jobs go through `ProcessingJob` — see `src/lib/ocr.ts` for the canonical pattern. ## Denormalized fields `ResponseCard.name` is a denormalized display string derived from `firstName + lastName`. Three places set it: OCR pipeline (`src/lib/ocr.ts`), the public survey submit handler (`src/app/api/survey/submit/route.ts`), and the PUT route (`src/app/api/cards/[id]/route.ts`, which recomputes on any first/last change). When you write a new path that mutates `firstName` or `lastName`, recompute `name` too — or call into the PUT path so it does it for you.