Runbooks & opérationsRepartir sur une base Neon propre

Repartir sur une base Neon propre

Runbook du propriétaire : classer chaque table, mesurer, créer une base neuve, appliquer le schéma, copier ce qui compte, basculer Vercel, contrôler, garder l'archive et revenir en arrière.

Runbook du propriétaire, rédigé le 2026-10-08 sur le schéma de l'app BoostEcom/boostecom.app (main à cee5f0f0, plus la PR data-platform/3120). Il répond au GO du propriétaire du 2026-10-08 (« Oui go pour (a) et (b) »), point (b).

Une seule base Neon, et c'est la production. Chaque action marquée [PROPRIÉTAIRE] est faite par le propriétaire lui-même : aucun agent ne se connecte à la base, ne lance db:push, db:deploy, prisma migrate ni aucune requête SQL. Les requêtes de ce runbook sont fournies pour être collées par le propriétaire.

Jamais --force-reset, jamais --accept-data-loss, jamais prisma migrate dev ni migrate reset. L'ancienne base neondb n'est jamais modifiée : c'est l'archive et le chemin de retour.

En bref

  • Pourquoi. Le propriétaire est le seul utilisateur (zéro client). La base de production porte des tables sans lecteur (dont les quatre tables Growth retirées du schéma par data-platform/3120), des données de l'ancien Studio interne et des journaux sans valeur. Une base neuve, créée depuis prisma/schema.prisma, colle exactement au schéma.
  • Comment. Une base vide boostecom dans la branche Neon main, le schéma appliqué par prisma db push sur cette base vide, une copie ciblée des seules tables qui ont de la valeur, puis la bascule de DATABASE_URL dans Vercel. L'ancienne base neondb n'est jamais modifiée : c'est l'archive et le chemin de retour.
  • Ce que le propriétaire refait à la main : se reconnecter (OTP), recréer son organisation, reconnecter sa boutique Shopify et ses connecteurs, réémettre ses clés. Liste exacte à l'étape 4.8.
  • Ce qui se reconstruit tout seul : schéma (cold start), annonces marketplace de BoostEcom, roadmap projetée depuis le backlog, brouillons de changelog, données Shopify ré-ingérées, agrégats et crons.
  • Ce qui est perdu si on ne le copie pas : voir la colonne de droite du classement (section 3). Les séries temporelles Intelligence ne se reconstruisent pas.
  • Durée : une demi-journée, dont l'essentiel en mesure et vérification. Fenêtre où l'app ne doit pas être utilisée : de l'étape 4.1 à l'étape 4.6.

1. Avant de commencer

[PROPRIÉTAIRE] Cocher, dans l'ordre :

  1. La PR data-platform/3120 (retrait de GrowthUnit, ContentRender, ContentReview, AttributionEvent) est mergée et déployée. Sinon la base neuve recrée ces quatre tables, vides.
  2. Un poste avec psql (client PostgreSQL 16 ou plus), Node et pnpm, et un clone à jour de main : pnpm install passe, puis pnpm db:guard:check dit OK.
  3. Dans ce clone, AUCUN fichier .env qui pointe vers la production. Les commandes ci-dessous passent l'URL explicitement. Vérifier : grep -c DATABASE_URL .env 2>/dev/null doit rendre 0 ou rien.
  4. Accès à la console Neon (projet boostecom-app) et à Vercel (projet boostecom.app, Settings > Environment Variables).
  5. Dans Stripe (le mode que la production utilise) : aucun abonnement actif lié à l'organisation du fondateur qu'on ne veuille pas perdre, et aucun webhook en ré-essai (Developers > Webhooks > l'endpoint de production). Un webhook rejoué après la bascule ne trouvera plus son organisation.
  6. Noter, AVANT d'y toucher, la valeur actuelle de chaque variable de base dans Vercel Production (voir 4.6), dans un endroit sûr hors de ce dépôt. C'est le rollback.

Au 2026-10-08, sur main de l'app (a7cf9edc), les quatre modèles Growth sont encore dans prisma/schema.prisma (189 modèles) : le prérequis 1 n'est pas rempli tant que data-platform/3120 n'est pas mergée et déployée.

2. Ce que la bascule change, et ce qu'elle ne change pas

RessourceEffet de la bascule
Postgresnouvelle base boostecom, schéma exact, données choisies
Ancienne base neondbintacte, plus aucune écriture de l'app : l'archive
Redis (Upstash)inchangé. Les clés indexées par les anciens ids ne seront plus lues et expirent ; le curseur Apify (discovery:apify:cursor:*) continue
Vercel Blobinchangé. Les objets préfixés par l'ancien storeId (stores/<id>/...) deviennent orphelins : rien ne les supprime automatiquement, ils restent lisibles par leur URL
QStashles jobs déjà en file qui portent un ancien id échouent proprement et finissent en DLQ
Stripeclients et abonnements existants intouchés ; la nouvelle organisation aura un nouveau client Stripe au prochain checkout
Shopifyl'app reste installée ; la reconnexion (4.8) remet un jeton et des lignes en base, l'ancien jeton reste valide chez Shopify
TOKEN_ENCRYPTION_KEY, NEXTAUTH_SECRETinchangés : les colonnes chiffrées copiées restent déchiffrables. Les sessions tombent quand même, la table Session repartant vide

3. Classement des 185 modèles

Règle : une table n'est copiée que si elle porte une valeur qu'aucun seed, aucune synchronisation et aucun cron ne recrée. Tout le reste repart vide. Les sous-classes de (a) sont toutes de la donnée de référence ou de tiers ; (b) est le compte du fondateur ; (c) est jetable.

Le schéma compte 185 modèles (14 tables préfixées bst_ via @@map). La colonne « Table » donne le nom PHYSIQUE, celui des requêtes SQL. Chaque classe porte un code (A1 à C) que l'étape 4.0 réutilise.

CodeClasseModèlesTraitement
A1Registres et réglages plateforme15copier
A2Reconstruits automatiquement5ne pas copier, sauf filtre indiqué
A3Données de tiers, consentements, conformité12copier si non vide
A4Corpus Intelligence (Store Graph, découverte)10copier (recommandé)
B1Compte du fondateur20refaire à la main
B2Données d'usage du fondateur32perdues (voir 3.3)
C!Attendus vides16une seule ligne, et on ARRÊTE pour décider
CJetables75repartent vides

3.1 Les quatre tables orphelines

GrowthUnit, ContentRender, ContentReview, AttributionEvent ne sont plus dans le schéma (data-platform/3120). Elles n'existeront pas dans la base neuve et restent dans l'archive. La procédure de DROP cible de data-platform/3121 (voir Maintenance des index) devient sans objet dès que la bascule est faite.

La requête de l'annexe B liste toute autre table présente en production et absente du schéma : même traitement, elle reste dans l'archive.

3.2 Les 185 modèles, un par un

A1. Registres et réglages plateforme : COPIER (15)

ModèleTableCe qu'on en fait, ce qu'on perd
PlanPlansurcharges admin du catalogue src/config/plans.ts, dont les stripePriceId* : sans copie, le code par défaut s'applique et un prix Stripe posé seulement en base est perdu (checkout cassé)
AIModelAIModelsurcharges admin des tarifs modèles, lues par la facturation (getProviderCost) ; sans copie : tarifs du code
AgentPersonaAgentPersonasurcharges admin des personas ; sans copie : identity-registry du code
PlatformConfigPlatformConfigsections « seo », « distribution », « founding » et « team » ; sans copie : défauts de src/services/platform/config-schemas.ts
SkillSkillcatalogue de publication /api/registry/skills ; sans copie : catalogue vide (aucun runtime agent ne le lit)
ContentOverrideContentOverridesurcharges CMS (publié, épinglé, archive) des contenus content/ ; sans copie : état par défaut des fichiers
EmailTemplateOverrideEmailTemplateOverridetemplates email édités en admin ; sans copie : templates du code
PlatformCostPlatformCostcoûts fournisseurs saisis à la main (/admin/revenue/platform-costs) ; sans copie : à ressaisir
DevStorePoolDevStorePoolpool de dev stores Shopify réellement créés chez Shopify Partners ; sans copie : on perd la trace de boutiques qui existent toujours
AffiliateCodeAffiliateCodecodes de parrainage créés en admin ; un code personnel du fondateur pointe vers son ancien userId (pas de FK) : le recréer
ChangelogEntrybst_changelog_entrytable bst_changelog_entry ; entrées éditées en admin. Alternative partielle : pnpm db:seed (prisma/seed-changelog.ts) + sync public-log au cold start (brouillons)
StatusIncidentbst_status_incidenttable bst_status_incident, historique public de /status ; sans copie : page de statut sans historique
StatusWebhookbst_status_webhooktable bst_status_webhook, webhooks sortants configurés en admin (secret chiffré par TOKEN_ENCRYPTION_KEY, inchangé)
EcosystemSignalEcosystemSignalmémoire éditoriale du Bulletin, écrite par l'opérateur ; sans copie : perdue
EventEventsessions communautaires créées en admin ; sans copie : à recréer

A2. Reconstruits automatiquement : NE PAS copier, sauf filtre indiqué (5)

ModèleTableCe qu'on en fait, ce qu'on perd
MarketplaceListingMarketplaceListingannonces BoostEcom resynchronisées depuis content/marketplace/*.json à chaque cold start (src/services/marketplace/sync-from-content.ts). Copier SEULEMENT ownedByBoostecom = false (annonces de vendeurs tiers), sellerId mis à NULL
RoadmapItembst_roadmap_itemtable bst_roadmap_item ; projetée depuis le backlog au cold start (src/services/fleet/sync.ts, src/services/public-log/sync.ts). Copier SEULEMENT backlogRef IS NULL (lignes saisies à la main)
StoreSignalIndexStoreSignalIndexindex inversé du Store Graph, reconstruit depuis StoreIntelligence par src/services/algorithms/intelligence/graph/builder.ts (crons intelligence)
MarketClusterMarketClusteragrégat par niche, recalculé par le cron intelligence/market-aggregate
PredictionAccuracyPredictionAccuracyrecalculé chaque semaine par intelligence/prediction-backtest, à partir de StoreMetricDaily

A3. Données de tiers, consentements, conformité : COPIER si non vide (12)

ModèleTableCe qu'on en fait, ce qu'on perd
IntelligenceOptOutIntelligenceOptOutretraits RGPD du Store Spy demandés par des propriétaires de boutiques : NE PAS perdre, sinon un domaine retiré peut revenir dans l'index
IntelligenceSuppressionIntelligenceSuppressiontombstones opérateur : domaines à ne jamais re-semer (le bootstrap discovery les relit)
ShopifyCustomerRedactionShopifyCustomerRedactionregistre d'effacement customers/redact : barrière qui empêche de ré-ingérer un client effacé
ShopifyComplianceRequestShopifyComplianceRequestdemandes de conformité Shopify en attente (customers/data_request, shop/redact) : une demande ouverte doit être traitée
BulletinContactBulletinContactpersonnes inscrites au Bulletin depuis un formulaire public (pas des utilisateurs)
BulletinSubscriptionBulletinSubscriptionabonnements Bulletin, y compris désabonnements à respecter
StatusSubscriptionbst_status_subscriptiontable bst_status_subscription, abonnés email de /status (double opt-in)
FoundingApplicationFoundingApplicationcandidatures Founding Cohort (formulaire public)
NetworkApplicationbst_network_applicationtable bst_network_application, candidatures réseau (formulaire public)
Feedbackbst_feedbacktable bst_feedback ; retours utilisateurs, userId mis à NULL à la copie
SupportThreadSupportThreadfils support (messages de contact) ; avec SupportMessage
SupportMessageSupportMessagemessages des fils support

A4. Corpus Intelligence (Store Graph, découverte) : COPIER, recommandé (10)

ModèleTableCe qu'on en fait, ce qu'on perd
StoreIntelligenceStoreIntelligencecorpus du Store Spy / Store Graph (boutiques publiques). Reconstructible seulement en partie : bootstrap de domaines curés au cold start si la table est vide, puis discovery-harvest-tick (CT, Common Crawl) et discovery-apify-sync (payant), des semaines de récolte. storeId mis à NULL à la copie
StoreMetricDailyStoreMetricDailysérie temporelle quotidienne par boutique : NON reconstructible (le passé n'est plus observable). Substrat du moteur de prédiction
AdActivitySnapshotAdActivitySnapshotsérie temporelle Meta Ad Library : NON reconstructible
IntelligencePriceObservationIntelligencePriceObservationhistorique des prix : NON reconstructible
CatalogDeltaCatalogDeltahistorique des diffs de catalogue : NON reconstructible
StoreAnomalyStoreAnomalyhistorique des anomalies : NON reconstructible (les 30 derniers jours vivent aussi dans StoreIntelligence.inferred)
CreativeTrendCreativeTrendsérie temporelle des angles créatifs : NON reconstructible
AdCreativeAnalysisAdCreativeAnalysisanalyses vision IA des créatives : reconstructibles, mais en repayant les appels modèle
AdCreativeLabelAdCreativeLabelétiquettes IA des créatives : reconstructibles en repayant les appels modèle
IntelligenceDiscoveryCandidateIntelligenceDiscoveryCandidatefile de découverte (domaines candidats et backoff) : se re-remplit par la récolte, la copie évite de repartir de zéro

B1. Compte du fondateur : à REFAIRE à la main (20)

ModèleTableCe qu'on en fait, ce qu'on perd
UserUserse reconnecter par OTP : le PREMIER utilisateur d'une base vide est promu ADMIN (/api/auth/welcome), comme une adresse égale à ADMIN_EMAIL
AccountAccountcomptes NextAuth OAuth ; l'OTP Resend est le seul fournisseur : rien à refaire en pratique
OrganizationOrganizationrecréer l'organisation avec le MÊME slug que INTERNAL_ORG_SLUGS (sinon corriger la variable). stripeCustomerId repart à vide : un nouveau client Stripe sera créé au prochain checkout
OrganizationMemberOrganizationMembercréé avec l'organisation (owner)
StoreStorereconnecter la boutique Shopify (OAuth ou Custom App). Nouvel id : les objets Blob préfixés par l'ancien storeId deviennent orphelins
StoreContextStoreContextcontexte de marque de la boutique : se reconstitue à la connexion et au wizard ; les modules écrits à la main sont à refaire
IntegrationConnectionIntegrationConnectionappairage Shopify Custom App et clé MCP : refaire l'appairage, régénérer la clé MCP
ConnectorConnectorjetons OAuth Google, Meta, Klaviyo, Figma, Notion : reconnecter chaque connecteur
McpConnectorMcpConnectorserveurs MCP tiers déclarés par boutique : à redéclarer
WhatsappChannelWhatsappChannelcanal WhatsApp par boutique : à reconfigurer
SubscriptionSubscriptionabonnement Stripe : la nouvelle organisation part en Free. Avant la bascule, vérifier dans Stripe qu'aucun abonnement actif ne pointe vers l'ancienne organisation
CreditCreditjournal de crédits : le solde repart de la dotation du plan (re-créditer à la main en admin si besoin)
AgentAutonomyAgentAutonomyniveaux d'autonomie par agent et par organisation : à régler
AccessGrantAccessGrantmandats délégués : à réaccorder
AccessGrantRequestAccessGrantRequestdemandes de mandat : sans objet sur une base neuve
ApiTokenbst_api_tokentable bst_api_token : jetons API à réémettre
IntelligenceApiKeyIntelligenceApiKeyclés bei_... à réémettre
IntelligenceBetaConsentIntelligenceBetaConsentconsentement beta calibration : à redonner
OAuthClientOAuthClientclients MCP OAuth (enregistrement dynamique) : chaque client MCP se réenregistre à la prochaine connexion
OAuthAccessTokenOAuthAccessTokenjetons MCP OAuth : chaque client MCP (Claude, etc.) doit se réautoriser

B2. Données d'usage du fondateur : PERDUES (32)

Copiables seulement avec les identités, voir 3.3.

C!. Attendus VIDES : une seule ligne, et on ARRÊTE pour décider (16)

ModèleTableCe qu'on en fait, ce qu'on perd
OfferOffermarketplace : offre d'achat d'un tiers
DealThreadDealThreadmarketplace : deal entre tiers
DealMessageDealMessagemarketplace : messages de deal
OrderOrdermarketplace : commande payée (argent)
ReviewReviewmarketplace : avis
SavedListingSavedListingmarketplace : favoris d'utilisateurs
ListingVoteListingVotemarketplace : votes
StripeConnectAccountStripeConnectAccountcompte Stripe Connect d'un vendeur (argent)
DisputeDisputelitige marketplace (argent)
KycVerificationKycVerificationKYC chiffré d'un tiers
LegalSignatureLegalSignaturesignatures NDA/LOI/APA
VerifiedRevenueVerifiedRevenuerevenus vérifiés d'une annonce
ListingEventListingEventtimeline des annonces (tiers)
SponsorPlacementSponsorPlacementplacement sponsor payé (argent)
AffiliateCommissionAffiliateCommissioncommissions de parrainage dues ou payées (argent)
AffiliateRedemptionAffiliateRedemptionutilisations de codes de parrainage

C. Jetables : journaux, files, caches, événements, données ré-ingérées (75)

3.3 Et si le fondateur veut garder son historique (B2) ?

Les tables B2 référencent User, Organization et Store par id. Les copier impose de copier ces lignes avec les MÊMES ids (plus Account, OrganizationMember, StoreContext, Subscription, Credit, IntegrationConnection, Connector), et de suivre l'ordre des clés étrangères. C'est faisable avec la même méthode que l'annexe C, mais ce n'est plus repartir sur une base propre, et cela reporte dans la base neuve l'état exact qu'on voulait quitter.

Recommandation : ne pas le faire, sauf pour garder les mêmes storeId si les médias Blob du fondateur comptent. Décision du propriétaire.

4. Procédure

Chaque étape est faite par le propriétaire, dans l'ordre. $SRC, $DST et $DST_POOLED sont des URL de connexion que le propriétaire relève dans la console Neon et garde dans son terminal : elles ne s'écrivent ni dans un fichier du dépôt, ni dans un ticket, ni dans une conversation.

4.0 Mesurer, en lecture seule [PROPRIÉTAIRE]

Dans la console Neon, branche main, base neondb, SQL Editor.

  1. Estimation instantanée de toutes les tables (ne peut pas échouer) :
SELECT relname AS t, n_live_tup AS approx_rows,
       pg_size_pretty(pg_total_relation_size(relid)) AS size
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC;
  1. Compte exact, une requête par table (annexe A). Si une ligne échoue sur relation does not exist, la supprimer et relancer : la table n'a jamais été créée en production.
  2. Tables présentes en base et absentes du schéma (annexe B).
  3. Lire les résultats avec ces règles :
    • une table C! avec au moins une ligne : ARRÊTER (voir ci-dessous) ;
    • une table A3 avec des lignes : elle est copiée (annexe C) ;
    • une table A1 ou A4 vide : sa ligne de copie est sans effet, la laisser ;
    • noter les comptes des tables copiées : ils servent au contrôle de 4.4.

Une table C! non vide arrête tout. De l'argent ou un tiers est en jeu (commande, payout, litige, KYC, sponsor, commission) : rien ne se copie sans décision écrite du propriétaire.

4.1 Geler et archiver [PROPRIÉTAIRE]

  1. Ne plus utiliser l'app jusqu'à la fin de 4.6. Les crons continuent d'écrire dans l'ancienne base : ces écritures (surtout Intelligence) sont perdues pour la base neuve, c'est accepté.
  2. Neon > Branches > Create branch : parent main, nom archive-avant-base-propre-AAAAMMJJ, depuis l'état courant (head). C'est un instantané copy-on-write, immédiat. Il sert de source à la copie (données figées) et de deuxième filet si neondb était touchée par erreur. Si le plan Neon le permet, la marquer protégée.
  3. Noter l'URL de connexion DIRECTE (hôte sans -pooler) de cette branche, base neondb : c'est $SRC ci-dessous.

4.2 Créer la base neuve [PROPRIÉTAIRE]

  1. Neon > Branches > main > Databases > New database : nom boostecom, propriétaire le même rôle que neondb (en général neondb_owner). Même hôte, même rôle, même mot de passe : seule la fin de l'URL change (/neondb devient /boostecom).
  2. Noter l'URL DIRECTE (sans -pooler) de main / boostecom : $DST. Noter aussi la variante poolée (avec -pooler) : $DST_POOLED, pour 4.6.
  3. Vérifier qu'elle est vide : psql "$DST" -c '\dt' doit répondre Did not find any relations.

4.3 Appliquer le schéma [PROPRIÉTAIRE]

Le chemin du dépôt en production est le garde généré appliqué au cold start (ADR 0009, Base de données). Sur une base VIDE, on applique d'abord le schéma entier avec Prisma, depuis le poste, pour pouvoir le vérifier et copier les données AVANT que l'app ne démarre dessus. Le cold start fera ensuite le reste (4.7).

Depuis le clone à jour de main :

pnpm exec prisma db push --url "$DST"
  • Lire la ligne Datasource "db": PostgreSQL database "boostecom" ... at "<hote>" avant tout : elle doit nommer boostecom. Sinon Ctrl-C.
  • Sur une base vide, Prisma n'a rien à perdre : il ne doit afficher AUCUN avertissement de perte de données. S'il en affiche un, la cible n'est pas la base vide : répondre non, ARRÊTER.
  • Ne jamais ajouter --accept-data-loss ni --force-reset ici.

Vérification (lecture seule) : le schéma en base doit être exactement le schéma du dépôt.

DATABASE_URL="$DST" pnpm exec prisma migrate diff \
  --from-config-datasource --to-schema prisma/schema.prisma --exit-code

Attendu : code de sortie 0 (diff vide). Après le premier cold start (4.7), cette même commande listera les index partiels de EXTRA_STEPS comme « à supprimer » : c'est normal, Prisma ne sait pas les exprimer, et c'est pour cela que le garde les crée.

4.4 Copier les tables retenues [PROPRIÉTAIRE]

Trois fichiers, générés depuis le schéma du 2026-10-08 (annexes C, D, E) : export.sql (lecture seule, sur $SRC), import.sql (une seule transaction, sur $DST), compare.sql (lecture seule, sur les deux). Les listes de colonnes sont celles du schéma : une colonne absente de la source fait échouer l'export au lieu de copier n'importe quoi.

Dans un dossier de travail vide :

psql "$SRC" -f export.sql      # écrit un CSV par table
psql "$DST" -f import.sql      # tout ou rien : BEGIN ... COMMIT
psql "$SRC" -f compare.sql > src.txt
psql "$DST" -f compare.sql > dst.txt
diff src.txt dst.txt           # attendu : aucune différence

Ce que la copie transforme, et pourquoi :

  • StoreIntelligence.storeId, MarketplaceListing.sellerId et bst_feedback.userId sont mis à NULL : ils pointaient vers des boutiques et des utilisateurs qui n'existent pas dans la base neuve (clé étrangère nullable, SET NULL dans le schéma) ;
  • MarketplaceListing : seules les annonces de tiers (ownedByBoostecom = false) ; celles de BoostEcom reviennent par la synchronisation content/marketplace au cold start ;
  • bst_roadmap_item : seules les lignes saisies à la main (backlogRef IS NULL) ; les autres sont reprojetées depuis le backlog ;
  • les séquences de bst_roadmap_item et bst_changelog_entry (seules colonnes autoincrement du schéma) sont recalées sur le maximum copié.

Les CSV contiennent des données personnelles (abonnés, candidatures) et des colonnes chiffrées : ils restent sur le poste, jamais dans un dépôt ni un partage, et sont supprimés une fois 4.9 validé.

4.5 Semer [PROPRIÉTAIRE]

DATABASE_URL="$DST" pnpm db:seed

prisma/seed.ts ne sème que ChangelogEntry, par upsert sur un identifiant stable : sans effet nuisible si la table a été copiée. Il n'y a pas d'autre seed : plans, modèles IA, personas et réglages vivent dans le code, la base ne porte que leurs surcharges (copiées en 4.4).

4.6 Basculer Vercel [PROPRIÉTAIRE]

  1. Vercel > Integrations > Neon : l'option « Create Database Branch For Deployment » est encore cochée pour Production (platform-ops/3160). La décocher avant la bascule, pour qu'aucun déploiement ne crée une branche ni ne réécrive les variables.
  2. Vercel > Settings > Environment Variables, environnement Production. Le code lit, dans cet ordre, la première non vide de : DATABASE_URL, POSTGRES_PRISMA_URL, DATABASE_URL_UNPOOLED, DATABASE_POSTGRES_URL_NON_POOLING, DATABASE_POSTGRES_URL, POSTGRES_URL_NON_POOLING, POSTGRES_URL (src/lib/core/database-url.ts). Pour CHACUNE qui existe : remplacer /neondb par /boostecom dans l'URL (version poolée pour les variables poolées, directe pour DATABASE_URL_UNPOOLED, DATABASE_POSTGRES_URL_NON_POOLING et POSTGRES_URL_NON_POOLING). Ne rien changer d'autre.
  3. Redéployer la production (Deployments > dernier déploiement de production > Redeploy) : une variable n'est lue qu'au déploiement.

Si ces variables sont gérées par l'intégration et non modifiables : ARRÊTER ici, ne pas improviser. Deux voies à valider avec la documentation Vercel et Neon du jour : redéfinir les variables à la main après avoir détaché l'intégration de l'environnement Production, ou restaurer la branche main depuis l'état voulu (fonction de restauration Neon, qui garde l'URL). Ce runbook ne tranche pas entre les deux.

4.7 Le premier cold start, sans action

Au démarrage (src/instrumentation-node.ts) :

  1. ensureSchemaGuard compare le catalogue généré à la base et applique ce qui manque : ici, seulement les index partiels et les étapes de EXTRA_STEPS (src/services/database/pending-migrations.ts). Les conversions de type y sont sans effet sur une base déjà au bon type. C'est la seule étape attendue ; les suivantes sont détachées et s'enchaînent dans cet ordre, sans qu'aucune requête ne les attende ;
  2. bootstrap Discovery seulement si StoreIntelligence est vide : copiée en 4.4, elle ne l'est pas, donc rien ;
  3. synchronisation des annonces BoostEcom depuis content/marketplace/ (src/services/marketplace/sync-from-content.ts) ;
  4. projection des brouillons de changelog et de la roadmap (src/services/public-log/sync.ts, puis src/services/fleet/sync.ts).

Les crons reprennent à leur horaire et remplissent les tables C.

4.8 Refaire le compte du fondateur [PROPRIÉTAIRE]

  1. Se déconnecter de l'app (ou effacer ses cookies), puis se reconnecter par OTP avec son adresse. Premier utilisateur de la base, il est promu ADMIN (/api/auth/welcome) ; une adresse égale à ADMIN_EMAIL l'est aussi.
  2. Créer l'organisation avec le MÊME slug que la variable INTERNAL_ORG_SLUGS (sinon le siège ne trouve plus la marque de la plateforme). Relevé du slug actuel, sur $SRC : SELECT slug, name FROM "Organization" ORDER BY "createdAt";
  3. Reconnecter la boutique Shopify (OAuth ou appairage Custom App) et, si utilisée, régénérer la clé MCP de la boutique.
  4. Reconnecter chaque connecteur utilisé (Google, Meta, Klaviyo, Figma, Notion) ; redéclarer les serveurs MCP tiers et le canal WhatsApp s'il y en avait.
  5. Réémettre les jetons API, les clés Intelligence bei_... et réautoriser chaque client MCP OAuth (Claude, etc.).
  6. Régler l'autonomie des agents, redonner les mandats délégués, recréer les workflows, trackers et recherches enregistrées qui comptent.
  7. Si un code de parrainage personnel existait : le recréer (il pointait vers l'ancien userId).
  8. Plan et crédits : l'organisation repart en Free avec sa dotation ; ajuster en admin si besoin.

4.9 Contrôles [PROPRIÉTAIRE]

ContrôleAttendu
GET /api/healthchecks.db: true. checks.cache dépend de Redis (platform-ops/3155), pas de cette bascule
SELECT count(*) FROM "User"; sur $DST après la connexion1 : l'app écrit bien dans la base neuve
Même requête sur l'ancienne baseinchangée depuis 4.0 : plus rien n'y écrit
GET /api/admin/db/push (admin)aucun drift
/adminaccessible (rôle ADMIN)
Organisation créée, boutique connectée, un message à @Atlasréponse, une ligne Credit débitée
/marketplace, /roadmap, /changelogcontenus présents (sync + copie)
Hub Intelligence / Store Spyboutiques présentes (corpus copié)
/statuspage et historique des incidents
Webhook de test depuis Stripe (endpoint de production)200, une ligne StripeEvent
Une heure après : SELECT "cronName", max("startedAt") FROM "CronExecution" GROUP BY 1 ORDER BY 2 DESC; sur $DSTles crons horaires y apparaissent
prisma migrate diff de 4.3seuls les index partiels de EXTRA_STEPS apparaissent

5. Garder l'archive

[PROPRIÉTAIRE, décision] Durée de conservation de neondb et de la branche d'archive : proposition 30 jours, à trancher. L'archive contient des données personnelles (abonnés, candidatures, journaux) : ne pas la garder indéfiniment (voir data-platform/0520, décision RGPD ouverte).

Pendant ce temps : ne rien y écrire. À l'échéance, et seulement si 4.9 est resté vert tout du long :

  1. si utile, un pg_dump complet de $SRC conservé hors Neon, avec son sha256sum ;
  2. supprimer la branche archive-avant-base-propre-AAAAMMJJ ;
  3. supprimer la base neondb de main (Databases > neondb > Delete) ;
  4. fermer data-platform/3121 (« sans objet : base reconstruite le ... »).

La suppression de neondb (point 3) est le seul pas irréversible de ce runbook. Elle n'a lieu qu'à l'échéance décidée, et seulement si les contrôles de 4.9 sont restés verts tout du long.

6. Rollback

  • Avant 4.6 : rien n'a changé pour l'app. Supprimer la base boostecom et recommencer.
  • Après 4.6 : remettre dans Vercel les valeurs notées en 1.6, puis redéployer. L'app repart sur neondb, intacte. Ce qui a été créé entre-temps dans boostecom (compte, organisation, boutique) n'existe pas dans neondb : à refaire ou à recopier à la main.
  • Code : aucun. Le retrait des quatre modèles Growth n'impose pas la bascule et ne se revert pas pour elle.

7. Risques et points ouverts

  • Variables Vercel gérées par l'intégration Neon (4.6) : seul point où la procédure peut bloquer ; elle s'arrête au lieu d'improviser.
  • Une table C! non vide (4.0) arrête tout jusqu'à décision.
  • Les séries temporelles Intelligence et les historiques Shopify antérieurs ne reviennent pas si on ne les copie pas.
  • Blob : les médias de l'ancien storeId restent stockés et facturés tant qu'on ne les purge pas (voir Vercel Blob).
  • Les comptes et listes de colonnes de ce runbook datent du schéma du 2026-10-08 : si prisma/schema.prisma a changé depuis, régénérer les annexes C, D, E avant de s'en servir (une colonne en trop ou en moins fait échouer l'export ou l'import, sans rien écrire à moitié).

Annexes

Requêtes générées depuis le schéma du 2026-10-08, à coller par le propriétaire. Aucune ne contient d'URL ni de secret : la connexion vient de psql "$SRC" ou psql "$DST".

Annexe A. Compte exact par table (lecture seule, sur neondb)

Annexe B. Tables en base absentes du schéma (lecture seule)

Annexe C. export.sql (sur $SRC, lecture seule)

Annexe D. import.sql (sur $DST, une transaction)

Annexe E. compare.sql (sur $SRC puis $DST, lecture seule)