Variables d'environnement
Cette fiche recense les variables d'environnement lues par TimePick au démarrage : serveur (server/.env) et client (client/.env — build-time uniquement). Elle ne couvre pas le pas-à-pas d'installation (voir Partie 1 — Installation) ni le détail par fournisseur SMTP ou HTTP (voir SMTP — Fournisseurs).
Deux catégories de réglages
TimePick sépare la configuration d'infrastructure (variables d'environnement, lues une fois au démarrage) des réglages applicatifs (stockés en base de données, modifiables à chaud depuis l'écran Paramètres). Cette distinction revient tout au long de la fiche — voir Paramètres in-app.
Variables serveur (server/.env)
Le fichier server/.env.example du dépôt sert de modèle ; le copier vers server/.env puis renseigner les valeurs.
| Variable | Rôle | Requise | Défaut |
|---|---|---|---|
DATABASE_URL | Chaîne de connexion PostgreSQL (postgresql://user:password@hôte:5432/timepick) | Oui | — |
PORT | Port d'écoute du serveur Express | Non | 3000 |
JWT_SECRET | Secret de signature des JWT (sessions admin) | Non | généré automatiquement (voir ci-dessous) |
APP_URL | URL de base du frontend, utilisée pour construire les liens de connexion (magic links) et les boutons d'action des emails | Non | http://localhost:5173 |
EMAIL_FROM | Adresse d'expéditeur par défaut, utilisée quand aucune adresse SMTP n'est configurée en base | Non | noreply@example.com |
ENCRYPTION_KEY | Clé de chiffrement (32 octets = 64 caractères hexadécimaux), utilisée pour chiffrer le mot de passe SMTP stocké en base | Non | généré automatiquement (voir ci-dessous) |
DATA_DIR | Répertoire où sont stockés les secrets générés automatiquement (encryption.key, jwt.secret) | Non | <server>/data (/app/server/data sous Docker) |
STORAGE_DRIVER | Stockage des images uploadées dans l'éditeur d'emails : local (disque) ou s3 (object storage S3-compatible) — voir la section dédiée | Non | local |
JWT_SECRET et ENCRYPTION_KEY sont optionnels
Ces deux secrets sont générés automatiquement au premier démarrage s'ils sont absents de l'environnement : 32 octets aléatoires (64 caractères hexadécimaux), écrits dans DATA_DIR (jwt.secret, encryption.key, permissions 0600). Ils sont recommandés en variables d'environnement en production — voir la section volume persistant ci-dessous pour comprendre pourquoi.
Ordre de priorité : variable d'environnement > fichier généré. Si la variable d'environnement est définie et valide, elle est toujours utilisée, et un éventuel fichier généré sur disque est ignoré (un avertissement est journalisé). C'est ainsi que l'on « promeut » une clé auto-générée en variable d'environnement gérée.
ENCRYPTION_KEY sert au chiffrement du mot de passe SMTP en base (AES-256-GCM côté serveur). Lorsqu'elle est fournie via l'environnement, elle doit faire exactement 64 caractères hexadécimaux : si elle est définie mais mal formée, le serveur refuse de démarrer (échec rapide, avec un message explicite), plutôt que d'échouer plus tard au premier enregistrement SMTP. JWT_SECRET accepte n'importe quelle valeur non vide fournie via l'environnement.
Volume persistant obligatoire en production
Le répertoire DATA_DIR (server/data par défaut) doit être placé sur un volume persistant en production, au même titre que server/uploads. En Docker, ajoutez -v timepick-data:/app/server/data à côté du volume uploads.
Sans volume persistant, les secrets sont régénérés à chaque recréation du conteneur (par exemple à chaque redéploiement Coolify) : le mot de passe SMTP enregistré ne peut plus être déchiffré, et toutes les sessions ainsi que les magic links en attente sont invalidés — une panne d'envoi d'emails silencieuse, pouvant aller jusqu'au verrouillage des comptes administrateurs. Le recours de dernier ressort est alors les codes de récupération de connexion d'urgence.
Générer les secrets manuellement (optionnel)
Par défaut, il n'est plus nécessaire de générer JWT_SECRET et ENCRYPTION_KEY : le serveur s'en charge automatiquement au premier démarrage (voir ci-dessus). Pour gérer ces secrets vous-même — méthode recommandée en production —, générez-les avec openssl, un bloc de commande à la fois :
openssl rand -hex 32Exécuter la commande une première fois pour JWT_SECRET :
openssl rand -hex 32Puis une seconde fois, séparément, pour ENCRYPTION_KEY (ne jamais réutiliser la même valeur pour les deux) :
openssl rand -hex 32Chaque exécution produit une chaîne de 64 caractères hexadécimaux — coller le résultat tel quel dans server/.env.
Restaurer une clé de chiffrement après une restauration de base
Restauration d'une sauvegarde PostgreSQL
Avant de restaurer une sauvegarde PostgreSQL sur une instance neuve, remettez en place la clé de chiffrement sauvegardée — en variable d'environnement ENCRYPTION_KEY, ou en restaurant le fichier server/data/encryption.key — avant de démarrer le serveur sur cette base restaurée. Sans cela, le mot de passe SMTP chiffré présent dans la base restaurée ne peut plus être déchiffré.
Sauvegardez toujours la clé de chiffrement séparément des dumps de base de données : une clé stockée à côté du texte chiffré qu'elle protège n'apporte que de l'obfuscation, pas du chiffrement.
Chaque conteneur qui ne voit ni variables d'environnement ni fichiers déjà générés (pas de volume data partagé, ou système de fichiers éphémère) génère sa propre paire de secrets : les sessions et les données chiffrées deviennent illisibles d'une instance à l'autre. Dès que plusieurs conteneurs coexistent sans ce stockage partagé, fournissez les secrets en variables d'environnement. Un régime permanent à plusieurs instances n'est de toute façon pas pris en charge aujourd'hui (limitations de débit maintenues en mémoire, par processus) — voir la contrainte « 1 seule instance » dans Déployer sur un PaaS.
Migrer une installation existante : secrets disque → environnement
Sur une installation déjà en service qui n'a jamais fourni ces deux secrets via l'environnement, ils ont été auto-générés sur disque au premier démarrage, et ENCRYPTION_KEY chiffre le mot de passe SMTP (et la clé API du fournisseur d'email, le cas échéant) stocké en base. Les promouvoir en variables d'environnement supprime la dépendance au volume data — à condition de promouvoir les valeurs existantes, jamais des neuves.
Extraire les valeurs AVANT de basculer
Poser une ENCRYPTION_KEY différente de celle qui a chiffré la base n'empêche pas le serveur de démarrer, mais rend le mot de passe SMTP indéchiffrable : plus aucun email ne part (la demande de lien de connexion répond alors 503) et les administrateurs se retrouvent verrouillés — recours de dernier ressort : les codes de secours. Toujours extraire d'abord les valeurs en place, puis les reposer à l'identique.
Résumé de la procédure :
- Sauvegarder : dump PostgreSQL frais, copie hors site des fichiers
jwt.secretetencryption.keydu répertoireDATA_DIR, codes de secours à portée de main. Noter l'empreinte de la clé affichée dans Profil administrateur → carte « Clé de chiffrement » (voir Paramètres in-app). - Extraire les valeurs en place :
ENCRYPTION_KEY— bouton de révélation de cette même carte (disponible tant que la clé provient du fichier), ou lecture directe :docker exec <conteneur> cat /app/server/data/encryption.key;JWT_SECRET— lecture du fichier uniquement :docker exec <conteneur> cat /app/server/data/jwt.secret(pas de révélation via l'interface).
- Poser les deux variables d'environnement avec les valeurs extraites, puis redéployer. Conserver le volume
dataintact : il sert de retour arrière (retirer les deux variables et redéployer restaure l'état initial à l'identique). - Vérifier : la carte « Clé de chiffrement » indique désormais que la clé est gérée via une variable d'environnement, avec la même empreinte qu'à l'étape 1 ; le badge du serveur d'email reste « Opérationnel » ; une déconnexion suivie d'une reconnexion par lien de connexion aboutit. En cas d'erreur de clé : badge « Non joignable » et demande de lien en
503— retirer les variables (retour arrière) et reprendre à l'étape 2.
Le bouton « Tester la connexion » n'est pas une preuve
En mode SMTP, ce bouton teste le mot de passe saisi dans le formulaire, jamais le secret chiffré stocké en base : il peut afficher « Connexion réussie » alors même que la clé de chiffrement posée est erronée. Les témoins fiables sont le badge d'état du serveur d'email et la réception réelle d'un lien de connexion.
Statut de vérification
Cette procédure (bascule, test négatif avec une clé erronée, retour arrière) a été validée de bout en bout le 2026-07-19 en répétition à blanc locale sur une instance jetable (conteneurs Docker) — pas encore déroulée sur l'instance de référence.
Sur une plateforme sans disque persistant (la plupart des PaaS), fournir ces deux secrets en variables d'environnement est obligatoire, et ils doivent être posés avant le premier démarrage (sur une migration, avec les valeurs extraites de l'ancienne installation) — sans quoi une clé neuve serait régénérée à chaque recréation du conteneur. Voir Déployer sur un PaaS.
Variables SMTP — provisionnement initial
Les sept variables SMTP_* de server/.env ne servent qu'au premier démarrage du serveur. À ce moment, elles sont copiées telles quelles dans la table app_config de la base de données ; ensuite, la configuration SMTP se gère exclusivement dans l'application (écran Paramètres → onglet Serveur d'email — voir Paramètres in-app).
Provisionnement one-shot
Une fois la copie effectuée, modifier les variables SMTP_* dans server/.env n'a plus aucun effet : le runtime lit la configuration en base, pas les variables d'environnement. Pour changer les paramètres SMTP après l'installation initiale, utiliser l'écran Paramètres.
Variable (.env) | Clé app_config correspondante | Rôle |
|---|---|---|
SMTP_HOST | smtp_host | Hôte du serveur SMTP |
SMTP_PORT | smtp_port | Port SMTP |
SMTP_SECURE | smtp_secure | TLS activé (true/false) |
SMTP_USER | smtp_user | Identifiant SMTP |
SMTP_PASSWORD | smtp_password | Mot de passe SMTP (chiffré en base avec ENCRYPTION_KEY) |
SMTP_FROM_NAME | smtp_from_name | Nom d'expéditeur affiché (défaut : TimePick) |
SMTP_FROM_EMAIL | smtp_from_email | Adresse d'expéditeur SMTP |
Le détail des réglages disponibles par fournisseur (Gmail, Brevo, OVH…) est traité dans SMTP — Fournisseurs.
Cascade de résolution SMTP
Au moment d'envoyer un email, le serveur détermine le transport SMTP à utiliser selon l'ordre suivant :
- Base de données (
app_config) — priorité la plus haute ; c'est la source utilisée en production dès que le provisionnement initial a eu lieu. - Variables
SMTP_*deserver/.env— utilisées uniquement si la base ne contient aucune configuration SMTP, et uniquement hors production. - Repli vers un intercepteur SMTP local (
127.0.0.1:1025) en développement/test si aucune des sources précédentes n'est renseignée — tout outil écoutant sur ce port convient (Mailpit, MailCatcher, MailDev…) ; en production, l'absence de configuration en base désactive l'envoi d'emails (aucun repli).
À confirmer
Cette cascade est vérifiée dans le code (server/src/services/email-transport.service.ts) et par un test de bout en bout avec Mailpit sur l'instance de référence (re-vérifié le 2026-07-18, y compris le repli automatique sans configuration). Un envoi via un fournisseur SMTP réel (Gmail, Brevo, OVH…) n'a pas encore été testé de bout en bout — toute configuration SMTP décrite ailleurs dans cette documentation porte la mention « à confirmer » tant qu'elle n'a pas été validée avec un compte réel.
Fournisseur d'email HTTP — provisionnement initial (EMAIL_PROVIDER)
Le transport d'email est pluggable : SMTP par défaut, ou l'un des cinq fournisseurs de la catégorie Envoi par API (HTTP) — Brevo, Mailjet, Scaleway, Sweego, Resend — pour les hébergeurs qui bloquent tout SMTP sortant (voir SMTP — Fournisseurs). Deux variables optionnelles provisionnent ce choix au premier démarrage, sur le même principe « one-shot » que les SMTP_* :
Variable (.env) | Clé app_config correspondante | Rôle |
|---|---|---|
EMAIL_PROVIDER | email_provider | smtp (défaut) ou l'id d'un fournisseur HTTP : brevo, mailjet, scaleway, sweego, resend |
EMAIL_API_CREDENTIALS | email_api_credentials | Identifiants du fournisseur choisi, en JSON — ex. {"apiKey":"…"} (Brevo, Sweego, Resend), {"apiKey":"…","secretKey":"…"} (Mailjet), ou {"secretKey":"…","projectId":"…","region":"fr-par"} (Scaleway) |
EMAIL_API_KEY (déprécié) | email_api_key | Alias legacy à un seul champ, équivalent à EMAIL_API_CREDENTIALS={"apiKey":"<valeur>"} — conservé pour compatibilité ascendante avec les installations qui provisionnaient déjà resend avant l'introduction de EMAIL_API_CREDENTIALS |
Précédence et validation du seed
- Le seed du fournisseur HTTP ne s'exécute que si
SMTP_HOSTest absent —SMTP_*reste prioritaire surEMAIL_PROVIDER, exactement comme la base de données prime sur les variables d'environnement dans la cascade ci-dessus. - Si
EMAIL_API_CREDENTIALSetEMAIL_API_KEYsont posées toutes les deux,EMAIL_API_CREDENTIALSprime ;EMAIL_API_KEYn'est lue qu'en son absence. EMAIL_API_CREDENTIALSdoit être un objet JSON valide (ex.{"apiKey":"…"}) : un JSON malformé, ou une valeur qui n'est pas un objet, est loggé explicitement côté serveur et le seed est ignoré (jamais avalé silencieusement).- Si un champ requis du fournisseur manque dans les identifiants fournis (ex.
mailjetsanssecretKey), le seed est loggé et ignoré — pas d'écriture partielle en base.
En l'absence de ces variables, rien ne change : le défaut reste smtp et les installations existantes ne sont pas concernées.
Rollback / downgrade — portée limitée
Revenir à une version du serveur antérieure à la migration 040_email_api_credentials.sql (chantier email-providers) ne restaure automatiquement l'envoi que pour les installations qui étaient déjà en resend avant cette migration : leur clé reste lisible via l'ancienne colonne email_api_key, conservée en base et jamais purgée. Toute configuration posée avec un fournisseur multi-champ (Mailjet, Scaleway…), ou un resend configuré après la migration, n'existe que dans email_api_credentials — une version pré-migration ne sait pas la lire et retombe silencieusement sur smtp sans configuration, ce qui coupe l'envoi d'emails en production si aucun SMTP n'est configuré en parallèle. Après un downgrade, reconfigurer le fournisseur email depuis Paramètres → Serveur d'email plutôt que de compter sur un rollback automatique. Note additionnelle : après une réinitialisation (DELETE /api/admin/settings/smtp), l'ancienne valeur email_api_key (chiffrée) reste conservée en base, pas purgée — précisément pour ce cas de rollback à portée limitée.
Stockage des images d'email (STORAGE_DRIVER)
Les images uploadées dans l'éditeur d'emails (logo, illustrations) sont stockées, au choix, sur le disque local (défaut) ou dans un object storage S3-compatible :
| Variable | Rôle | Requise | Défaut |
|---|---|---|---|
STORAGE_DRIVER | local (disque, servi par /uploads) ou s3 (object storage S3-compatible) | Non | local |
S3_ENDPOINT | URL de l'endpoint S3 (ex. https://ams3.digitaloceanspaces.com) | Si s3 | — |
S3_BUCKET | Nom du bucket | Si s3 | — |
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY | Clés d'accès au bucket | Si s3 | — |
S3_REGION | Région du bucket (Cloudflare R2 exige auto) | Non | auto |
S3_PUBLIC_BASE_URL | Base publique des URLs d'images (CDN ou domaine public du bucket) | Non (obligatoire pour Cloudflare R2, voir ci-dessous) | <S3_ENDPOINT>/<S3_BUCKET> |
localest le défaut et reste strictement identique au comportement historique : aucune installation existante n'a besoin de changer quoi que ce soit. Le drivers3sert avant tout les hébergeurs sans disque persistant (voir Déployer sur un PaaS).- Si
STORAGE_DRIVER=s3et qu'une des quatre variables requises manque, le serveur refuse de démarrer (échec rapide, comme pour uneENCRYPTION_KEYmalformée). - Le driver est générique : tout stockage S3-compatible convient (DigitalOcean Spaces, Cloudflare R2, Scaleway Object Storage, MinIO auto-hébergé…).
Configuration du bucket, à la charge de l'installateur :
- lecture publique (« public-read ») — les clients email chargent les images sans authentification ;
- règle CORS autorisant les requêtes
GETdepuis l'origine de l'application — l'éditeur d'emails et son aperçu chargent les images en cross-origin ; - URL publique servie en HTTPS obligatoirement : une image en
http://est acceptée à l'upload mais rejetée au moment du rendu des emails — symptôme typique : « l'upload a réussi mais le logo manque dans les emails envoyés ».
Cloudflare R2 : S3_PUBLIC_BASE_URL obligatoire
L'endpoint R2 (<compte>.r2.cloudflarestorage.com) n'est jamais accessible en lecture publique : renseigner S3_PUBLIC_BASE_URL avec le domaine public du bucket (domaine managé pub-<hash>.r2.dev à activer dans le dashboard R2, ou domaine personnalisé attaché au bucket). Le défaut <S3_ENDPOINT>/<S3_BUCKET> ne convient qu'aux fournisseurs dont l'endpoint peut être configuré en lecture publique (Spaces, Scaleway, MinIO).
Le choix du fournisseur appartient à l'installateur
TimePick n'impose aucun fournisseur de stockage. Le choix du fournisseur et de la région relève de l'installateur, y compris sous l'angle réglementaire : Cloudflare R2 et DigitalOcean Spaces sont opérés par des sociétés américaines (soumises au CLOUD Act) même en région UE, tandis que Scaleway est un opérateur européen. À arbitrer selon vos contraintes de résidence des données — l'application manipule des adresses email, donc des données personnelles.
Basculer une installation existante vers s3 ne concerne que les nouveaux uploads : les images déjà stockées sur disque restent servies par /uploads (qui reste actif en permanence) et continuent de fonctionner — aucune migration de fichiers n'est requise. Impact sur les sauvegardes : voir Sauvegarde et restauration.
Statut de vérification
Le driver s3 a été vérifié de bout en bout en local contre un MinIO auto-hébergé (upload, rendu des emails, suppression). Il n'a pas encore été exercé contre un fournisseur cloud réel (Spaces, R2, Scaleway) sur l'instance de référence.
Variable client (client/.env)
| Variable | Rôle | Requise | Défaut |
|---|---|---|---|
VITE_API_URL | URL complète de l'API backend | Non | http://localhost:3000/api |
VITE_API_URL est une variable de build, pas de runtime
Vite intègre VITE_API_URL dans le bundle JavaScript au moment de la compilation (npm run build). La modifier après coup dans un conteneur déjà construit n'a aucun effet : il faut reconstruire l'image avec la bonne valeur. Voir Installation en production (Docker) pour la procédure de build avec cette variable.
L'image canonique publiée par la CI du projet est, elle, construite avec VITE_API_URL=/api (chemin relatif) : le frontend appelle alors l'API en same-origin, quel que soit le domaine — c'est ce qui rend cette image unique portable d'un hôte à l'autre. Voir Déployer sur un PaaS.
Infrastructure vs réglages applicatifs
Pour résumer la distinction évoquée en introduction :
- Infrastructure (
.env/ variables du conteneur, lues au démarrage, redémarrage requis pour tout changement) :DATABASE_URL,PORT,JWT_SECRET,APP_URL,EMAIL_FROM,ENCRYPTION_KEY,STORAGE_DRIVERet les variablesS3_*,VITE_API_URL, ainsi que les variablesSMTP_*,EMAIL_PROVIDER,EMAIL_API_CREDENTIALSetEMAIL_API_KEY(déprécié) — ces familles uniquement pour leur rôle de provisionnement initial. - Applicatif (table
app_config, modifiable à chaud depuis l'écran Paramètres, sans redémarrage) : nom et description de l'organisation, logo, intervalle de polling du calendrier, durées de vie des liens de connexion et des sessions, et — une fois le provisionnement initial effectué — la configuration complète du serveur d'email (SMTP ou l'un des fournisseurs HTTP).
Le détail des réglages applicatifs et de l'écran Paramètres est couvert dans Configuration initiale et Paramètres in-app.