Aller au contenu

Prise en main de Chainlit

Mise à jour :

logo chainlit

Chainlit est une bibliothèque Python open-source conçue pour créer facilement des interfaces web conversationnelles autour des LLMs comme GPT, Mistral ou Llama. Son objectif ? Permettre aux développeurs de concevoir des chatbots ou assistants interactifs sans se soucier du frontend. En quelques lignes de code, vous lancez une application web responsive, avec support natif du streaming, d’actions personnalisées, d’authentification OAuth, et même d’intégration avec des frameworks comme LangChain ou LlamaIndex. Chainlit est donc une solution idéale pour tester, prototyper, ou même déployer en production des assistants IA avec une interface moderne et réactive.

Installation de Chainlit

Chainlit est conçu pour être simple à installer et rapide à démarrer. En quelques commandes, vous pouvez lancer une application conversationnelle fonctionnelle avec interface web.

Commencez par créer un environnement virtuel Python pour isoler votre projet :

Terminal window
python -m venv venv
source venv/bin/activate # Sur Windows, utilisez `venv\Scripts\activate`

Installez la bibliothèque avec pip :

Terminal window
pip install chainlit

Pendant l’installation, Chainlit va télécharger toutes les dépendances nécessaires, y compris les bibliothèques pour le support du streaming et des actions interactives, mais aussi les outils pour l’interface web.

Vérifiez que la commande chainlit est bien disponible :

Terminal window
chainlit --help

Normalement, vous devriez voir l’aide de Chainlit s’afficher, indiquant que l’installation s’est bien déroulée. Mais aussi la création de deux dossiers importants :

  • .chainlit/ : contient le fichier de configuration et les fichiers de traduction.
  • .files/ : utilisé pour stocker les fichiers uploadés par les utilisateurs.
  • chainlit.md : le fichier de documentation de Chainlit.

Premier script minimal

Créez un fichier app.py avec ce contenu :

import chainlit as cl
@cl.on_chat_start
async def start():
await cl.Message(content="Bienvenue !").send()

Lancez ensuite l’application avec :

Terminal window
chainlit run app.py -w

Vous devriez voir un message de bienvenue s’afficher dans votre navigateur à l’adresse http://localhost:8000 comme dans l’aperçu ci-dessous :

aperçu de l'application

Concepts clés

Chainlit repose sur un modèle d’événements asynchrones. Il fournit une série de décorateurs Python qui vous permettent de réagir aux actions de l’utilisateur et de structurer le comportement de votre application conversationnelle.

Le cycle de vie d’une session utilisateur

Voici les principaux événements gérés par Chainlit :

  • @cl.on_chat_start : appelé à l’ouverture d’une nouvelle session ou au rechargement.
  • @cl.on_message : déclenché à chaque message utilisateur.
  • @cl.on_stop : optionnel, déclenché lors de l’arrêt de la session.
  • @cl.action_callback : pour gérer les actions interactives (boutons).

Exemple de base :

import chainlit as cl
@cl.on_chat_start
async def start():
await cl.Message(content="Bienvenue !").send()
@cl.on_message
async def handle_message(message: cl.Message):
await cl.Message(content=f"Vous avez dit : {message.content}").send()

Si vous lancez ce code avec chainlit run app.py, vous verrez un message de

Les objets fournis par Chainlit

Les fonctions déclenchées par les hooks reçoivent ou manipulent différents objets natifs. Voici les principaux :

  • cl.Message : représente un message dans la conversation. Vous pouvez envoyer du texte, des actions, des fichiers, etc.
  • cl.Action : représente une action interactive (bouton) que l’utilisateur peut cliquer. Chaque action doit avoir un name, un value et un label.
  • cl.UserSession : permet de stocker des données persistantes spécifiques à l’utilisateur, comme son nom ou ses préférences.

Exemple d’utilisation des actions :

import chainlit as cl
@cl.on_chat_start
async def start():
await cl.Message(content="Bienvenue ! Tapez 'Bonjour' pour commencer.").send()
@cl.on_message
async def handle_message(message: cl.Message):
if message.content == "Bonjour":
# Créer des actions avec la syntaxe correcte (payload requis)
actions = [
cl.Action(name="help", value="help", label="🆘 Aide", payload={"action": "help"}),
cl.Action(name="bye", value="bye", label="👋 Au revoir", payload={"action": "bye"})
]
await cl.Message(
content="Comment puis-je vous aider ?",
actions=actions
).send()
elif message.content == "Aide":
await cl.Message(content="Voici comment je peux vous aider...\n- Tapez 'Bonjour' pour commencer\n- Tapez 'Aide' pour cette aide\n- Tapez 'Au revoir' pour quitter").send()
elif message.content == "Au revoir":
await cl.Message(content="À bientôt ! Merci d'avoir utilisé cette application.").send()
else:
await cl.Message(content="Je ne comprends pas votre message. Tapez 'Bonjour' pour commencer ou 'Aide' pour obtenir de l'aide.").send()
@cl.action_callback("help")
async def on_help_action(action: cl.Action):
await cl.Message(content="Voici comment je peux vous aider...\n- Tapez 'Bonjour' pour commencer\n- Cliquez sur 'Aide' pour cette aide\n- Cliquez sur 'Au revoir' pour quitter").send()
@cl.action_callback("bye")
async def on_bye_action(action: cl.Action):
await cl.Message(content="À bientôt ! Merci d'avoir utilisé cette application.").send()

Quelques explications sur le code ci-dessus :

  • Quand l’utilisateur tape “Bonjour”, l’application envoie un message avec deux actions : “Aide” et “Au revoir”.
  • Si l’utilisateur clique sur “Aide”, l’application répond avec un message détaillant les options disponibles.
  • Si l’utilisateur clique sur “Au revoir”, l’application envoie un message de remerciement et de fin de session (fictif).

aperçu de l'application avec actions

Authentification dans Chainlit

Par défaut, une application Chainlit est publique, accessible à tous. Pour restreindre l’accès, il est possible d’activer trois méthodes d’authentification : mot de passe, header HTTP, et OAuth. Avant tout, commencez par définir une clé secrète pour signer les tokens utilisateur :

Terminal window
chainlit create-secret

Cette clé permet de sécuriser les sessions : la modifier déconnectera automatiquement tous les utilisateurs. Vous devez la définir dans votre fichier .env.

Terminal window
CHAINLIT_AUTH_SECRET="la_clé_secrète_que_vous_avez_générée"

N’oubliez pas d’ajouter ce fichier .env à votre .gitignore pour ne pas le partager publiquement.

La méthode la plus simple consiste à utiliser un mot de passe. Vous pouvez configurer un callback @cl.password_auth_callback qui sera appelé à chaque connexion. Ce callback doit vérifier les identifiants et retourner un objet cl.User si la connexion est réussie, ou None en cas d’échec.

import chainlit as cl
from typing import Optional
@cl.password_auth_callback
def auth_callback(username: str, password: str) -> Optional[cl.User]:
# Vérification des identifiants (exemple simple)
# En production, utilisez une base de données et des mots de passe hachés
if (username, password) == ("admin", "admin"):
return cl.User(
identifier="admin",
metadata={"role": "admin", "provider": "credentials"}
)
elif (username, password) == ("user", "user"):
return cl.User(
identifier="user",
metadata={"role": "user", "provider": "credentials"}
)
else:
return None
@cl.on_chat_start
async def start():
# Récupérer l'utilisateur connecté
user = cl.user_session.get("user")
if user:
username = user.identifier
await cl.Message(
content=f"Bienvenue {username} ! Tapez 'Bonjour' pour commencer."
).send()
else:
await cl.Message(content="Bienvenue ! Tapez 'Bonjour' pour commencer.").send()
@cl.on_message
async def handle_message(message: cl.Message):
if message.content == "Bonjour":
# Créer des actions avec la syntaxe correcte (payload requis)
actions = [
cl.Action(name="help", value="help", label="🆘 Aide", payload={"action": "help"}),
cl.Action(name="bye", value="bye", label="👋 Au revoir", payload={"action": "bye"})
]
await cl.Message(
content="Comment puis-je vous aider ?",
actions=actions
).send()
elif message.content == "Aide":
await cl.Message(content="Voici comment je peux vous aider...\n- Tapez 'Bonjour' pour commencer\n- Tapez 'Aide' pour cette aide\n- Tapez 'Au revoir' pour quitter").send()
elif message.content == "Au revoir":
await cl.Message(content="À bientôt ! Merci d'avoir utilisé cette application.").send()
else:
await cl.Message(content="Je ne comprends pas votre message. Tapez 'Bonjour' pour commencer ou 'Aide' pour obtenir de l'aide.").send()
@cl.action_callback("help")
async def on_help_action(action: cl.Action):
await cl.Message(content="Voici comment je peux vous aider...\n- Tapez 'Bonjour' pour commencer\n- Cliquez sur 'Aide' pour cette aide\n- Cliquez sur 'Au revoir' pour quitter").send()
@cl.action_callback("bye")
async def on_bye_action(action: cl.Action):
await cl.Message(content="À bientôt ! Merci d'avoir utilisé cette application.").send()
  • Si la fonction retourne None ➜ échec de connexion.

  • Sinon, un objet cl.User est associé à la session, accessible via cl.user_session.get("user").

  • Vous pouvez personnaliser l’objet cl.User avec des métadonnées (rôle, provider, etc.) pour gérer les permissions ou personnaliser l’expérience utilisateur.

chainlit login

Plus d’informations sur l’authentification sont disponibles dans la documentation officielle de Chainlit.

Personnalisation de l’interface Chainlit

Avec Chainlit, vous pouvez transformer une interface générique en application à l’image de votre entreprise. Cela inclut l’apparence visuelle, les comportements dynamiques, les avatars, ainsi que la traduction des éléments d’interface. Toutes les personnalisations se font sans modifier le cœur de la bibliothèque, en déposant simplement des fichiers dans des répertoires dédiés.

Configuration de l’interface

Pour personnaliser l’interface, modifiez le fichier config.toml dans le dossier .chainlit/. Vous pouvez y définir des options globales comme le nom du projet, la description, et les paramètres d’interface utilisateur. Voici un exemple de configuration :

[UI]
name = "Ton assistant"
default_theme = "light"

Cette configuration permet de définir le nom de l’application et le thème par défaut. Vous pouvez aussi choisir entre les thèmes “light” et “dark”.

Langue de l’interface

Pour ajouter une langue spécifique, créez un fichier de traduction dans le dossier .chainlit/translations/. Par exemple, pour le français, créez un fichier fr-FR.json :

{
"common": {
"actions": {
"cancel": "Annuler",
"confirm": "Confirmer",
"continue": "Continuer",
"goBack": "Retour",
"reset": "Réinitialiser",
"submit": "Soumettre"
},
"status": {
"loading": "Chargement...",
"error": {
"default": "Une erreur s'est produite",
"serverConnection": "Impossible de contacter le serveur"
}
}
},
"auth": {
"login": {
"title": "Connectez-vous pour accéder à l'application",
"form": {
"email": {
"label": "Adresse e-mail",
"required": "l'e-mail est un champ obligatoire"
},
"password": {
"label": "Mot de passe",
"required": "le mot de passe est un champ obligatoire"
},
"actions": {
"signin": "Se connecter"
},
"alternativeText": {
"or": "OU"
}
},
"errors": {
"default": "Impossible de se connecter",
"signin": "Essayez de vous connecter avec un compte différent",
"oauthSignin": "Essayez de vous connecter avec un compte différent",
"redirectUriMismatch": "L'URI de redirection ne correspond pas à la configuration de l'application OAuth",
"oauthCallback": "Essayez de vous connecter avec un compte différent",
"oauthCreateAccount": "Essayez de vous connecter avec un compte différent",
"emailCreateAccount": "Essayez de vous connecter avec un compte différent",
"callback": "Essayez de vous connecter avec un compte différent",
"oauthAccountNotLinked": "Pour confirmer votre identité, connectez-vous avec le même compte que vous avez utilisé initialement",
"emailSignin": "L'e-mail n'a pas pu être envoyé",
"emailVerify": "Veuillez vérifier votre e-mail, un nouvel e-mail a été envoyé",
"credentialsSignin": "Échec de la connexion. Vérifiez que les détails fournis sont corrects",
"sessionRequired": "Veuillez vous connecter pour accéder à cette page"
}
},
"provider": {
"continue": "Continuer avec {{provider}}"
}
},
"chat": {
"input": {
"placeholder": "Tapez votre message ici...",
"actions": {
"send": "Envoyer le message",
"stop": "Arrêter la tâche",
"attachFiles": "Joindre des fichiers"
}
},
"speech": {
"start": "Commencer l'enregistrement",
"stop": "Arrêter l'enregistrement",
"connecting": "Connexion en cours"
},
"fileUpload": {
"dragDrop": "Glisser-déposer les fichiers ici",
"browse": "Parcourir les fichiers",
"sizeLimit": "Limite :",
"errors": {
"failed": "Échec du téléchargement",
"cancelled": "Téléchargement annulé de"
}
},
"messages": {
"status": {
"using": "Utilise",
"used": "Utilisé"
},
"actions": {
"copy": {
"button": "Copier dans le presse-papiers",
"success": "Copié !"
}
},
"feedback": {
"positive": "Utile",
"negative": "Pas utile",
"edit": "Modifier le commentaire",
"dialog": {
"title": "Ajouter un commentaire",
"submit": "Soumettre le commentaire"
},
"status": {
"updating": "Mise à jour",
"updated": "Commentaire mis à jour"
}
}
},
"history": {
"title": "Dernières saisies",
"empty": "Rien ici...",
"show": "Afficher l'historique"
},
"settings": {
"title": "Panneau de paramètres"
},
"watermark": "Développé avec"
},
"threadHistory": {
"sidebar": {
"title": "Conversations précédentes",
"filters": {
"search": "Rechercher",
"placeholder": "Rechercher des conversations..."
},
"timeframes": {
"today": "Aujourd'hui",
"yesterday": "Hier",
"previous7days": "7 derniers jours",
"previous30days": "30 derniers jours"
},
"empty": "Aucune conversation trouvée",
"actions": {
"close": "Fermer la barre latérale",
"open": "Ouvrir la barre latérale"
}
},
"thread": {
"untitled": "Conversation sans titre",
"menu": {
"rename": "Renommer",
"delete": "Supprimer"
},
"actions": {
"delete": {
"title": "Confirmer la suppression",
"description": "Cela supprimera la conversation ainsi que ses messages et éléments. Cette action ne peut pas être annulée",
"success": "Conversation supprimée",
"inProgress": "Suppression de la conversation"
},
"rename": {
"title": "Renommer la conversation",
"description": "Saisissez un nouveau nom pour cette conversation",
"form": {
"name": {
"label": "Nom",
"placeholder": "Saisissez le nouveau nom"
}
},
"success": "Conversation renommée !",
"inProgress": "Renommage de la conversation"
}
}
}
},
"navigation": {
"header": {
"chat": "Chat",
"readme": "Lisez-moi",
"theme": {
"light": "Thème clair",
"dark": "Thème sombre",
"system": "Suivre le système"
}
},
"newChat": {
"button": "Nouvelle conversation",
"dialog": {
"title": "Créer une nouvelle conversation",
"description": "Cela effacera votre historique de conversation actuel. Êtes-vous sûr de vouloir continuer ?",
"tooltip": "Nouvelle conversation"
}
},
"user": {
"menu": {
"settings": "Paramètres",
"settingsKey": "S",
"apiKeys": "Clés API",
"logout": "Se déconnecter"
}
}
},
"apiKeys": {
"title": "Clés API requises",
"description": "Pour utiliser cette application, les clés API suivantes sont requises. Les clés sont stockées dans le stockage local de votre appareil.",
"success": {
"saved": "Enregistré avec succès"
}
},
"alerts": {
"info": "Info",
"note": "Note",
"tip": "Astuce",
"important": "Important",
"warning": "Avertissement",
"caution": "Attention",
"debug": "Débogage",
"example": "Exemple",
"success": "Succès",
"help": "Aide",
"idea": "Idée",
"pending": "En attente",
"security": "Sécurité",
"beta": "Bêta",
"best-practice": "Bonne pratique"
}
}

Si vous lancez l’application avec cette traduction, l’interface sera affichée en français. Vous pouvez ajouter d’autres langues en créant des fichiers de traduction similaires, par exemple es-ES.json pour l’espagnol.

Créer un fichier d’aide personnalisé

Pour ajouter un fichier d’aide personnalisé, éditez le fichier chainlit.md présent à la racine de votre projet. Ce fichier sera affiché dans l’onglet “Documentation” de l’interface. Vous pouvez y inclure des instructions, des guides d’utilisation, ou toute autre information utile pour les utilisateurs de votre application.

Un exemple de contenu pour chainlit.md :

# Bienvenue dans l'application Chainlit
Cette application est conçue pour vous aider à interagir avec notre assistant IA.
## Comment utiliser l'application
1. Tapez votre message dans la zone de saisie en bas de l'écran.
2. Cliquez sur "Envoyer" ou appuyez sur Entrée pour envoyer votre message.
3. Vous pouvez également utiliser les boutons d'action pour
interagir avec l'assistant.

Logo, favicon et visuel de connexion

Pour intégrer votre identité visuelle, déposez les fichiers suivants dans le dossier public/ :

  • logo_light.png et logo_dark.png : adaptés au thème clair ou sombre.
  • favicon.ico, .png ou .svg : affiché dans l’onglet du navigateur.

Vous pouvez aussi ajouter une image de fond pour la page de connexion :

[UI]
login_page_image = "/public/custom-bg.png"
login_page_image_filter = "brightness-50 grayscale"
login_page_image_dark_filter = "contrast-200 blur-sm"

Le filtre CSS applique un effet visuel directement dans l’interface (grayscale, sepia, blur, etc.).

aperçu de l'application avec logo et favicon

Thème personnalisé

Le fichier theme.json dans le dossier public/ vous permet de redéfinir l’apparence globale de Chainlit. Exemple :

{
"custom_fonts": ["https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap"],
"variables": {
"light": {
"--primary": "220 90% 56%",
"--background": "0 0% 100%",
"--foreground": "0 0% 10%"
},
"dark": {
"--primary": "220 90% 56%",
"--background": "0 0% 10%",
"--foreground": "0 0% 90%"
}
}
}

Ce fichier permet de définir :

  • des polices personnalisées (Google Fonts, etc.)
  • des variables CSS pour les couleurs principales, arrière-plan, texte, etc.

Pour un contrôle total, ajoutez un fichier CSS et référencez-le dans chainlit.toml :

[UI]
custom_css = "/public/style.css"

Ce fichier vous permet de redéfinir les bordures, polices, marges, tailles d’éléments, ou masquer certains composants. Vous pouvez, par exemple, supprimer le champ “README” ou changer la disposition des messages.

#theme-toggle,
#new-chat-button {
display: none !important;
}
.watermark {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
height: 0px !important;
width: 0px !important;
overflow: hidden !important;
}
#chat-input:empty::before {
content: 'Demandez-moi n’importe quoi';
}

Dans cet exemple, nous masquons le bouton de changement de thème et le bouton de nouvelle conversation, ainsi que le watermark. Nous modifions aussi le placeholder du champ de saisie pour afficher “Ask anything” au lieu de “Type your message here…”.

Astuce : utilisez l’inspecteur de votre navigateur pour identifier les sélecteurs CSS à surcharger.

JavaScript personnalisé

Vous pouvez injecter du JavaScript dans l’interface :

[UI]
custom_js = "/public/script.js"

Cela permet d’ajouter :

  • du tracking analytics (Google Tag, Matomo…)
  • des interactions client dynamiques
  • des appels à des APIs externes
  • ou des événements personnalisés (logique métier, switch UI, etc.)

Avatars personnalisés

Pour remplacer les avatars par défaut :

  1. Déposez vos images dans public/avatars/
  2. Nommez les fichiers selon le nom de l’auteur du message, ex. my_bot.png pour name="My Bot"

Chainlit détectera automatiquement le bon avatar dans les conversations.

Intégration avec LangChain

Associer LangChain à Chainlit permet de bâtir des assistants IA puissants, capables de suivre une logique conversationnelle avancée tout en offrant une interface utilisateur réactive et interactive.

LangChain fournit la logique métier et le chaînage des outils LLM, tandis que Chainlit sert d’interface frontale permettant :

  • d’afficher les messages utilisateurs et IA,
  • de visualiser les étapes internes des chaînes,
  • de gérer les interactions UI (boutons, fichiers, sliders…),
  • de streamer la réponse au fil de son élaboration.

Cette complémentarité vous permet de créer des assistants bien plus interactifs qu’une simple API ou CLI.

Prérequis

Avant de commencer, assurez-vous d’avoir installé les bibliothèques. Pour cela, créer le fichier requirements.txt avec le contenu suivant :

# Chainlit - Framework de chat UI
chainlit>=2.6.2
# OpenAI et LangChain
openai>=1.0.0
langchain>=0.1.0
langchain-openai>=0.1.0
langchain-community>=0.1.0

Installez-les avec :

Terminal window
pip install -r requirements.txt

Ajoutez dans le fichier .env la clé de votre API OpenAI :

Terminal window
OPENAI_API_KEY=your_api_key_here

Voici un exemple simple d’intégration de LangChain avec Chainlit pour créer un chatbot capable de répondre à des questions sur un sujet donné :

import chainlit as cl
from typing import Dict, Optional
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema import StrOutputParser
from langchain.schema.runnable import Runnable
from langchain.schema.runnable.config import RunnableConfig
from typing import cast
@cl.oauth_callback
def oauth_callback(
provider_id: str,
token: str,
raw_user_data: Dict[str, str],
default_user: cl.User,
) -> Optional[cl.User]:
# Autoriser tous les utilisateurs qui ont passé l'authentification GitHub
if provider_id == "github":
return default_user
return None
@cl.on_chat_start
async def on_chat_start():
# Récupérer l'utilisateur connecté
user = cl.user_session.get("user")
if user:
username = user.identifier
provider = user.metadata.get("provider", "inconnu")
welcome_message = f"Bienvenue {username} ! Vous êtes connecté via {provider}."
else:
welcome_message = "Bienvenue !"
# Initialiser le modèle OpenAI avec streaming
model = ChatOpenAI(
model="gpt-3.5-turbo",
temperature=0.7,
streaming=True
)
# Créer le prompt template
prompt = ChatPromptTemplate.from_messages([
(
"system",
"Vous êtes un assistant IA très compétent et serviable. "
"Répondez de manière claire, précise et utile aux questions des utilisateurs. "
"Utilisez un ton amical et professionnel."
),
("human", "{question}"),
])
# Créer la chaîne runnable
runnable = prompt | model | StrOutputParser()
# Stocker la chaîne dans la session utilisateur
cl.user_session.set("runnable", runnable)
# Envoyer le message de bienvenue
await cl.Message(content=f"{welcome_message}\n\nJe suis prêt à répondre à vos questions !").send()
@cl.on_message
async def on_message(message: cl.Message):
# Récupérer la chaîne runnable de la session
runnable = cast(Runnable, cl.user_session.get("runnable"))
# Créer un message vide qui sera rempli progressivement
msg = cl.Message(content="")
try:
# Utiliser le streaming pour une réponse progressive
async for chunk in runnable.astream(
{"question": message.content},
config=RunnableConfig(callbacks=[cl.LangchainCallbackHandler()]),
):
await msg.stream_token(chunk)
# Envoyer le message final
await msg.send()
except Exception as e:
# Gérer les erreurs
error_message = f"Désolé, une erreur s'est produite : {str(e)}"
await cl.Message(content=error_message).send()

Ce code crée un assistant IA qui répond aux questions des utilisateurs en utilisant un modèle OpenAI. Il utilise le streaming pour envoyer la réponse au fur et à mesure qu’elle est générée, offrant ainsi une expérience utilisateur fluide et interactive.

aperçu de l'application avec LangChain

Conclusion

Ce guide est une introduction complète à Chainlit, conçue pour vous aider à prendre en main rapidement cette bibliothèque, de l’installation à l’intégration avec LangChain. Mais ce n’est que le début.

Dans les prochaines semaines, je publierai un tutoriel avancé basé sur ce socle, incluant :

  • La mise en place de l’historique des conversations,
  • une architecture complète avec RAG (Retrieval-Augmented Generation),
  • l’ingestion de documents PDF/Markdown dans une base vectorielle (OpenSearch, PostgreSQL Vector, etc.),
  • l’extraction contextuelle et le chaînage via LangChain,
  • la persistance des sessions utilisateurs et des métadonnées,
  • une interface Chainlit customisée, interactive et sécurisée.

L’objectif : créer un chatbot documenté et industrialisable, capable de répondre à des questions sur une base de connaissances métier, avec logs, permissions, et outils en production.