ArchitectureBase de donnees et schema sync

Base de donnees et schema sync

Deplace depuis la racine CLAUDE.md / AGENTS.md par platform-ops/3048. src/test/deploy-schema-path.test.ts lit la section « Schema sync en deploy » de ce fichier : c'est elle qui decrit le pipeline reel.

Deplace depuis la racine CLAUDE.md / AGENTS.md par platform-ops/3048. src/test/deploy-schema-path.test.ts lit la section « Schema sync en deploy » de ce fichier : c'est elle qui decrit le pipeline reel.

Base de donnees (Prisma + Neon Postgres)

Source de verite unique pour toute l'application. Schema dans prisma/schema.prisma. Convention : nom du modele = nom de la table, sans prefixe @@map — avec treize exceptions heritees, toutes prefixees bst_ (bst_roadmap_item, bst_kpi_snapshot, bst_status_*, …). La convention vaut pour les nouveaux modeles ; elle ne decrit pas l'etat du schema.

Le tableau ci-dessous est un ECHANTILLON commente, pas un index : le schema declare beaucoup plus de modeles que les lignes listees ici, et seul prisma/schema.prisma fait autorite. Sont notamment absents, sans que leur absence signifie quoi que ce soit : NextAuth (User, Account, Session, VerificationToken), l'OAuth du serveur MCP (OAuthClient, OAuthAuthorizationCode, OAuthAccessToken), CronExecution, OrganizationInvitation, Workflow, Task, la memoire (OrgFact, StoreFact, UserFact, MemoryEvent), le studio (StudioAsset, CreativeConcept, …), l'ingestion commerce, les vitals et le scan. Avant de creer un modele, lire le schema, jamais ce tableau.

TableRole
OrganizationOrganisations
OrganizationMemberMembres RBAC (owner / admin / member / viewer)
StoreStores (par organisation)
StoreAnnotation / StoreAnnotationReplyNotes d'equipe epinglees a un element d'une page de la vitrine (cle pageKeyOf), statut open / resolved, fil de reponses. Builder et routes /api/stores/[storeId]/annotations (ai-platform/3074)
SubscriptionFacturation Stripe
CreditSolde + journal d'usage (append-only ledger)
StripeEventIdempotency webhook Stripe (par event.id)
MonthlyResetIdempotency cron monthly (par orgId, year, month)
DailyBonusIdempotency daily bonus (par orgId, day — per-org)
AffiliateCode / AffiliateRedemptionCodes de parrainage + journal
AffiliateCommissionLedger de commission de parrainage — 30% du net d'une facture payee, 12 factures, idempotent sur stripeInvoiceId, reversible sur remboursement ou litige (cf. docs/business-model/affiliate.md)
Conversation / MessageSessions chat multi-canal
Connector / IntegrationConnectionIntegrations OAuth + Shopify Custom App
DevStorePoolPool de dev stores Shopify pre-provisionnes (wizard turnkey) — claim atomique + machine a etats du transfert d'ownership (cf. docs/architecture/store-provisioning.md)
AuditLogJournal de securite + billing
AdminAuditLogActions admin globales (marketplace, plan, user ban)
MarketplaceListing / VerifiedRevenue / Offer / DealThread / DealMessage / Order / Review / SavedListingMarketplace V1 (cf. docs/architecture/marketplace.md)
StripeConnectAccountMarketplace V4 — Stripe Connect Express onboarding vendor (payout vendeur en escrow : versé après la fenêtre de dispute 14j via le cron marketplace-payouts, plus au checkout). Une listing SUBSCRIPTION produit un Order par mois payé (Order.stripeInvoiceId, unique), donc chaque mois a sa propre fenêtre de dispute et son propre payout — avant marketplace/0460 seul le premier mois atteignait le vendeur
DisputeMarketplace V4 — buyer dispute (refund request, NOT_DELIVERED, ASSET_TRANSFER_FAILED, …) — mediated by admin
ListingEventMarketplace V4 — timeline horodatée (view, save, offer, deal, …) pour ML recos v2 + analytics
KycVerificationMarketplace V5.1 — enhanced KYC pipeline (Stripe Identity / Persona / Onfido / Manual) avec PII chiffrée at-rest. Garde une porte, depuis marketplace/0441 : la signature de l'APA par l'ACHETEUR sur un deal > $10k (les deux chemins qui écrivent apaSignedAt, code kyc-required). Depuis marketplace/0603 la condition est aussi lue sur la TRANSITION DILIGENCE → APA_SIGNED (advanceDealStage), quel que soit qui a signé : apaSignedAt est une colonne partagée et le seul bouton « signer l'APA » du produit est celui du vendeur, donc le gater lui seul laissait le vendeur avancer le deal à la place de l'acheteur. Le vendeur n'est pas gaté sur l'acte de signer — Stripe Connect le vérifie déjà. Cf. docs/decisions/0011-la-porte-kyc-du-marketplace.md
LegalSignatureMarketplace V5.1 — audit trail NDA/LOI/APA signatures sur DealThread (DocuSign / HelloSign / BOOSTECOM internal)
IntelligenceApiKeyTokens API Intelligence emis par BoostEcom (bei_…, hash SHA-256, lecture seule) — PAS du BYOK provider (le BYOK LLM a ete retire au passage AI Gateway)
StatusIncidentIncidents declares depuis /admin/platform/status, surfacés sur /status
StatusCheckDayRollup quotidien worst-of des sondes (cron status-snapshot, 30 min)
StatusSubscriptionInscriptions email /status (double opt-in, token unsubscribe)
StatusWebhookWebhooks sortants admin → HMAC-SHA256 sur les évènements incidents
GrowthUnitGrowth — une verite verifiee, distribuable (these, faits, sources, mecanisme, impact, action). Les rendus selectionnent dedans, ils n'y ajoutent jamais rien. Composantes du score stockees, total derive a la lecture
ContentRenderGrowth — un rendu d'une unite sur un canal (email, article, linkedin, x, instagram, facebook). Publie exige une URL publique observee + preuve
AttributionEventGrowth — attribution observee uniquement (inscription confirmee, envoi reel). Pas de type impression : rien ici ne sait en observer une
StoreSignalIndexIntelligence — index inversé (domain, kind, value) du Store Graph / Clone Intelligence (relie les stores par tracking ids / theme+apps / créatives partagés). Voir docs/architecture/intelligence-pipeline.md §21
StoreMetricDailyIntelligence — rollup time-series quotidien par store (créatives, spend, review velocity, visites, followers, discount depth…). Substrat d'entraînement du Prediction Engine. Voir docs/architecture/intelligence-prediction-roadmap.md
PredictionAccuracyIntelligence — ledger de backtesting (precision/recall/f1/mape par modèle × horizon) prouvant l'edge prédictif. Cron prediction-backtest hebdo
MarketClusterIntelligence — agrégat par niche (store count, revenu moyen, momentum, emerging score). Omniscience marché + détection de niches émergentes. Cron market-aggregate

Schema sync en deploy (schema guard auto-genere)

Le drift schema ↔ DB est gere par un schema guard genere automatiquement depuis prisma/schema.prisma, AUCUN step manuel a maintenir (l'ancien systeme PENDING_STEPS + canaries a la main a cause l'incident Subscription.canceledAt du 10 juin 2026 : colonne ajoutee au schema sans step, /api/me 500, organisations/stores invisibles pour tous les users).

Pipeline :

  1. scripts/generate-schema-guard.mjs (pnpm db:guard) derive le catalogue complet + DDL idempotent (tables, colonnes, enums, valeurs d'enum, index, FKs) via prisma migrate diff --from-empty (aucune connexion DB) → src/services/database/schema-guard.generated.ts. Regenere au postinstall, a chaque build Vercel, et verifie par le hook pre-push (pnpm db:guard:check).
  2. scripts/vercel-build.mjs : regenere le guard (build FAIL si guard irregenerable ET stale), puis prisma db push seulement si une URL de base est presente dans l'environnement de BUILD, et sur ce projet elle ne l'est pas. L'integration Vercel + Neon n'expose DATABASE_URL qu'au runtime des fonctions, par conception ; le build journalise alors DATABASE_URL not in BUILD env … skipping prisma db push et enchaine sur next build. Quand elle EST presente (dev, autre hebergeur, variable posee a la main), le push tourne sans --accept-data-loss : un deploy ne peut JAMAIS supprimer de donnees, delta destructif = action operateur deliberee via pnpm db:deploy.
  3. Cold-start (src/instrumentation-node.ts → ensureSchemaGuard) : diff generique du catalogue attendu vs information_schema/pg_enum/ pg_indexes/pg_constraint (4 requetes read-only) et application des seuls steps manquants. Couvre automatiquement tout changement de schema. En pratique c'est LE mecanisme, pas un filet : vu le point 2, c'est le premier cold-start apres un deploy qui applique reellement le delta. Verifie sur le deploiement dpl_vP1mRnji… du 25 aout 2026 : le push est saute, la ligne de log est explicite. Lire le point 2 comme « le deploy pousse le schema » est l'erreur que ce paragraphe existe pour empecher.
  4. Fallback request-time : /api/me (et tout route handler qui le souhaite) attrape les erreurs P2021/P2022 via isSchemaDriftError → reactiveSchemaHeal → retry. Les relations de /api/me utilisent des select explicites (jamais include: true) pour decoupler le hot path des colonnes futures.
  5. Operateur : GET /api/admin/db/push = rapport de drift (dry-run), POST /api/admin/db/push = heal ({"mode":"full"} pour rejouer tous les steps idempotents).

Ce que le guard ne fait PAS, et qui a donc son propre garde : il est additif, il ne supprime rien et ne signale rien de superflu. Un index entierement servi par un autre ([a] quand [a, b] existe) lui est invisible. pnpm db:indexes (scripts/check-redundant-indexes.mjs) derive ces paires du schema et refuse toute nouvelle, sur une liste d'acceptation qui ne peut que retrecir. La suppression des onze existantes est une action operateur : docs/ops/database-index-maintenance.md.

Deux classes de DDL echappent au guard genere et vivent dans src/services/database/pending-migrations.ts (EXTRA_STEPS) :

  • inexprimable en Prisma — index partiels, casts que prisma db push refuse sans --accept-data-loss ;
  • exprimable en Prisma mais pas par le guard : le catalogue genere sait CREER une table, AJOUTER une colonne, un index, une valeur d'enum, il ne sait jamais ALTERER une colonne existante. Un changement de type ou de nullabilite sur une colonne deja en place n'a donc aucun chemin automatique, et sans step ici il ne serait applique nulle part (item 0039).

Un step porte une cible de detection (index, ou column avec dbType et/ou nullable) : sans elle il n'est applique que si un AUTRE drift est deja en cours de heal, donc il part sur des heals sans rapport et jamais sur la base qui en a besoin.

Une troisieme classe ne va pas dans EXTRA_STEPS : elle est REFUSEE a la generation. Une colonne requise sans DEFAULT ajoutee a un modele existant produit ADD COLUMN ... NOT NULL sans defaut, et Postgres repond 23502 des que la table a une ligne. Le healer echouerait alors sur ce step pour toujours : build vert, db:guard:check vert, puis P2022 en production toutes les trente secondes. C'est exactement la forme de l'incident Subscription.canceledAt du 10 juin 2026.

pnpm db:guard compare donc la liste des colonnes NOT NULL sans defaut a celle du catalogue precedemment commite et refuse toute nouvelle entree, en nommant les trois sorties : un @default, un champ optionnel a resserrer plus tard, ou un EXTRA_STEPS ecrit a la main avec sa justification. La comparaison se fait la et pas dans --check : au moment du --check, le fichier commite contient deja la colonne, donc les deux listes sont la meme liste et rien ne peut se detecter (data-platform/0200). src/services/database/not-null-column-adds.test.ts tient le compte des 757 entrees heritees comme un plafond qui ne peut que baisser.

Pourquoi ce systeme plutot que des migrations Prisma — et ce qu'il coute : docs/decisions/0009-db-push-et-guard-genere-plutot-que-migrations.md. Ce depot n'a pas de prisma/migrations/, et prisma migrate dev contre la base partagee y lit le schema vivant comme un drift : il propose un reset.