Base de données
Prisma 7 avec isolation multi-tenant automatique sur Supabase PostgreSQL.
Configuration
HeartCo utilise Prisma 7 comme ORM avec deux URLs de connexion :
# Connexion poolée (via pgBouncer) — pour les requêtes de l'app
DATABASE_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT].supabase.co:6543/postgres?pgbouncer=true"
# Connexion directe — pour les migrations uniquement
DIRECT_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT].supabase.co:5432/postgres"Client Prisma
Le client est généré dans generated/prisma/ (pas dans node_modules) :
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
output = "../generated/prisma"
binaryTargets = ["native", "rhel-openssl-3.0.x"] // Vercel + local
}Commandes
# Générer le client après modification du schéma
npx prisma generate
# Créer une migration
npx prisma migrate dev --name nom-descriptif
# Valider le schéma (sans migrer)
npx prisma validate
# Interface graphique de la base
npx prisma studio
# Réinitialiser la base (ATTENTION : perte de données)
npx prisma migrate resetMulti-tenant : le coeur de l'architecture
Principe
Chaque organisation a ses propres données, isolées des autres. Tous les modèles multi-tenant ont un champ organizationId qui sert de clé de partition logique.
ctx.orgDb vs ctx.db
HeartCo fournit deux clients Prisma dans le contexte tRPC :
| Client | Usage | Scope |
|---|---|---|
ctx.orgDb | Lectures scopées à l'organisation | Auto-filtre organizationId sur findMany, findFirst, findFirstOrThrow |
ctx.db | Écritures et accès cross-org | Pas de filtre automatique — webhooks, admin, crons |
Fichier : src/lib/prisma-org-scope.ts
Comment ça marche
ctx.orgDb utilise Prisma.$extends() pour injecter automatiquement organizationId dans les requêtes de lecture :
// Sous le capot — ce que fait withOrgScope()
const orgDb = prisma.$extends({
query: {
$allModels: {
findMany({ args, query }) {
args.where = { ...args.where, organizationId };
return query(args);
},
findFirst({ args, query }) {
args.where = { ...args.where, organizationId };
return query(args);
},
},
},
});125 modèles scopés
Les modèles suivants sont automatiquement filtrés par ctx.orgDb :
Invoice, Quote, Client, Product, CrmDeal, CrmContact, WorkOrder, WorkReport, Supplier, Team, Membership, Notification, Subscription, ExpenseReport, LeaveRequest, Payslip, TimeEntry, Appointment, Calendar, BankAccount, BankTransaction, EmailCampaign, AutomationRule, AuditLog, et ~100 autres.
La liste complète est dans ORG_SCOPED_MODELS de src/lib/prisma-org-scope.ts.
Règles de sécurité — OBLIGATOIRE
1. Toujours organizationId dans les écritures
// ✅ Correct
await ctx.db.invoice.update({
where: { id: input.id, organizationId: ctx.session.user.organizationId },
data: { status: "PAID" },
});
// ❌ INTERDIT — pas d'organizationId dans le where
await ctx.db.invoice.update({
where: { id: input.id },
data: { status: "PAID" },
});2. findFirst au lieu de findUnique
Quand on filtre par organizationId + id, utiliser findFirst :
// ✅ Correct — findFirst avec organizationId
const invoice = await ctx.orgDb.invoice.findFirst({
where: { id: input.id }, // organizationId injecté automatiquement
});
// ❌ INTERDIT — findUnique ne supporte pas le filtre composite
const invoice = await ctx.db.invoice.findUnique({
where: { id: input.id }, // Pas de filtre org → fuite de données !
});3. Sous-entités (sans organizationId direct)
Pour les modèles qui n'ont pas directement organizationId mais qui appartiennent à un parent scopé :
// ✅ Filtre imbriqué vers le parent
const line = await ctx.db.invoiceLine.findFirst({
where: {
id: input.lineId,
invoice: { organizationId: ctx.session.user.organizationId },
},
});4. Erreurs tRPC (jamais throw new Error)
// ✅ Correct
throw new TRPCError({ code: "NOT_FOUND", message: "Facture introuvable" });
// ❌ INTERDIT
throw new Error("Facture introuvable");Ajouter un nouveau modèle
1. Définir le modèle dans prisma/schema.prisma
model NewModel {
id String @id @default(cuid())
name String
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
}2. Ajouter à ORG_SCOPED_MODELS
// src/lib/prisma-org-scope.ts
export const ORG_SCOPED_MODELS = new Set([
// ... modèles existants
"NewModel",
]);3. Générer et migrer
npx prisma generate
npx prisma migrate dev --name add-new-modelProblèmes courants
EPERM sur Windows
L'erreur EPERM: operation not permitted sur query_engine-windows.dll.node est normale quand le serveur dev tourne (le fichier est verrouillé).
Solution : Arrêter pnpm dev → npx prisma generate → Relancer pnpm dev.
Migration en conflit
Si une migration échoue suite à un conflit :
# Voir l'état des migrations
npx prisma migrate status
# En développement uniquement — reset complet
npx prisma migrate reset
npx prisma migrate devNe JAMAIS modifier manuellement
- Les fichiers dans
prisma/migrations/— ils sont générés par Prisma - Le dossier
generated/prisma/— il est régénéré à chaqueprisma generate