Variables d'environnement
Le service lit son environnement au démarrage : fichier .env en local (chargé par
dotenvy), ConfigMap et SealedSecret en Kubernetes (core/product-service/deploy/k8s/).
Une variable obligatoire absente fait échouer le démarrage. Les secrets ne se commitent
jamais en clair.
SERVICE_NAME
Section intitulée « SERVICE_NAME »Nom du service dans les logs. Défaut : product-service.
SERVICE_NAME=product-serviceHTTP_ADDR
Section intitulée « HTTP_ADDR »Adresse d’écoute REST. Défaut : 0.0.0.0:8093. Doit écouter sur 0.0.0.0 (pas
127.0.0.1) pour que Traefik, dans son conteneur, l’atteigne via host.docker.internal.
HTTP_ADDR=0.0.0.0:8093GRPC_ADDR
Section intitulée « GRPC_ADDR »Adresse d’écoute gRPC. Défaut : 0.0.0.0:50051. Non exposé par Traefik.
GRPC_ADDR=0.0.0.0:50051CORS_ALLOWED_ORIGINS
Section intitulée « CORS_ALLOWED_ORIGINS »Origines navigateur autorisées à appeler le service, séparées par des virgules, sans
slash final. Jamais * : les cookies de session accompagnent les requêtes. Défaut : vide
(aucune origine autorisée).
CORS_ALLOWED_ORIGINS=https://app.clairtyx.com,https://clairtyx.com,http://localhost:5173Dossier des fichiers de logs. Défaut : ./logs. En Kubernetes : /var/log/product-service.
LOG_DIR=./logsRUST_LOG
Section intitulée « RUST_LOG »Niveau de logs (syntaxe tracing). Facultatif ; défaut : info.
RUST_LOG=info,product_service=debugDATABASE_URL
Section intitulée « DATABASE_URL »Connexion PostgreSQL (tables products, prices, outbox, dead-letter, clés d’idempotence).
Les migrations sont dans migrations/. Défaut : postgres://product:product@localhost:5432/product.
Secret (contient le mot de passe).
DATABASE_URL=postgres://product:motdepasse@localhost:5432/productREDIS_URL
Section intitulée « REDIS_URL »Connexion Valkey/Redis du cache des produits provisoires (créés mais pas encore confirmés
par le webhook). Défaut : redis://localhost:6379.
REDIS_URL=redis://localhost:6379CACHE_TTL_SECONDS
Section intitulée « CACHE_TTL_SECONDS »Durée de vie d’une entrée provisoire, en secondes. Doit dépasser la latence habituelle
Stripe + webhook + Kafka, sinon un produit disparaît de la lecture avant d’être confirmé en
base. Défaut : 300. Une valeur non numérique retombe sur 300.
CACHE_TTL_SECONDS=300STRIPE_API_KEY
Section intitulée « STRIPE_API_KEY »Clé d’API Stripe. Obligatoire. Secret. Le service accepte n’importe quelle valeur, il ne vérifie pas le préfixe.
STRIPE_API_KEY=rk_test_51MexempleSTRIPE_API_VERSION
Section intitulée « STRIPE_API_VERSION »Version d’API Stripe figée pour tous les appels. Défaut : 2026-05-27.dahlia.
STRIPE_API_VERSION=2026-05-27.dahliaKafka / Redpanda
Section intitulée « Kafka / Redpanda »KAFKA_BROKERS
Section intitulée « KAFKA_BROKERS »Liste des brokers hôte:port, séparés par des virgules. Défaut : localhost:9092.
KAFKA_BROKERS=localhost:9092KAFKA_TOPIC
Section intitulée « KAFKA_TOPIC »Topic sur lequel le service publie ses événements produit. Défaut : product.events.
KAFKA_TOPIC=product.eventsKAFKA_WEBHOOK_EVENTS_TOPIC
Section intitulée « KAFKA_WEBHOOK_EVENTS_TOPIC »Topic que le service consomme : les webhooks Stripe bruts, publiés par
webhook-ingestion-service. Défaut : provider.webhooks.raw.
KAFKA_WEBHOOK_EVENTS_TOPIC=provider.webhooks.rawKAFKA_WEBHOOK_EVENTS_GROUP
Section intitulée « KAFKA_WEBHOOK_EVENTS_GROUP »Groupe de consommation Kafka. Plusieurs réplicas partagent le même groupe. Défaut :
product-service-webhook-events.
KAFKA_WEBHOOK_EVENTS_GROUP=product-service-webhook-eventsImages (stockage S3)
Section intitulée « Images (stockage S3) »S3_BUCKET
Section intitulée « S3_BUCKET »Bucket des photos produit. Défaut : product-images.
S3_BUCKET=product-imagesS3_ENDPOINT
Section intitulée « S3_ENDPOINT »Point d’accès d’un stockage S3 compatible (Garage, MinIO). Absent : AWS S3 réel. Quand
il est défini, le service utilise l’adressage « path-style » et la région garage si
AWS_REGION est vide.
S3_ENDPOINT=http://localhost:3900AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY
Section intitulée « AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY »Identifiants du stockage S3, lus par la chaîne standard du SDK AWS (pas par le code du
service). Secrets. Pour Garage, la paire créée avec garage key create.
AWS_ACCESS_KEY_ID=GKexempleAWS_SECRET_ACCESS_KEY=secretexempleAWS_REGION
Section intitulée « AWS_REGION »Région S3. Facultatif ; ne sert qu’avec S3_ENDPOINT (défaut garage).
AWS_REGION=garageIMAGE_PUBLIC_BASE_URL
Section intitulée « IMAGE_PUBLIC_BASE_URL »Base publique du CDN qui sert les photos. Stripe n’accepte que des URL : une clé
products/tee.png lui est envoyée sous la forme <base>/products/tee.png. Sans slash final.
Absente : seules les URL absolues sont acceptées comme images. Si le CDN monte les clés
sous un sous-chemin, mettre ce chemin dans la base.
IMAGE_PUBLIC_BASE_URL=https://cdn.clairtyx.comAuthentification
Section intitulée « Authentification »AUTH_MODE
Section intitulée « AUTH_MODE »enforce (défaut) vérifie le JWT et applique AUTH_RULES. off laisse tout passer :
développement local uniquement.
AUTH_MODE=enforceAUTH_JWKS_URL
Section intitulée « AUTH_JWKS_URL »URL du JWKS du backend (clés publiques de signature). Obligatoire en enforce. À
pointer directement sur le backend, pas sur https://api.clairtyx.com : Traefik n’y
autorise que les réseaux privés (jwks-allowlist) et répond 403 Forbidden à un appel venu
de l’adresse publique.
AUTH_JWKS_URL=http://localhost:8091/.well-known/jwks.jsonAUTH_ISSUER
Section intitulée « AUTH_ISSUER »Valeur attendue du champ iss du JWT. Défaut : boutique-backend.
AUTH_ISSUER=boutique-backendAUTH_COOKIE_NAME
Section intitulée « AUTH_COOKIE_NAME »Nom du cookie d’accès, lu quand la requête n’a pas d’en-tête Authorization: Bearer.
Défaut : shop_access.
AUTH_COOKIE_NAME=shop_accessAUTH_JWKS_TTL_SECONDS
Section intitulée « AUTH_JWKS_TTL_SECONDS »Durée pendant laquelle les clés récupérées sont gardées, en secondes. Défaut : 300.
AUTH_JWKS_TTL_SECONDS=300AUTH_RULES
Section intitulée « AUTH_RULES »Politique d’accès, une règle par ligne (ou séparées par ;). Obligatoire en
enforce : vide, le service refuse de démarrer.
MÉTHODES CHEMIN = EXIGENCEMÉTHODES:*ou une liste (PATCH,DELETE).HEADcompte commeGET.CHEMIN: le motif de route tel qu’enregistré (/v1/products/:id,/v1/products/images/*key), pas l’URL reçue. Comparaison exacte :DELETE /v1/products(suppression en lot) etDELETE /v1/products/:idsont deux routes distinctes.EXIGENCE:public: aucun jeton ;authenticated: n’importe quel jeton valide ;a|b|…: au moins un des éléments. Un élément nu est une permission (claimpermsdu JWT) ;role:<code>est un rôle (claimroles).
- Les lignes qui commencent par
#sont des commentaires. - La première règle qui correspond l’emporte.
- Une route sans règle répond
403et écrit un avertissement dans les logs. - Jeton absent, invalide ou expiré :
401. Jeton valide sans le droit :403.
Permission ou rôle ? Préférer les permissions. admin et editor ont tous deux
products.write et prices.write : une règle sur la permission les accepte sans que le
service connaisse les rôles. editor n’est pas inclus dans admin : role:editor seul
refuse un compte admin, et un rôle renommé côté backend casse la règle.
| Permission | admin | editor | customer |
|---|---|---|---|
products.write |
✓ | ✓ | |
prices.write |
✓ | ✓ | |
coupons.write, promo_codes.write, discounts.manage |
✓ | ✓ | |
customers.read, customers.manage, members.read, members.manage |
✓ |
Source de la matrice : backend/src/domains/authz/permissions.rs.
AUTH_RULES='* /healthz = public* /readyz = publicGET /metrics = public
GET /v1/products = publicGET /v1/products/:id = publicGET /v1/products/images/*key = publicGET /v1/products/schema = authenticated
POST /v1/products = products.writeDELETE /v1/products = products.writePATCH /v1/products/:id = products.writeDELETE /v1/products/:id = products.writePOST /v1/products/images = products.writePATCH /v1/products/:id/images = products.write
POST /v1/products/:id/prices = prices.writePATCH /v1/prices/:id = prices.writeDELETE /v1/prices/:id = prices.write'Pour réserver la suppression aux administrateurs, remplacer products.write par
role:admin sur les deux routes DELETE.
Métriques
Section intitulée « Métriques »METRICS_TOKEN
Section intitulée « METRICS_TOKEN »Jeton Bearer exigé par GET /metrics (comparé en temps constant). Secret. Sans lui,
l’endpoint refuse toutes les requêtes, sauf si METRICS_ALLOW_OPEN=true. Note : /metrics
peut être public dans AUTH_RULES et rester protégé par ce jeton.
METRICS_TOKEN=un-jeton-long-et-aleatoireMETRICS_ALLOW_OPEN
Section intitulée « METRICS_ALLOW_OPEN »true ouvre /metrics sans jeton. Défaut : false. À réserver à un réseau fermé.
METRICS_ALLOW_OPEN=falseExemple de .env complet et valide
Section intitulée « Exemple de .env complet et valide »Développement local, avec Garage, Redpanda, PostgreSQL et Valkey lancés sur l’hôte. Les
valeurs exemple sont à remplacer.
# --- Service ---SERVICE_NAME=product-serviceHTTP_ADDR=0.0.0.0:8093GRPC_ADDR=0.0.0.0:50051CORS_ALLOWED_ORIGINS=http://localhost:5173,http://localhost:5174LOG_DIR=./logsRUST_LOG=info
# --- Données ---DATABASE_URL=postgres://product:motdepasse@localhost:5432/productREDIS_URL=redis://localhost:6379CACHE_TTL_SECONDS=300
# --- Stripe ---STRIPE_API_KEY=rk_test_51MexempleSTRIPE_API_VERSION=2026-05-27.dahlia
# --- Kafka / Redpanda ---KAFKA_BROKERS=localhost:9092KAFKA_TOPIC=product.eventsKAFKA_WEBHOOK_EVENTS_TOPIC=provider.webhooks.rawKAFKA_WEBHOOK_EVENTS_GROUP=product-service-webhook-events
# --- Images ---S3_BUCKET=product-imagesS3_ENDPOINT=http://localhost:3900AWS_ACCESS_KEY_ID=GKexempleAWS_SECRET_ACCESS_KEY=secretexempleIMAGE_PUBLIC_BASE_URL=https://cdn.clairtyx.com
# --- Authentification ---AUTH_MODE=enforceAUTH_JWKS_URL=http://localhost:8091/.well-known/jwks.jsonAUTH_ISSUER=boutique-backendAUTH_COOKIE_NAME=shop_accessAUTH_JWKS_TTL_SECONDS=300AUTH_RULES='* /healthz = public* /readyz = publicGET /metrics = public
GET /v1/products = publicGET /v1/products/:id = publicGET /v1/products/images/*key = publicGET /v1/products/schema = authenticated
POST /v1/products = products.writeDELETE /v1/products = products.writePATCH /v1/products/:id = products.writeDELETE /v1/products/:id = products.writePOST /v1/products/images = products.writePATCH /v1/products/:id/images = products.write
POST /v1/products/:id/prices = prices.writePATCH /v1/prices/:id = prices.writeDELETE /v1/prices/:id = prices.write'
# --- Métriques ---METRICS_TOKEN=un-jeton-long-et-aleatoireMETRICS_ALLOW_OPEN=false