Production
Le backend se déploie séparément : bunx convex deploy (+ convex env set
sur le déploiement de prod). Le frontend est une image Docker autonome :
docker build -t fabulous-crm .
docker run -d -p 8099:80 -e VITE_CONVEX_URL=https://<deployment>.convex.cloud fabulous-crm
Image multi-stage : build bun (tsc + vite) → caddy:2-alpine servant dist/
(cache immutable sur /assets, no-cache sur le HTML et env.js, fallback SPA).
Après une mise à jour
bunx convex run migrations:backfillCampaignStats --produne fois, à la première version qui tient les compteurs d'une campagne (en attente, ignorés, ouverts, cliqués…) : chaque campagne antérieure est comptée depuis ses envois, par pages planifiées (environ 500 envois par page). À lancer à tout moment, campagne en cours d'envoi et événements du fournisseur compris : un envoi qui change pendant le comptage n'est compté qu'une fois. Jusque-là, une campagne antérieure affiche des zéros.- Si les compteurs d'une campagne semblent faux :
bunx convex run features/campaigns/internal:recountCampaignStats '{"campaignId":"<id>"}' --prodla recompte depuis ses envois, à tout moment et autant de fois que voulu. bunx convex run features/workflows/internal:listWorkflowsToFix --prodliste les workflows actifs ou en pause que l'activation refuserait avec les règles de la version déployée. Un workflow en faute continue de tourner, mais une fois mis en pause il ne se réactive plus avant d'être corrigé : mieux vaut le corriger avant.- Chaque fonction Convex vérifie à l'exécution ce qu'elle rend (
returns). Après un déploiement, uneReturnsValidationErrordans les logs (bunx convex logs --prod) désigne une fonction qui rend un champ que son validator ne déclare pas.
Déploiement local via Docker (build + run)
Procédure complète pour lancer l'image en local et arriver jusqu'à l'assistant
/setup.
Renseigner l'environnement du déploiement Convex (côté serveur — pas l'environnement du conteneur),
SETUP_TOKENen tête car exigé au premier démarrage :bunx convex env set SETUP_TOKEN $(openssl rand -hex 32) # requis au 1er démarrage bunx convex env set SITE_URL https://crm.example.com # requis (auth) : origine SPA + base des liens email/consentement bunx convex env set BETTER_AUTH_SECRET $(openssl rand -hex 32) # requis en prod : signature des sessions bunx convex env set BREVO_API_KEY <clé> # requis pour les emails et SMS bunx convex env set EMAIL_SENDER_EMAIL noreply@example.com # expéditeur Brevo vérifié bunx convex env set EMAIL_SENDER_NAME "CRM" bunx convex env set BREVO_SMS_SENDER "CRM" # requis pour les SMS (≤ 11 car. alphanum.) bunx convex env set BREVO_WEBHOOK_SECRET $(openssl rand -hex 32) # optionnel : désinscription SMS via réponse STOP bunx convex env set FHIR_API_KEY <clé> # optionnel : vérif. RPPS (Annuaire Santé)Liste complète : Variables d'environnement — Backend.
Construire l'image dev (
Dockerfile.local, baseoven/bun— c'est elle qui embarquebun; l'image de productioncaddy:2-alpinen'a pasbun) :docker build -f Dockerfile.local --build-arg UID=$(id -u) --build-arg GID=$(id -g) -t wap-crm:dev .Lancer le conteneur (nom
wap-crm-dev, réseauproxy, dossier projet monté en volume) :docker rm -f wap-crm-dev || true && \ docker run -d --name wap-crm-dev --network proxy -v "$PWD":/app wap-crm:devAucun port publié : le reverse proxy de l'hôte route vers
wap-crm-dev:4202.Entrer dans le conteneur, installer, démarrer (l'entrypoint est inerte,
tail -f) :docker exec -it wap-crm-dev bash bun install bun run dev # convex dev + vite (`bunx convex login` une fois)Les
VITE_*(dontVITE_CONVEX_URL) sont lues depuis.env.localmonté dans le conteneur — pas d'injection-e, qui ne sert qu'à l'image de production.Ouvrir
/setup, saisir leSETUP_TOKEN, terminer l'assistant : le premier administrateur est créé et connecté, puis l'assistant se verrouille.
Deux niveaux d'environnement distincts. Le conteneur dev lit les
VITE_*depuis.env.local(monté en volume).SETUP_TOKENet les autres secrets (BREVO_API_KEY, expéditeur…) vivent sur le déploiement Convex (bunx convex env set) — les placer dans l'environnement du conteneur n'a aucun effet, car l'assistant vérifie le jeton côté Convex.
Caddy & reverse proxy
Le Caddy embarqué dans l'image (docker/Caddyfile) n'est pas un reverse
proxy : c'est un simple serveur statique sur :80 (fallback SPA vers
index.html, Cache-Control: immutable sur /assets/*, no-cache sur le HTML
et env.js, en-têtes de sécurité). Rien n'est proxifié car le navigateur parle
directement au déploiement Convex (VITE_CONVEX_URL) — aucun trafic API ne
transite par le conteneur.
Pour exposer le conteneur derrière le reverse proxy de l'hôte (TLS, nom de
domaine), il suffit de pointer vers le port publié (8099 dans l'exemple
ci-dessus). Avec Caddy sur l'hôte :
crm.example.com {
reverse_proxy 127.0.0.1:8099
}
Aucune directive particulière n'est nécessaire : pas de WebSocket ni de chemin
d'API côté conteneur (le WebSocket Convex va directement du navigateur vers
*.convex.cloud). Penser à aligner SITE_URL (env Convex) sur l'URL
publique pour que l'authentification, les liens de connexion et de consentement
soient corrects.