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