Architecture technique
CEL est une application full-stack pour gérer des ligues d’esport autour d’EA Sports FC.
Vue d’ensemble
web/ : frontend Next.js 16 / React 19.
convex/ : backend Convex self-hosted.
- Scripts racine : build, tests Convex, stack self-hosted, backups/restores, Infisical.
Qualité statique
Depuis la racine, npm run lint contrôle à la fois le backend Convex et
web/src. Les deux typechecks restent explicites car ils utilisent des projets
TypeScript distincts : npm run typecheck:convex puis cd web && npm run typecheck. npx knip analyse séparément les workspaces racine et web; son
entrée web/design-system.entry.tsx protège les primitives réellement exposées
à /design-sync même lorsqu’elles ne sont pas importées par l’application.
Stack vérifiée
- Next.js 16
- React 19
- TypeScript
- TailwindCSS v4
- Shadcn UI / Radix UI
- TanStack Table
- React Hook Form + Zod
- Convex React
- TipTap
- Algolia / React InstantSearch
- PostHog
- Framer Motion
- Recharts
- React Three Fiber / Drei
CI/CD réel et séparation des responsabilités
Workflows actifs
Règle d’architecture CI/CD
pipeline.yml = qualité et conformité d’intégration, pas de déploiement.
Le job lint-convex exécute test:environment-contract puis env:check
ainsi que les gates d’architecture avant ESLint et le typecheck.
Le job GitHub-hosted e2e-web-smoke exécute les scénarios navigateur
autonomes sans secret. e2e-web-auth démarre un Convex isolé et vérifie une
vraie connexion avec sa redirection. Les deux doivent réussir avant
build-web et héritent des trois origines publiques locales définies au
niveau du workflow. Les jobs Web qui importent convex/_generated installent
les dépendances verrouillées de la racine puis celles de web/ : la résolution
des modules part du fichier généré situé dans convex/, pas du seul projet Web.
deploy.yml = exécution manuelle de production.
- Pendant le build de l’image
web, l’upload des source maps PostHog est best-effort :
une erreur réseau PostHog ne bloque pas le déploiement, et les .map client sont
supprimées avant l’image runtime.
release.yml = versioning uniquement, sans déclenchement du déploiement.
docs.yml = validation documentation, sans impact applicatif.
Frontend web/
Les hooks de production utilisent directement convex/react pour les données
réactives. Ils n’exposent pas de compatibilité de cache factice (query-client,
useQueryClient, refetch sans effet ou états d’erreur/chargement codés en
dur). Le gate npm run test:data-adapter-boundary protège cette frontière dans
le pre-push et la CI.
Scripts principaux :
Frontière E2E
La suite navigateur possède une seule configuration,
web/playwright.config.ts, et une seule racine de specs, web/e2e/. Sans
E2E_BASE_URL, Playwright possède le serveur Next local et refuse de réutiliser
un processus déjà actif ; cela empêche de valider par erreur le checkout d’un
autre worktree. E2E_BASE_URL active explicitement le mode serveur externe.
Les tags séparent les niveaux de prérequis : @smoke ne demande ni secret ni
backend alimenté, @auth demande un compte de test, @live-data demande des
données backend et @write annonce des mutations. Avec
E2E_REQUIRE_AUTH_FIXTURE=true, l’absence des identifiants d’authentification
est une erreur, jamais un skip silencieux.
Frontière des images distantes
L’optimiseur d’images Next.js accepte uniquement les uploads servis sous
/uploads/** par l’origine exacte de NEXT_PUBLIC_CONVEX_AUTH_API_URL, les
avatars Google sous lh3.googleusercontent.com/a/** et les drapeaux de
flagcdn.com. L’origine des uploads est dérivée du contrat d’environnement :
aucun port local historique n’est codé en dur. Les redirections distantes sont
désactivées et l’accès aux IP locales n’est permis qu’hors production.
Backend convex/
Scripts principaux :
Le backend et le dashboard du Compose Convex local utilisent
restart: unless-stopped afin de revenir automatiquement après un redémarrage
de Docker ou de WSL, sauf arrêt explicite par l’utilisateur.
Les routes HTTP personnalisées sont inventoriées dans
api-http.md.
Frontière d’identité
convex/lib/actor.ts est le socle neutre de résolution de l’acteur.
- Une fonction Convex publique dérive toujours l’acteur de
ctx.auth; le
frontend et les routes HTTP ne lui transmettent ni actorUserId ni
requesterUserId.
- Les champs d’acteur des fonctions internes sont réservés à la causalité des
jobs et à la provenance d’audit. Ils ne servent jamais à autoriser une
requête publique.
- Les domaines réexportent les gardes neutres depuis leur
_shared.ts afin de
ne pas dépendre de l’implémentation d’identité d’un autre domaine.
npm run test:identity-boundary empêche la réintroduction d’un identifiant
d’acteur fourni par un appelant public.
Frontières des domaines Convex
Les primitives utilisées par plusieurs domaines vivent dans convex/lib/ :
la timezone applicative, la construction du texte d’audit, les recherches de
clubs/profils et les contrôles d’accès aux clubs. admin, competition et
social peuvent conserver des intégrations métier explicites (annonces,
notifications, traitements planifiés), mais ne récupèrent pas ces primitives
depuis le _shared.ts ou l’adapter d’un autre domaine.
npm run test:domain-boundary résout canoniquement chaque import TypeScript de
production et refuse par défaut toute dépendance entre ces trois domaines. Son
allowlist nomme précisément les fichiers, modules et symboles des intégrations
métier actuelles ; un nouveau helper reste donc interdit même s’il est ajouté à
un module déjà autorisé. Le gate scanne aussi convex/lib/ et lui interdit sans
exception d’importer admin, competition ou social, afin que le socle
neutre ne puisse pas servir de bridge entre domaines.
Secrets et environment
config/environment-contract.mjs est la source déclarative des profils, des
consommateurs, de la visibilité et des relations entre variables. Les gardes
suivantes couvrent le code Convex/Web, les Dockerfiles, les Compose et les
fichiers d’exemple :
Le repo fournit aussi les scripts Infisical :
convex:env:sync valide le profil local puis pousse exactement les clés
déclarées pour convex-runtime. Les listes ad hoc et le scan de
process.env.* ne participent plus à la synchronisation.
Production : composants réels
Le stack de production défini dans docker-compose.prod.yml contient :
postgres
pgbouncer
postgres-backup
convex-backend
convex-dashboard
web
cloudflared
portainer
Invariants connus :
- frontend canonique :
https://cel-eleague.com
- Convex auth HTTP :
https://api.cel-eleague.com/api
- déploiement sur
main/master dans le workflow manuel
- runner de déploiement : self-hosted macOS dans
deploy.yml
Crons et jobs actifs (Convex)
Enregistrement piloté par convex/admin/crons.ts (initCrons) :
match-auto-validation → 0 0 * * *
match-ea-score-sync → */30 20-23 * * 1-5
person-sanction-lifecycle-maintenance → */15 * * * *
transfer-lifecycle-maintenance → */15 * * * *
stripe-premium-entitlements-reconcile → 0 2 * * *
initCrons retire explicitement les IDs suivants :
user-ban-expiration-cleanup
totw-weekly
Autres points opérables via API admin Convex :
listCronConfigs
listCronExecutions
upsertCronConfig
triggerCron
Backups et restauration
Backups Convex :
Restauration Convex :
Comportements observés :
- snapshots dans
backups/convex/<timestamp>/convex-export.zip
- symlink
backups/convex/latest
convex:restore = remplacement par défaut (--replace-all)
--append à n’utiliser que si lineage/tableaux compatibles
Développement local
Prérequis :
- Node.js 22+
- npm
- PostgreSQL 16+ ou Docker pour le développement ; la production utilise
PostgreSQL 18
Commandes de montée en charge :
Frontend local :
La première commande préserve un éventuel fichier .env existant. Le
postinstall crée web/.env.local comme lien vers ce fichier racine. Il ne faut
pas copier un exemple vers ce lien, car la copie écraserait .env.
Frontend via Portless : https://cel.localhost.
Les deux surfaces Convex browser-facing sont
https://cel-convex.localhost et https://cel-convex-site.localhost/api.
Profil direct explicite : npm run dev:port puis http://localhost:3000, avec
NEXT_PUBLIC_APP_URL et les deux URLs Convex configurées sur les ports hôte de
.env.convex.
Écarts opératoires restant à traiter
- Les scripts
healthcheck.sh, monitor-prod.sh et monitor-prod-rich.sh
contiennent encore des références à une ancienne stack de monitoring absente
du compose de production.
- Certains chemins de restauration et noms de conteneurs historiques doivent
être revérifiés avant exécution.
Validation runtime externe requise :
- Les domaines exacts exposés sur le tunnel/public et le schéma d’alerting
externe ne sont pas testés dans une session d’exécution ici.
Last modified on August 9, 2026