# 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. |