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 PRdata-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 depuisprisma/schema.prisma, colle exactement au schéma. - Comment. Une base vide
boostecomdans la branche Neonmain, le schéma appliqué parprisma db pushsur cette base vide, une copie ciblée des seules tables qui ont de la valeur, puis la bascule deDATABASE_URLdans Vercel. L'ancienne baseneondbn'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 :
- La PR
data-platform/3120(retrait deGrowthUnit,ContentRender,ContentReview,AttributionEvent) est mergée et déployée. Sinon la base neuve recrée ces quatre tables, vides. - Un poste avec
psql(client PostgreSQL 16 ou plus), Node etpnpm, et un clone à jour demain:pnpm installpasse, puispnpm db:guard:checkditOK. - Dans ce clone, AUCUN fichier
.envqui pointe vers la production. Les commandes ci-dessous passent l'URL explicitement. Vérifier :grep -c DATABASE_URL .env 2>/dev/nulldoit rendre0ou rien. - Accès à la console Neon (projet
boostecom-app) et à Vercel (projetboostecom.app, Settings > Environment Variables). - 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.
- 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
| Ressource | Effet de la bascule |
|---|---|
| Postgres | nouvelle base boostecom, schéma exact, données choisies |
Ancienne base neondb | intacte, 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 Blob | inchangé. Les objets préfixés par l'ancien storeId (stores/<id>/...) deviennent orphelins : rien ne les supprime automatiquement, ils restent lisibles par leur URL |
| QStash | les jobs déjà en file qui portent un ancien id échouent proprement et finissent en DLQ |
| Stripe | clients et abonnements existants intouchés ; la nouvelle organisation aura un nouveau client Stripe au prochain checkout |
| Shopify | l'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_SECRET | inchangé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.
| Code | Classe | Modèles | Traitement |
|---|---|---|---|
| A1 | Registres et réglages plateforme | 15 | copier |
| A2 | Reconstruits automatiquement | 5 | ne pas copier, sauf filtre indiqué |
| A3 | Données de tiers, consentements, conformité | 12 | copier si non vide |
| A4 | Corpus Intelligence (Store Graph, découverte) | 10 | copier (recommandé) |
| B1 | Compte du fondateur | 20 | refaire à la main |
| B2 | Données d'usage du fondateur | 32 | perdues (voir 3.3) |
| C! | Attendus vides | 16 | une seule ligne, et on ARRÊTE pour décider |
| C | Jetables | 75 | repartent 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èle | Table | Ce qu'on en fait, ce qu'on perd |
|---|---|---|
Plan | Plan | surcharges 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é) |
AIModel | AIModel | surcharges admin des tarifs modèles, lues par la facturation (getProviderCost) ; sans copie : tarifs du code |
AgentPersona | AgentPersona | surcharges admin des personas ; sans copie : identity-registry du code |
PlatformConfig | PlatformConfig | sections « seo », « distribution », « founding » et « team » ; sans copie : défauts de src/services/platform/config-schemas.ts |
Skill | Skill | catalogue de publication /api/registry/skills ; sans copie : catalogue vide (aucun runtime agent ne le lit) |
ContentOverride | ContentOverride | surcharges CMS (publié, épinglé, archive) des contenus content/ ; sans copie : état par défaut des fichiers |
EmailTemplateOverride | EmailTemplateOverride | templates email édités en admin ; sans copie : templates du code |
PlatformCost | PlatformCost | coûts fournisseurs saisis à la main (/admin/revenue/platform-costs) ; sans copie : à ressaisir |
DevStorePool | DevStorePool | pool de dev stores Shopify réellement créés chez Shopify Partners ; sans copie : on perd la trace de boutiques qui existent toujours |
AffiliateCode | AffiliateCode | codes de parrainage créés en admin ; un code personnel du fondateur pointe vers son ancien userId (pas de FK) : le recréer |
ChangelogEntry | bst_changelog_entry | table 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) |
StatusIncident | bst_status_incident | table bst_status_incident, historique public de /status ; sans copie : page de statut sans historique |
StatusWebhook | bst_status_webhook | table bst_status_webhook, webhooks sortants configurés en admin (secret chiffré par TOKEN_ENCRYPTION_KEY, inchangé) |
EcosystemSignal | EcosystemSignal | mémoire éditoriale du Bulletin, écrite par l'opérateur ; sans copie : perdue |
Event | Event | sessions communautaires créées en admin ; sans copie : à recréer |
A2. Reconstruits automatiquement : NE PAS copier, sauf filtre indiqué (5)
| Modèle | Table | Ce qu'on en fait, ce qu'on perd |
|---|---|---|
MarketplaceListing | MarketplaceListing | annonces 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 |
RoadmapItem | bst_roadmap_item | table 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) |
StoreSignalIndex | StoreSignalIndex | index inversé du Store Graph, reconstruit depuis StoreIntelligence par src/services/algorithms/intelligence/graph/builder.ts (crons intelligence) |
MarketCluster | MarketCluster | agrégat par niche, recalculé par le cron intelligence/market-aggregate |
PredictionAccuracy | PredictionAccuracy | recalculé chaque semaine par intelligence/prediction-backtest, à partir de StoreMetricDaily |
A3. Données de tiers, consentements, conformité : COPIER si non vide (12)
| Modèle | Table | Ce qu'on en fait, ce qu'on perd |
|---|---|---|
IntelligenceOptOut | IntelligenceOptOut | retraits RGPD du Store Spy demandés par des propriétaires de boutiques : NE PAS perdre, sinon un domaine retiré peut revenir dans l'index |
IntelligenceSuppression | IntelligenceSuppression | tombstones opérateur : domaines à ne jamais re-semer (le bootstrap discovery les relit) |
ShopifyCustomerRedaction | ShopifyCustomerRedaction | registre d'effacement customers/redact : barrière qui empêche de ré-ingérer un client effacé |
ShopifyComplianceRequest | ShopifyComplianceRequest | demandes de conformité Shopify en attente (customers/data_request, shop/redact) : une demande ouverte doit être traitée |
BulletinContact | BulletinContact | personnes inscrites au Bulletin depuis un formulaire public (pas des utilisateurs) |
BulletinSubscription | BulletinSubscription | abonnements Bulletin, y compris désabonnements à respecter |
StatusSubscription | bst_status_subscription | table bst_status_subscription, abonnés email de /status (double opt-in) |
FoundingApplication | FoundingApplication | candidatures Founding Cohort (formulaire public) |
NetworkApplication | bst_network_application | table bst_network_application, candidatures réseau (formulaire public) |
Feedback | bst_feedback | table bst_feedback ; retours utilisateurs, userId mis à NULL à la copie |
SupportThread | SupportThread | fils support (messages de contact) ; avec SupportMessage |
SupportMessage | SupportMessage | messages des fils support |
A4. Corpus Intelligence (Store Graph, découverte) : COPIER, recommandé (10)
| Modèle | Table | Ce qu'on en fait, ce qu'on perd |
|---|---|---|
StoreIntelligence | StoreIntelligence | corpus 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 |
StoreMetricDaily | StoreMetricDaily | série temporelle quotidienne par boutique : NON reconstructible (le passé n'est plus observable). Substrat du moteur de prédiction |
AdActivitySnapshot | AdActivitySnapshot | série temporelle Meta Ad Library : NON reconstructible |
IntelligencePriceObservation | IntelligencePriceObservation | historique des prix : NON reconstructible |
CatalogDelta | CatalogDelta | historique des diffs de catalogue : NON reconstructible |
StoreAnomaly | StoreAnomaly | historique des anomalies : NON reconstructible (les 30 derniers jours vivent aussi dans StoreIntelligence.inferred) |
CreativeTrend | CreativeTrend | série temporelle des angles créatifs : NON reconstructible |
AdCreativeAnalysis | AdCreativeAnalysis | analyses vision IA des créatives : reconstructibles, mais en repayant les appels modèle |
AdCreativeLabel | AdCreativeLabel | étiquettes IA des créatives : reconstructibles en repayant les appels modèle |
IntelligenceDiscoveryCandidate | IntelligenceDiscoveryCandidate | file 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èle | Table | Ce qu'on en fait, ce qu'on perd |
|---|---|---|
User | User | se reconnecter par OTP : le PREMIER utilisateur d'une base vide est promu ADMIN (/api/auth/welcome), comme une adresse égale à ADMIN_EMAIL |
Account | Account | comptes NextAuth OAuth ; l'OTP Resend est le seul fournisseur : rien à refaire en pratique |
Organization | Organization | recré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 |
OrganizationMember | OrganizationMember | créé avec l'organisation (owner) |
Store | Store | reconnecter la boutique Shopify (OAuth ou Custom App). Nouvel id : les objets Blob préfixés par l'ancien storeId deviennent orphelins |
StoreContext | StoreContext | contexte de marque de la boutique : se reconstitue à la connexion et au wizard ; les modules écrits à la main sont à refaire |
IntegrationConnection | IntegrationConnection | appairage Shopify Custom App et clé MCP : refaire l'appairage, régénérer la clé MCP |
Connector | Connector | jetons OAuth Google, Meta, Klaviyo, Figma, Notion : reconnecter chaque connecteur |
McpConnector | McpConnector | serveurs MCP tiers déclarés par boutique : à redéclarer |
WhatsappChannel | WhatsappChannel | canal WhatsApp par boutique : à reconfigurer |
Subscription | Subscription | abonnement Stripe : la nouvelle organisation part en Free. Avant la bascule, vérifier dans Stripe qu'aucun abonnement actif ne pointe vers l'ancienne organisation |
Credit | Credit | journal de crédits : le solde repart de la dotation du plan (re-créditer à la main en admin si besoin) |
AgentAutonomy | AgentAutonomy | niveaux d'autonomie par agent et par organisation : à régler |
AccessGrant | AccessGrant | mandats délégués : à réaccorder |
AccessGrantRequest | AccessGrantRequest | demandes de mandat : sans objet sur une base neuve |
ApiToken | bst_api_token | table bst_api_token : jetons API à réémettre |
IntelligenceApiKey | IntelligenceApiKey | clés bei_... à réémettre |
IntelligenceBetaConsent | IntelligenceBetaConsent | consentement beta calibration : à redonner |
OAuthClient | OAuthClient | clients MCP OAuth (enregistrement dynamique) : chaque client MCP se réenregistre à la prochaine connexion |
OAuthAccessToken | OAuthAccessToken | jetons 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èle | Table | Ce qu'on en fait, ce qu'on perd |
|---|---|---|
Offer | Offer | marketplace : offre d'achat d'un tiers |
DealThread | DealThread | marketplace : deal entre tiers |
DealMessage | DealMessage | marketplace : messages de deal |
Order | Order | marketplace : commande payée (argent) |
Review | Review | marketplace : avis |
SavedListing | SavedListing | marketplace : favoris d'utilisateurs |
ListingVote | ListingVote | marketplace : votes |
StripeConnectAccount | StripeConnectAccount | compte Stripe Connect d'un vendeur (argent) |
Dispute | Dispute | litige marketplace (argent) |
KycVerification | KycVerification | KYC chiffré d'un tiers |
LegalSignature | LegalSignature | signatures NDA/LOI/APA |
VerifiedRevenue | VerifiedRevenue | revenus vérifiés d'une annonce |
ListingEvent | ListingEvent | timeline des annonces (tiers) |
SponsorPlacement | SponsorPlacement | placement sponsor payé (argent) |
AffiliateCommission | AffiliateCommission | commissions de parrainage dues ou payées (argent) |
AffiliateRedemption | AffiliateRedemption | utilisations 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.
- 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;
- 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. - Tables présentes en base et absentes du schéma (annexe B).
- 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]
- 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é.
- Neon > Branches > Create branch : parent
main, nomarchive-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 sineondbétait touchée par erreur. Si le plan Neon le permet, la marquer protégée. - Noter l'URL de connexion DIRECTE (hôte sans
-pooler) de cette branche, baseneondb: c'est$SRCci-dessous.
4.2 Créer la base neuve [PROPRIÉTAIRE]
- Neon > Branches >
main> Databases > New database : nomboostecom, propriétaire le même rôle queneondb(en généralneondb_owner). Même hôte, même rôle, même mot de passe : seule la fin de l'URL change (/neondbdevient/boostecom). - Noter l'URL DIRECTE (sans
-pooler) demain/boostecom:$DST. Noter aussi la variante poolée (avec-pooler) :$DST_POOLED, pour 4.6. - Vérifier qu'elle est vide :
psql "$DST" -c '\dt'doit répondreDid 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 nommerboostecom. 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-lossni--force-resetici.
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.sellerIdetbst_feedback.userIdsont mis à NULL : ils pointaient vers des boutiques et des utilisateurs qui n'existent pas dans la base neuve (clé étrangère nullable,SET NULLdans le schéma) ;MarketplaceListing: seules les annonces de tiers (ownedByBoostecom = false) ; celles de BoostEcom reviennent par la synchronisationcontent/marketplaceau 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_itemetbst_changelog_entry(seules colonnesautoincrementdu 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]
- 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. - 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/neondbpar/boostecomdans l'URL (version poolée pour les variables poolées, directe pourDATABASE_URL_UNPOOLED,DATABASE_POSTGRES_URL_NON_POOLINGetPOSTGRES_URL_NON_POOLING). Ne rien changer d'autre. - 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) :
ensureSchemaGuardcompare le catalogue généré à la base et applique ce qui manque : ici, seulement les index partiels et les étapes deEXTRA_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 ;- bootstrap Discovery seulement si
StoreIntelligenceest vide : copiée en 4.4, elle ne l'est pas, donc rien ; - synchronisation des annonces BoostEcom depuis
content/marketplace/(src/services/marketplace/sync-from-content.ts) ; - projection des brouillons de changelog et de la roadmap (
src/services/public-log/sync.ts, puissrc/services/fleet/sync.ts).
Les crons reprennent à leur horaire et remplissent les tables C.
4.8 Refaire le compte du fondateur [PROPRIÉTAIRE]
- 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_EMAILl'est aussi. - 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"; - Reconnecter la boutique Shopify (OAuth ou appairage Custom App) et, si utilisée, régénérer la clé MCP de la boutique.
- Reconnecter chaque connecteur utilisé (Google, Meta, Klaviyo, Figma, Notion) ; redéclarer les serveurs MCP tiers et le canal WhatsApp s'il y en avait.
- Réémettre les jetons API, les clés Intelligence
bei_...et réautoriser chaque client MCP OAuth (Claude, etc.). - Régler l'autonomie des agents, redonner les mandats délégués, recréer les workflows, trackers et recherches enregistrées qui comptent.
- Si un code de parrainage personnel existait : le recréer (il pointait vers l'ancien
userId). - Plan et crédits : l'organisation repart en Free avec sa dotation ; ajuster en admin si besoin.
4.9 Contrôles [PROPRIÉTAIRE]
| Contrôle | Attendu |
|---|---|
GET /api/health | checks.db: true. checks.cache dépend de Redis (platform-ops/3155), pas de cette bascule |
SELECT count(*) FROM "User"; sur $DST après la connexion | 1 : l'app écrit bien dans la base neuve |
| Même requête sur l'ancienne base | inchangée depuis 4.0 : plus rien n'y écrit |
GET /api/admin/db/push (admin) | aucun drift |
/admin | accessible (rôle ADMIN) |
| Organisation créée, boutique connectée, un message à @Atlas | réponse, une ligne Credit débitée |
/marketplace, /roadmap, /changelog | contenus présents (sync + copie) |
| Hub Intelligence / Store Spy | boutiques présentes (corpus copié) |
/status | page 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 $DST | les crons horaires y apparaissent |
prisma migrate diff de 4.3 | seuls 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 :
- si utile, un
pg_dumpcomplet de$SRCconservé hors Neon, avec sonsha256sum; - supprimer la branche
archive-avant-base-propre-AAAAMMJJ; - supprimer la base
neondbdemain(Databases > neondb > Delete) ; - 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
boostecomet 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 dansboostecom(compte, organisation, boutique) n'existe pas dansneondb: à 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
storeIdrestent 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.prismaa 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".