Skip to content

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.

VariableRôleRequiseDéfaut
DATABASE_URLChaîne de connexion PostgreSQL (postgresql://user:password@hôte:5432/timepick)Oui
PORTPort d'écoute du serveur ExpressNon3000
JWT_SECRETSecret de signature des JWT (sessions admin)Nongénéré automatiquement (voir ci-dessous)
APP_URLURL de base du frontend, utilisée pour construire les liens de connexion (magic links) et les boutons d'action des emailsNonhttp://localhost:5173
EMAIL_FROMAdresse d'expéditeur par défaut, utilisée quand aucune adresse SMTP n'est configurée en baseNonnoreply@example.com
ENCRYPTION_KEYClé de chiffrement (32 octets = 64 caractères hexadécimaux), utilisée pour chiffrer le mot de passe SMTP stocké en baseNongénéré automatiquement (voir ci-dessous)
DATA_DIRRé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_DRIVERStockage des images uploadées dans l'éditeur d'emails : local (disque) ou s3 (object storage S3-compatible) — voir la section dédiéeNonlocal

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 :

bash
openssl rand -hex 32

Exécuter la commande une première fois pour JWT_SECRET :

bash
openssl rand -hex 32

Puis une seconde fois, séparément, pour ENCRYPTION_KEY (ne jamais réutiliser la même valeur pour les deux) :

bash
openssl rand -hex 32

Chaque 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.keyavant 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 :

  1. Sauvegarder : dump PostgreSQL frais, copie hors site des fichiers jwt.secret et encryption.key du répertoire DATA_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).
  2. 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).
  3. Poser les deux variables d'environnement avec les valeurs extraites, puis redéployer. Conserver le volume data intact : il sert de retour arrière (retirer les deux variables et redéployer restaure l'état initial à l'identique).
  4. 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 correspondanteRôle
SMTP_HOSTsmtp_hostHôte du serveur SMTP
SMTP_PORTsmtp_portPort SMTP
SMTP_SECUREsmtp_secureTLS activé (true/false)
SMTP_USERsmtp_userIdentifiant SMTP
SMTP_PASSWORDsmtp_passwordMot de passe SMTP (chiffré en base avec ENCRYPTION_KEY)
SMTP_FROM_NAMEsmtp_from_nameNom d'expéditeur affiché (défaut : TimePick)
SMTP_FROM_EMAILsmtp_from_emailAdresse 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 :

  1. 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.
  2. Variables SMTP_* de server/.env — utilisées uniquement si la base ne contient aucune configuration SMTP, et uniquement hors production.
  3. 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 correspondanteRôle
EMAIL_PROVIDERemail_providersmtp (défaut) ou l'id d'un fournisseur HTTP : brevo, mailjet, scaleway, sweego, resend
EMAIL_API_CREDENTIALSemail_api_credentialsIdentifiants 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_keyAlias 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_HOST est absentSMTP_* reste prioritaire sur EMAIL_PROVIDER, exactement comme la base de données prime sur les variables d'environnement dans la cascade ci-dessus.
  • Si EMAIL_API_CREDENTIALS et EMAIL_API_KEY sont posées toutes les deux, EMAIL_API_CREDENTIALS prime ; EMAIL_API_KEY n'est lue qu'en son absence.
  • EMAIL_API_CREDENTIALS doit ê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. mailjet sans secretKey), 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 :

VariableRôleRequiseDéfaut
STORAGE_DRIVERlocal (disque, servi par /uploads) ou s3 (object storage S3-compatible)Nonlocal
S3_ENDPOINTURL de l'endpoint S3 (ex. https://ams3.digitaloceanspaces.com)Si s3
S3_BUCKETNom du bucketSi s3
S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEYClés d'accès au bucketSi s3
S3_REGIONRégion du bucket (Cloudflare R2 exige auto)Nonauto
S3_PUBLIC_BASE_URLBase publique des URLs d'images (CDN ou domaine public du bucket)Non (obligatoire pour Cloudflare R2, voir ci-dessous)<S3_ENDPOINT>/<S3_BUCKET>
  • local est le défaut et reste strictement identique au comportement historique : aucune installation existante n'a besoin de changer quoi que ce soit. Le driver s3 sert avant tout les hébergeurs sans disque persistant (voir Déployer sur un PaaS).
  • Si STORAGE_DRIVER=s3 et qu'une des quatre variables requises manque, le serveur refuse de démarrer (échec rapide, comme pour une ENCRYPTION_KEY malformé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 GET depuis 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)

VariableRôleRequiseDéfaut
VITE_API_URLURL complète de l'API backendNonhttp://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_DRIVER et les variables S3_*, VITE_API_URL, ainsi que les variables SMTP_*, EMAIL_PROVIDER, EMAIL_API_CREDENTIALS et EMAIL_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.

Publié sous licence FSL-1.1-MIT.