CognxSafeTrack
chore: finalize Sprint P2 & P3 optimizations, baseline prisma migrations, and update technical audit docs
cfbb685 | # Postmortem : Crash au déploiement Railway — 30 avril 2026 | |
| ## Résumé | |
| Deux crashs successifs ont empêché le démarrage des processus `api` et `whatsapp-worker` en production sur Railway. Aucune régression fonctionnelle n'a eu lieu : le service n'a jamais démarré. Les deux bugs étaient liés à une mauvaise configuration de la chaîne de compilation TypeScript dans le monorepo. | |
| --- | |
| ## Crash 1 — `Cannot find package 'tsx'` | |
| ### Symptôme | |
| ``` | |
| Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'tsx' imported from /app/ | |
| Did you mean to import "tsx/dist/loader.mjs"? | |
| ``` | |
| PM2 redémarrait `api` et `worker` en boucle avec code de sortie `1`. | |
| ### Cause racine | |
| Le fichier `ecosystem.config.js` configurait PM2 pour exécuter les fichiers source TypeScript directement, en passant `--import tsx` à Node.js : | |
| ```js | |
| // ecosystem.config.js (avant correction) | |
| { | |
| script: 'apps/api/src/index.ts', | |
| interpreter: 'node', | |
| interpreter_args: '--import tsx', | |
| } | |
| ``` | |
| `tsx` était déclaré comme `devDependency` dans les deux apps. En production, les `devDependencies` ne sont pas installées, donc `tsx` était introuvable au démarrage. | |
| Le Dockerfile compilait déjà `api` via `pnpm --filter api build` (vers `dist/`), mais PM2 ignorait ce build et tentait de re-exécuter le source TypeScript — contradiction inutile. | |
| ### Correction | |
| **`ecosystem.config.js`** — pointer vers les fichiers compilés, sans tsx : | |
| ```js | |
| // Avant | |
| { script: 'apps/api/src/index.ts', interpreter_args: '--import tsx' } | |
| { script: 'apps/whatsapp-worker/src/index.ts', interpreter_args: '--import tsx' } | |
| // Après | |
| { script: 'apps/api/dist/index.js' } | |
| { script: 'apps/whatsapp-worker/dist/index.js' } | |
| ``` | |
| **`Dockerfile`** — ajouter la compilation du worker (seul `api` était compilé) : | |
| ```dockerfile | |
| # Avant | |
| RUN pnpm --filter api build | |
| # Après | |
| RUN pnpm --filter api build | |
| RUN pnpm --filter whatsapp-worker build | |
| ``` | |
| --- | |
| ## Crash 2 — `Cannot find module '.../packages/database/src/extension.js'` | |
| ### Symptôme | |
| ``` | |
| Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/app/packages/database/src/extension.js' | |
| imported from /app/packages/database/index.ts | |
| ``` | |
| ### Cause racine | |
| Trois problèmes imbriqués dans le package `@repo/database` : | |
| **1. `package.json` pointait vers le source TypeScript** | |
| ```json | |
| // packages/database/package.json (avant) | |
| "main": "./index.ts", | |
| "types": "./index.ts", | |
| "exports": { ".": "./index.ts" } | |
| ``` | |
| Node.js chargeait `index.ts` directement — impossible sans tsx. | |
| **2. `tsconfig.json` héritait `noEmit: true` sans le surcharger** | |
| ```json | |
| // packages/tsconfig/base.json | |
| { "noEmit": true } | |
| // packages/database/tsconfig.json (avant) | |
| { | |
| "extends": "../tsconfig/base.json", | |
| "compilerOptions": { "outDir": "dist", "rootDir": "." } | |
| // noEmit: true hérité — tsc ne produisait aucun fichier JS | |
| } | |
| ``` | |
| Le Dockerfile n'appelait que `prisma generate` pour ce package, jamais `tsc`. Même si on l'avait appelé, aucun fichier n'aurait été émis. | |
| **3. Imports avec extension `.js` incompatibles avec `moduleResolution: "node"`** | |
| ```typescript | |
| // packages/database/index.ts (avant) | |
| export * from './src/extension.js'; // résolution node16 requise | |
| export * from './src/context.js'; | |
| // packages/database/src/extension.ts (avant) | |
| import { getOrganizationId } from './context.js'; | |
| ``` | |
| La convention `import './foo.js'` pour référencer `foo.ts` est une pratique ESM/node16. Avec `moduleResolution: "node"` (celui des apps), TypeScript ne fait pas la substitution `.js` → `.ts` et la résolution échoue à la compilation comme au runtime. | |
| ### Correction | |
| **`packages/database/tsconfig.json`** — activer l'émission et aligner sur le style CommonJS des apps : | |
| ```json | |
| { | |
| "extends": "../tsconfig/base.json", | |
| "compilerOptions": { | |
| "outDir": "dist", | |
| "rootDir": ".", | |
| "noEmit": false, | |
| "declaration": true, | |
| "module": "CommonJS", | |
| "moduleResolution": "node", | |
| "esModuleInterop": true, | |
| "allowImportingTsExtensions": false, | |
| "target": "ES2020", | |
| "lib": ["ES2020"] | |
| }, | |
| "include": ["index.ts", "src/**/*"] | |
| } | |
| ``` | |
| **`packages/database/index.ts`** — supprimer les extensions `.js` : | |
| ```typescript | |
| // Avant | |
| export * from './src/extension.js'; | |
| export * from './src/context.js'; | |
| // Après | |
| export * from './src/extension'; | |
| export * from './src/context'; | |
| ``` | |
| **`packages/database/src/extension.ts`** — idem : | |
| ```typescript | |
| // Avant | |
| import { getOrganizationId } from './context.js'; | |
| // Après | |
| import { getOrganizationId } from './context'; | |
| ``` | |
| **`packages/database/package.json`** — ajouter le script build, pointer vers `dist/` : | |
| ```json | |
| { | |
| "main": "./dist/index.js", | |
| "types": "./dist/index.d.ts", | |
| "exports": { ".": "./dist/index.js", "./seed": "./src/seed.ts" }, | |
| "scripts": { | |
| "build": "tsc --build", | |
| ... | |
| } | |
| } | |
| ``` | |
| **`Dockerfile`** — compiler `@repo/database` après `generate` : | |
| ```dockerfile | |
| RUN pnpm --filter @repo/database generate | |
| RUN pnpm --filter @repo/database build # ajouté | |
| RUN pnpm --filter @repo/shared-types build | |
| RUN pnpm --filter @repo/prompts build | |
| ``` | |
| --- | |
| ## Ordre de build final dans le Dockerfile | |
| ``` | |
| prisma generate (@repo/database) | |
| → tsc (@repo/database) → dist/index.js + dist/src/*.js | |
| → tsc (@repo/shared-types) → dist/index.js | |
| → tsc (@repo/prompts) → dist/index.js | |
| → tsc (api) → dist/index.js (consomme les packages ci-dessus) | |
| → tsc (whatsapp-worker) → dist/index.js (idem) | |
| ``` | |
| PM2 démarre ensuite `apps/api/dist/index.js` et `apps/whatsapp-worker/dist/index.js` avec Node.js pur — aucune dépendance de dev requise au runtime. | |
| --- | |
| ## Leçons retenues | |
| | # | Règle | | |
| |---|---| | |
| | 1 | Tout package workspace consommé par une app compilée doit lui-même être compilé et pointer `main` vers `dist/`. | | |
| | 2 | Vérifier que `noEmit` est explicitement surchargé à `false` dans tout package qui doit émettre des fichiers JS. | | |
| | 3 | Ne jamais utiliser `--import tsx` ou `tsx` en production — compiler TypeScript à l'étape build, pas au runtime. | | |
| | 4 | Les imports avec extension `.js` dans un fichier `.ts` requièrent `moduleResolution: "node16"` ou `"nodenext"`. Avec `"node"`, utiliser des imports sans extension. | | |
| | 5 | Le Dockerfile doit compiler **tous** les packages et apps dans leur ordre de dépendance, pas seulement certains. | | |