edtech / docs /railway_deployment_crash_postmortem.md
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. |