Variables d'environnement

Frontend — .env.local (voir .env.local.example)

VariableRequisRôle
CONVEX_DEPLOYMENTCLIDéploiement ciblé par convex dev / convex run (écrit automatiquement)
VITE_CONVEX_URLouiURL du déploiement Convex ; main.tsx lève une erreur si absente
VITE_CONVEX_SITE_URLnonOrigine .convex.site servant les routes Better Auth (/api/auth/*), utilisée comme baseURL du client d'auth. Si absente, dérivée de VITE_CONVEX_URL (.convex.cloud → .convex.site)
VITE_GOOGLE_MAPS_API_KEYnonAutocomplétion d'adresse Google Places pour les pays hors France (restreinte au pays sélectionné). Sans clé, l'app utilise Photon (OpenStreetMap, sans clé) ; la France passe toujours par l'API BAN gouvernementale. Voir src/lib/countryInputs/address.tsx (registerAddressProvider).

En production, les VITE_* sont injectées au démarrage du conteneur : l'entrypoint génère /srv/env.js (window.__ENV__) à partir des variables d'environnement du conteneur — pas de rebuild par environnement.

Backend — environnement du déploiement Convex (bunx convex env set …)

VariableRequisRôle
SETUP_TOKENoui au premier démarrageJeton exigé par l'assistant de configuration initiale (/setup). Sans lui, l'assistant refuse de démarrer. Générer avec bunx convex env set SETUP_TOKEN $(openssl rand -hex 32). Voir Configuration initiale.
SITE_URLoui (auth)Origine(s) de la SPA. Sert à Better Auth (trustedOrigins + transport de session crossDomain, cible des redirections après connexion) et de base aux liens email/consentement (en secours de appConfig.appUrl). Plusieurs origines séparées par des virgules ; ex. https://crm.example.com. Sans elle, les connexions social/email sont refusées.
BETTER_AUTH_SECREToui en prodSecret de signature des sessions Better Auth. Générer avec bunx convex env set BETTER_AUTH_SECRET $(openssl rand -hex 32). Absent = secret éphémère (sessions invalidées à chaque déploiement).
BREVO_API_KEYoui pour les emails et SMSEnvoi des liens de connexion (email OTP, plugin Better Auth) et des campagnes email/SMS (même clé pour l'API SMS transactionnel)
FHIR_API_KEYnon (requis pour la vérif. RPPS)Clé de l'API FHIR Annuaire Santé (gateway.api.esante.gouv.fr), envoyée en en-tête ESANTE-API-KEY. Utilisée par l'action features/practitionerInfo/actions.verifyRpps pour vérifier un numéro RPPS (propriété personnalisée de type rpps). Sans elle, la vérification renvoie une erreur mais la saisie reste possible.
CRM_APP_URLdépréciéAncien nom de l'origine SPA — repli si SITE_URL est absente. Utiliser SITE_URL.
DEV_WHITELIST_EMAILSdevListe blanche emails (séparée par virgules) : tout envoi email Brevo vers un destinataire hors liste est bloqué ; vide = tout passe (comportement prod)
DEV_WHITELIST_PHONESdevListe blanche numéros (séparée par virgules, format E.164 ex. +33612345678) : tout envoi SMS Brevo vers un numéro hors liste est bloqué ; vide = tout passe (comportement prod)
EMAIL_SENDER_NAMEprodNom d'expéditeur des emails (défaut CRM) — secours si non défini dans la config runtime
EMAIL_SENDER_EMAILprodAdresse d'expéditeur des emails (défaut noreply@example.com) — le domaine doit être un expéditeur Brevo vérifié ; secours si non défini dans la config runtime
BREVO_SMS_SENDERoui pour les SMSNom d'expéditeur affiché sur les SMS (ID alphanumérique Brevo, ≤ 11 caractères ; défaut CRM)
BREVO_WEBHOOK_SECRETnon (requis pour les webhooks)Secret des webhooks Brevo au niveau compte (/webhooks/brevo/email et /webhooks/brevo/sms). Envoyé dans l'en-tête x-webhook-secret fixé à l'enregistrement (registerBrevoEmailWebhook / registerBrevoSmsWebhook) — jamais dans l'URL. Générer avec bunx convex env set BREVO_WEBHOOK_SECRET $(openssl rand -hex 32). Absent = webhooks désactivés.
BREVO_SMS_WEBHOOK_SECRETnon (recommandé pour le STOP SMS)Secret dédié du webhook SMS par message (webUrl posé sur chaque envoi) : les webhooks par message de Brevo ne peuvent pas envoyer d'en-tête, ce secret voyage donc dans l'URL — d'où une valeur distincte, révocable sans toucher au secret de compte. Repli sur BREVO_WEBHOOK_SECRET si absente. Générer avec bunx convex env set BREVO_SMS_WEBHOOK_SECRET $(openssl rand -hex 32).
SECRETS_KEYnon (oui en hébergement)Clé maître, 32 octets en hexadécimal, qui chiffre les secrets stockés dans appConfig (clé et secret webhook Brevo, mot de passe SMTP, secrets des fournisseurs sociaux et SSO) en AES-256-GCM avant écriture ; ils ne sont déchiffrés qu'au point d'usage, jamais renvoyés au navigateur. Absente : les secrets restent en clair, avec un avertissement dans les journaux au premier écrit. Générer avec bunx convex env set SECRETS_KEY $(openssl rand -hex 32), puis chiffrer l'existant avec bunx convex run migrations:run '{"fn":"migrations:encryptAppConfigSecrets"}'. Rotation : docs/secrets.md.
SECRETS_KEY_NEXTnonNouvelle clé pendant une rotation : les écritures l'utilisent, les lectures essaient les deux. Voir docs/secrets.md.
CONNECTOR_GOOGLE_CLIENT_ID, CONNECTOR_GOOGLE_CLIENT_SECRET, CONNECTOR_MICROSOFT_CLIENT_ID, CONNECTOR_MICROSOFT_CLIENT_SECRETnonApplication OAuth « gérée » d'un fournisseur de connecteurs, fournie par l'hébergeur : utilisée quand l'organisation n'a pas configuré et activé la sienne dans Réglages → Intégrations. Voir docs/connectors.md.
OAUTH_CALLBACK_BASEnonOrigine d'un répartiteur de callback OAuth : quand elle est définie, l'adresse de redirection des connecteurs devient ${OAUTH_CALLBACK_BASE}/oauth/callback, et le répartiteur renvoie le navigateur vers /connectors/callback de ce déploiement. Le code est toujours échangé ici, jamais par le répartiteur. Absente : ${CONVEX_SITE_URL}/connectors/callback.
OAUTH_CALLBACK_TENANTavec OAUTH_CALLBACK_BASEIdentifiant de ce déploiement pour le répartiteur, placé dans l'état OAuth signé.
OAUTH_STATE_SECRETavec OAUTH_CALLBACK_BASEClé de signature de l'état OAuth des connecteurs, partagée avec le répartiteur. Absente : clé dérivée de BETTER_AUTH_SECRET.
API_KEY_HASH_SALTnonSel du hachage SHA-256 des secrets de clés d'API (API REST publique /api/v1/, réglages → Clés d'API). Repli sur une valeur par défaut si absente — définir en prod avant de créer la première clé pour durcir les lignes apiKeys en cas de fuite de la base. Le sel entre dans chaque hachage : le changer invalide toutes les clés existantes (les secrets font 24 octets aléatoires, il ne se tourne donc jamais en routine). Générer avec bunx convex env set API_KEY_HASH_SALT $(openssl rand -hex 16).
FORM_IP_HASH_SALTnonClé du HMAC-SHA256 qui pseudonymise les adresses IP stockées avec les soumissions de formulaires publics (formSubmissions.ipHash). Absente, une clé dérivée de BETTER_AUTH_SECRET sert. Générer avec bunx convex env set FORM_IP_HASH_SALT $(openssl rand -hex 16).

La plupart des réglages ci-dessus (URL, expéditeur) et les identifiants des fournisseurs sociaux (Google…) sont stockés dans la table Convex singleton appConfig, renseignée par l'assistant de configuration initiale. Les variables d'environnement restantes servent de secours (EMAIL_SENDER_*) ou de secret/config déploiement (SETUP_TOKEN, BREVO_API_KEY, SITE_URL, BETTER_AUTH_SECRET). CONVEX_SITE_URL est injectée automatiquement par Convex et sert d'origine aux routes/callbacks Better Auth — rien à définir.

Rotation des secrets webhook Brevo

Les deux secrets se tournent indépendamment, sans interruption de service :

  1. Secret de compte (BREVO_WEBHOOK_SECRET) — en-têtes des webhooks e-mail et SMS entrants :
    bunx convex env set BREVO_WEBHOOK_SECRET $(openssl rand -hex 32) --prod
    bunx convex run features/campaigns/actions:registerBrevoEmailWebhook --prod
    bunx convex run features/campaigns/actions:registerBrevoSmsWebhook --prod
    L'enregistrement met à jour l'en-tête x-webhook-secret chez Brevo ; les routes comparent en temps constant et acceptent immédiatement la nouvelle valeur.
  2. Secret SMS par message (BREVO_SMS_WEBHOOK_SECRET) — présent dans le webUrl de chaque SMS envoyé :
    bunx convex env set BREVO_SMS_WEBHOOK_SECRET $(openssl rand -hex 32) --prod
    Prend effet pour les envois suivants. Attention : les événements des SMS déjà partis porteront encore l'ancien webUrl — tourner ce secret hors d'une campagne en cours, ou accepter la perte des événements tardifs (STOP compris) des messages déjà envoyés.