Aller au contenu principal

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 reset

Multi-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 :

ClientUsageScope
ctx.orgDbLectures scopées à l'organisationAuto-filtre organizationId sur findMany, findFirst, findFirstOrThrow
ctx.dbÉcritures et accès cross-orgPas 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-model

Problè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 devnpx 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 dev

Ne 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é à chaque prisma generate