Topologie de Déploiement en Production
🌐 Vue d'ensemble
| Composant | Hébergeur | Déclenchement | Adresse publique |
|---|---|---|---|
Site (apps/web) | Vercel, région cdg1 (Paris) | Projet Vercel | clair.vote |
Documentation (docs) | Vercel (configuration dans docs/vercel.json) | Projet Vercel | docs.clair.vote |
API (apps/api) | Railway, service « CLAIR » | Déploiement depuis la branche master de accelaire/CLAIR | api.clair.vote |
| Batch nocturne | Railway, service « Ingestion nightly » | Cron 0 3 * * * (UTC), depuis master | |
| Batch intraday | Railway, service « Ingestion intraday » | Cron 0 6-21/2 * * * (UTC), depuis master | |
| PostgreSQL 17 | Railway, service « Postgres » (image postgres-ssl:17) | ||
| Redis 8.2 | Railway, service « Redis » (image redis:8.2.1) | ||
| DNS | Dynadot |
L'API et les deux services d'ingestion joignent PostgreSQL par le réseau privé de Railway (*.railway.internal), comme l'API pour Redis. L'ingestion n'utilise pas Redis.
Une fusion sur master redéploie donc l'API et les deux services d'ingestion. Les migrations de base, elles, ne suivent pas (voir plus bas).
🖥️ Site : Vercel
La configuration versionnée est dans apps/web/vercel.json :
- région
cdg1; - en-têtes de sécurité sur toutes les pages (
X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy,Permissions-Policy) ; - cache au CDN de Vercel (
Vercel-CDN-Cache-Control) : 10 min sur les pages de liste (/deputes,/scrutins,/sujets…), 1 h sur les pages de détail, avecstale-while-revalidatede 24 h ; 1 min sur/senatoriales-2026(10 min destale-while-revalidate), pour les résultats. Le navigateur, lui, revalide à chaque visite (max-age=0).
La version de Node vient du champ engines de apps/web/package.json (24.x), qui prime sur le réglage du projet Vercel : la preview d'une pull request se construit donc avec la version que déclare la branche.
Variables lues par le code du site :
| Variable | Rôle |
|---|---|
NEXT_PUBLIC_API_URL | Adresse de l'API (défaut http://localhost:3001) |
NEXT_PUBLIC_APP_URL | Adresse publique du site (défaut https://clair.vote), pour les liens absolus et le SEO |
NEXT_PUBLIC_PLAUSIBLE_DOMAIN | Domaine déclaré à Plausible ; sans lui, aucune mesure d'audience n'est chargée |
NEXT_PUBLIC_PLAUSIBLE_SCRIPT_URLS | Scripts Plausible à charger, séparés par des virgules |
CLAIR_INTERNAL_SECRET | Secret partagé avec l'API, utilisé côté serveur uniquement (rendu serveur, sitemap, proxy /api/v1). Ne jamais le préfixer par NEXT_PUBLIC_ |
API_URL et CLAIR_INTERNAL_SECRET sont déclarées dans globalEnv de turbo.json : sans cela, Turborepo ne les transmettrait pas au build sur Vercel. Le formulaire de don HelloAsso est intégré par une adresse écrite dans le code (app/soutenir/page.tsx) : aucune variable n'est nécessaire.
📚 Documentation : Vercel
docs/vercel.json déclare le framework (vite) et la commande de build (pnpm build) ; la version de Node vient, comme pour le site, des engines de docs/package.json (24.x). Le build télécharge la spécification OpenAPI de l'API de production, régénère la navigation, construit le site statique, puis y injecte les balises Open Graph. La référence de l'API affichée par la doc est donc celle de l'API en production au moment du build. Comme une fusion sur master déploie la doc (Vercel, environ 30 s) en même temps que l'API (Railway, environ 1 min 30), le build de production de la doc attend que l'API ait changé de version : il lit le commit qu'elle sert sur /health (fourni par Railway dans RAILWAY_GIT_COMMIT_SHA) et patiente tant que c'est le commit précédent, 5 minutes au plus. Si l'API n'annonce pas de commit ou ne change pas à temps, la doc se construit avec la spécification en ligne, et le log du build le signale ; un nouveau build de la doc rattrape alors l'écart. Les previews de pull request n'attendent pas.
🌍 DNS
Zone gérée chez Dynadot :
Code
⚡ API : Railway
- Image construite depuis le
Dockerfileà la racine du dépôt (node:24-alpine, build pnpm, génération du client Prisma). - Démarrage :
node --max-old-space-size=4096 dist/index.js, port 3001. - Healthcheck Railway :
/health. Redémarrage en cas d'échec (ON_FAILURE). - Au démarrage en production, l'API préchauffe son cache Redis (voir Limites & Cache).
Variables lues par le code de l'API :
| Variable | Rôle |
|---|---|
DATABASE_URL, REDIS_URL | Base et cache |
NODE_ENV | production en prod |
PORT, HOST | Écoute (défauts 3001 et 0.0.0.0) |
LOG_LEVEL | Niveau de log |
CORS_ORIGIN | Origines autorisées en production, séparées par des virgules |
CLAIR_INTERNAL_SECRET | Secret partagé avec le site et l'ingestion (voir Limites & Cache) |
FEEDBACK_DISCORD_WEBHOOK_URL | Notification des retours anonymes |
PUBLIC_APP_URL | Lien vers le site dans la notification de retour (défaut https://clair.vote) |
PLAUSIBLE_EVENTS_URL, PLAUSIBLE_API_DOMAIN | Mesure d'usage de l'API (voir Vue d'ensemble de l'API) ; inactive sans PLAUSIBLE_EVENTS_URL |
API_URL | Hors production seulement : adresse annoncée par la spécification OpenAPI (défaut http://localhost:3001). En production, elle annonce toujours https://api.clair.vote |
SENATORIALES_HORLOGE | Rejouer une soirée électorale à une heure simulée ; ignorée en production |
apps/api/railway.toml décrit ce déploiement mais n'est pas lu par Railway : la configuration fait foi dans le tableau de bord.
📥 Ingestion : Railway
Les deux services sont construits depuis services/ingestion/Dockerfile (node:24-alpine), seul à installer poppler-utils (pdftotext), indispensable pour lire les comptes rendus de commission de l'Assemblée. Le conteneur tourne avec TZ=Europe/Paris, mais les crons Railway s'expriment en UTC.
| Service | Cron (UTC) | Commande |
|---|---|---|
| Ingestion nightly | 0 3 * * * : 5 h à Paris en heure d'été, 4 h en heure d'hiver | node --max-old-space-size=6144 services/ingestion/dist/cli.js smart-sync --all (le CMD du Dockerfile) |
| Ingestion intraday | 0 6-21/2 * * * : de 8 h à 22 h à Paris en heure d'été | node services/ingestion/dist/cli.js sync-intraday |
Ni l'un ni l'autre n'est relancé en cas d'échec (politique de redémarrage NEVER) : un passage raté attend le suivant. Le batch nocturne ne sort d'ailleurs en erreur que si aucune source n'a pu être synchronisée (voir Orchestrateur d'ingestion).
Variables lues par le code de l'ingestion : DATABASE_URL, LOG_LEVEL, NODE_ENV, MISTRAL_API_KEY, TAVILY_API_KEY, API_URL et CLAIR_INTERNAL_SECRET (pour reconstruire le cache de la page d'accueil en fin de batch). Le service intraday n'utilise que DATABASE_URL, LOG_LEVEL et NODE_ENV.
services/ingestion/railway.toml et railway.intraday.toml recopient la configuration du tableau de bord pour la documenter ; Railway ne les lit pas.
🗄️ Base de données
- Les migrations ne sont pas appliquées automatiquement au déploiement. Elles s'appliquent avec
prisma migrate deploy, lancé depuisapps/apiavec leDATABASE_URLde production (voir Modèle Prisma). Une migration dont dépend le code doit donc être appliquée avant la fusion surmaster. - La base est mise à jour chaque nuit par le batch nocturne, et dans la journée par le batch intraday.
🔁 CI : GitHub Actions
.github/workflows/ci.yml, sur chaque push et pull request vers master (ou main), avec la version de Node lue dans .nvmrc :
- lint-and-test : installation, génération du client Prisma,
pnpm lint,pnpm test, couverture de l'API envoyée à Codecov sur les pull requests. Letype-checkest désactivé (erreurs préexistantes dans l'ingestion et le site). - build : démarre PostgreSQL 16 et Redis 7, applique le schéma par
prisma db pushpuis les triggers (db:triggers), construit et lance l'API, puis construit tout le monorepo. Le build du site prérend/sitemap.xml, qui interroge l'API : c'est ce qui impose une API réelle pendant la CI.
La CI ne déploie rien.