<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Premier Octet</title>
        <link>https://www.premieroctet.com</link>
        <description>Premier Octet est une agence de développement d'applications Web, mobiles et IA basée à Paris, experte en React</description>
        <lastBuildDate>Mon, 31 Aug 2026 14:37:10 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>fr</language>
        <image>
            <title>Premier Octet</title>
            <url>https://www.premieroctet.com/images/logo-600x600.png</url>
            <link>https://www.premieroctet.com</link>
        </image>
        <copyright>All rights reserved 2026, Premier Octet</copyright>
        <atom:link href="https://www.premieroctet.com/atom.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Medusa en 2026 : Comment l'architecture modulaire et l'IA redéfinissent l'e-commerce]]></title>
            <link>https://www.premieroctet.com/blog/medusa-2026-architecture-modulaire-ia-ecommerce</link>
            <guid>https://www.premieroctet.com/blog/medusa-2026-architecture-modulaire-ia-ecommerce</guid>
            <pubDate>Tue, 21 Jul 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Medusa en 2026 : architecture modulaire, ingénierie agentic et plugins open-source pour repenser l'e-commerce.]]></description>
            <content:encoded><![CDATA[<p>Longtemps, les solutions e-commerce clés en main type Shopify ont séduit par leur simplicité et leur rapidité de mise sur le marché. Pourtant, le modèle montre aujourd&#x27;hui ses limites. Marges rognées par les commissions, coûts qui flambent et écosystème fermé finissent souvent par freiner les marchands dans leur évolution. C&#x27;est dans cette quête de flexibilité et de maîtrise des coûts que <a href="https://medusajs.com/">Medusa</a> a récemment commencé à tirer son épingle du jeu.</p>
<p>Chez Premier Octet, nous suivons cette évolution de près. En octobre 2023, nous décryptions déjà la V1 de Medusa dans un <a href="https://www.premieroctet.com/blog/e-commerce-headless-avec-medusa-js">premier article sur notre blog</a>. Trois ans plus tard, les annonces de ce printemps/été 2026 confirment cette tendance : l&#x27;avenir appartient aux architectures modulaires et nativement pensées pour le développement assisté par IA. C&#x27;est donc l&#x27;occasion d&#x27;un nouvel article pour faire le tour de ces nouveautés qui redéfinissent notre façon de concevoir l&#x27;e-commerce.</p>
<h2>1. L&#x27;ingénierie « Agentic » : quand l&#x27;IA s&#x27;intègre au workflow de développement</h2>
<p>La force de Medusa, c&#x27;est d&#x27;accompagner un changement de paradigme fondamental du secteur de l&#x27;e-commerce. Là où le marché s&#x27;est longtemps standardisé autour de templates et de sites clés en main pour simplifier les lancements, la tendance actuelle exige une personnalisation absolue, désormais accompagnée par l&#x27;IA.</p>
<p>C&#x27;est tout l&#x27;objet de l&#x27;AI Toolbox de Medusa : un écosystème d&#x27;outils qui cadre et sécurise l&#x27;intervention des agents pour leur permettre de bâtir des fonctionnalités sur mesure en un simple prompt.</p>
<h3>Agent Skills Library : cadrer l&#x27;IA avec l&#x27;expertise native de Medusa</h3>
<p>Pour guider efficacement les agents et éviter qu&#x27;ils ne dévient des standards du framework, Medusa fournit sa propre bibliothèque de skills. Pour l&#x27;ajouter à votre projet, il suffit d&#x27;exécuter :</p>
<pre><code class="language-bash">npx skills add medusajs/medusa-agent-skills
</code></pre>
<p>Trois grandes familles de compétences sont disponibles dans le contexte de votre agent :</p>
<ul>
<li><strong>Admin &amp; Dashboard</strong> (<code>building-with-medusa</code>, <code>building-admin-dashboard-customizations</code>) : fournit les connaissances fondamentales de l&#x27;architecture Medusa pour créer des features e-commerce personnalisées (wishlist, intégration de services externes…) et étendre l&#x27;UI de l&#x27;admin.</li>
<li><strong>Storefront &amp; UX</strong> (<code>building-storefronts</code>, <code>storefront-best-practices</code>) : rassemble les bonnes pratiques e-commerce (SEO, UX) pour bâtir votre boutique indépendamment du framework choisi (Next.js, Vite).</li>
<li><strong>Store Operations &amp; Automatisation</strong> (<code>creating-internal-agents</code>) : permet de configurer des agents autonomes dédiés à la gestion quotidienne de votre site (audit automatique des stocks, support client personnalisé).</li>
</ul>
<p><img src="https://www.premieroctet.com/blog/medusa-2026-architecture-modulaire-ia-ecommerce/Skills.webp" alt="Bibliothèque Agent Skills de Medusa" title="Agent Skills Library"/></p>
<h3>Un plugin et un tutoriel interactif sur Claude Code</h3>
<p>Medusa s&#x27;intègre directement dans le CLI d&#x27;Anthropic via deux extensions spécifiques : si <code>medusa-dev</code> sert à générer des fonctionnalités à la volée, le framework se démarque surtout par un onboarding interactif inédit avec <code>learn-medusa@medusa</code>. En tapant la commande <code>/learn-medusa:learning-medusa</code> ou le prompt « I want to learn Medusa », l&#x27;IA se transforme en mentor et vous guide pas à pas dans un cas pratique réel.</p>
<pre><code class="language-bash"># Démarrez Claude Code et ajoutez le dépôt Medusa
claude
/plugin marketplace add medusajs/medusa-claude-plugins

# Features
/plugin install medusa-dev@medusa

# Tutoriel
/plugin install learn-medusa@medusa
</code></pre>
<h3>Un plugin ESLint adapté à Medusa</h3>
<p>Les linters classiques ignorent les conventions de Medusa. Résultat : un code TypeScript valide peut quand même crasher au runtime (ex: données non sérialisables, mauvais nommage de route). C&#x27;est là qu&#x27;intervient <code>@medusajs/eslint-plugin</code>. Si le code écrit est non conforme, le linter lève une erreur claire, fini le debug fastidieux dans les logs. Le plugin est inclus par défaut depuis la v2.16.0, et embarque trois presets : recommended (standard), strict (typé) et modules (packages).</p>
<h2>2. Nouvelles fonctionnalités et plugins au service du sur-mesure</h2>
<p>L&#x27;offre open-source de Medusa s&#x27;étoffe avec l&#x27;arrivée de nouvelles fonctionnalités et plugins majeurs.</p>
<h3>Global Product Options et Multi-shipping : la fin des limites du catalogue</h3>
<p>La mise à jour de juin 2026 apporte l&#x27;une des fonctionnalités les plus réclamées par la communauté : les <strong>Global Product Options</strong>. Il est désormais possible de créer des options réutilisables, de centraliser leurs valeurs et de permettre aux utilisateurs de filtrer les produits par attributs communs directement sur le storefront.</p>
<p><img src="https://www.premieroctet.com/blog/medusa-2026-architecture-modulaire-ia-ecommerce/options.webp" alt="Product options dans l&#x27;admin Medusa" title="Product Options"/></p>
<p>Côté checkout, le framework repousse encore les limites avec la prise en charge des paniers <strong>Multi-shipping</strong>. Une commande peut désormais inclure plusieurs méthodes d&#x27;expédition réparties sur différents profils de livraison.</p>
<h3>Loyalty Plugin : la fidélisation passe au 100 % open-source</h3>
<p>Initialement réservé aux utilisateurs de Medusa Cloud, le Loyalty Plugin fait son entrée dans la version open-source standard (v2.14.0). Divisé en deux modules autonomes (Loyalty et Store Credit), cet outil offre aux marchands une gestion centralisée des cartes cadeaux et crédits d&#x27;achat. Fini les abonnements coûteux à des SaaS tiers. Les développeurs disposent désormais de briques natives pour coder des portefeuilles virtuels (wallets), des systèmes de cashback ou des programmes VIP à plusieurs niveaux, avec une liberté totale sur la logique métier.</p>
<p><img src="https://www.premieroctet.com/blog/medusa-2026-architecture-modulaire-ia-ecommerce/Loyalty-Plugin.webp" alt="Loyalty Plugin dans l&#x27;admin Medusa" title="Loyalty Plugin"/></p>
<h3>Shopify Sync : le plugin qui simplifie la transition depuis Shopify</h3>
<p>L&#x27;engouement autour de Medusa comme alternative à Shopify se confirme avec le plugin <code>@rx-ventures/medusa-plugin-shopify-sync</code>. Conçu pour les migrations ou les architectures hybrides, cet outil automatise le rapatriement complet du catalogue (clients, produits, commandes, remises) de Shopify vers Medusa v2.</p>
<pre><code class="language-bash"># 1. Installer le plugin Shopify Sync
yarn add @rx-ventures/medusa-plugin-shopify-sync

# 2. Générer la clé de chiffrement pour sécuriser le token Shopify
openssl rand -hex 32
# -&gt; À copier dans le .env : SHOPIFY_SYNC_ENCRYPTION_KEY=...

# 3. Exécuter la migration pour créer les tables de suivi
yarn medusa db:migrate
</code></pre>
<p>Une fois le plugin configuré, tout se pilote depuis le tableau de bord Medusa :</p>
<ul>
<li><strong>Configuration &amp; synchronisation</strong> : liaison de la boutique via l&#x27;onglet dédié et import historique en un clic dans l&#x27;ordre recommandé (clients, produits, remises, puis commandes).</li>
<li><strong>Webhooks &amp; logs</strong> : activation instantanée des routes en temps réel pour synchroniser les stocks et ventes, avec un écran de monitoring rafraîchi toutes les 5 secondes pour suivre l&#x27;activité.</li>
</ul>
<h3>Une sécurité renforcée : Authentification multifacteur (MFA)</h3>
<p>Le module d&#x27;authentification de Medusa intègre désormais le MFA en natif. L&#x27;implémentation repose sur des fournisseurs configurables, notamment le support du TOTP pour les applications d&#x27;authentification (Google Authenticator, etc.). Cette approche modulaire permet de sécuriser les accès — en particulier pour les comptes administrateurs — tout en offrant un contrôle précis sur les flux de vérification et les politiques d&#x27;accès.</p>
<h2>3. Infrastructure et production : la maturité de Medusa Cloud</h2>
<p>L&#x27;un des grands avantages d&#x27;une architecture découplée avec Medusa réside dans la liberté de déploiement. Le framework reste une application Node.js standard, hébergeable sur l&#x27;infrastructure de votre choix (AWS, GCP, Railway, etc.), ce qui garantit une maîtrise totale des coûts d&#x27;hébergement.</p>
<p>Toutefois, pour les équipes qui souhaitent déléguer la partie DevOps sans subir les blocages des SaaS traditionnels, Medusa Cloud s&#x27;est doté d&#x27;outils plus matures pour la production. Sans être un passage obligé, la solution managée intègre désormais :</p>
<ul>
<li><strong>Un accès direct à Postgres</strong> : l&#x27;ouverture de la base de données en écriture permet d&#x27;exécuter des scripts complexes ou de brancher des outils d&#x27;analytique en direct.</li>
</ul>
<p><img src="https://www.premieroctet.com/blog/medusa-2026-architecture-modulaire-ia-ecommerce/DBWriteAccess.webp" alt="Accès en écriture à la base de données sur Medusa Cloud"/></p>
<ul>
<li><strong>Une télémétrie avancée</strong> : des dashboards natifs pour monitorer la latence des routes API et anticiper les pics de charge.</li>
</ul>
<p><img src="https://www.premieroctet.com/blog/medusa-2026-architecture-modulaire-ia-ecommerce/CloudMonitoring.webp" alt="Dashboard de télémétrie Medusa Cloud"/></p>
<ul>
<li><strong>Un CLI dédié</strong> : avec <code>@medusajs/mcloud</code>, le pilotage des environnements et le suivi des logs se font directement depuis le terminal.</li>
</ul>
<p>Que l&#x27;on gère sa propre infrastructure ou que l&#x27;on opte pour leur cloud, le framework fournit désormais tous les outils pour gérer sereinement la mise en production et la montée en charge.</p>
<h2>En conclusion</h2>
<p>Dès 2023, Medusa apparaissait déjà à nos yeux comme une alternative prometteuse au monopole des solutions propriétaires. Trois ans plus tard, le framework a non seulement tenu ses promesses de flexibilité, mais il a pris une longueur d&#x27;avance décisive.</p>
<p>Les hausses de prix et les barrières techniques des plateformes SaaS ne sont plus une fatalité. Avec l&#x27;intégration de l&#x27;IA dans son workflow et un écosystème open-source toujours plus riche, Medusa confirme son statut d&#x27;alternative de choix face à Shopify. Le framework redonne le contrôle aux développeurs pour construire des architectures sur mesure, le tout sans la moindre commission sur les ventes.</p>
<p>Votre plateforme actuelle freine vos ambitions ? Chez Premier Octet, nous vous accompagnons dans la transition vers une architecture e-commerce performante, libre et scalable. <a href="https://www.premieroctet.com/contact">Contactez-nous pour discuter de votre projet</a>.</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/medusa-2026-architecture-modulaire-ia-ecommerce/illu.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Créer un RAG minimaliste avec Gemini et Google Drive]]></title>
            <link>https://www.premieroctet.com/blog/creer-un-rag-avec-gemini-et-google-drive</link>
            <guid>https://www.premieroctet.com/blog/creer-un-rag-avec-gemini-et-google-drive</guid>
            <pubDate>Mon, 22 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Créer un RAG minimaliste avec Gemini et Google Drive]]></description>
            <content:encoded><![CDATA[<p>La plupart des tutoriels « construis ton RAG » vous noient sous les bases de
données vectorielles, les stratégies de découpage et les comparaisons de
rerankers. Tout cela peut être très simplifié avec Google Workspace qui fournit déjà les trois briques d&#x27;un RAG, et la
colle tient en quelques heures.</p>
<p>Ce billet déroule une petite expérience : une web app qui vous connecte avec
Google, vous laisse choisir des fichiers dans votre Drive, les indexe avec Gemini, puis discute avec eux.</p>
<p><img src="https://www.premieroctet.com/blog/creer-un-rag-avec-gemini-et-google-drive/gemini-rag.png" alt="RAG" title="Démonstration du RAG"/></p>
<h2>C&#x27;est quoi un RAG, vraiment ?</h2>
<p><strong>Retrieval-Augmented Generation</strong>, dans ce contexte c&#x27;est en fait deux étapes :</p>
<ol>
<li><strong>Récupérer</strong> — étant donné une question, trouver les passages de vos
documents les plus susceptibles de contenir la réponse.</li>
<li><strong>Générer</strong> — coller ces passages dans le prompt et laisser le LLM
formuler la réponse.</li>
</ol>
<p>Habituellement, nous avons besoin de : découper les documents
en chunks, les transformer en vecteurs, stocker ces vecteurs dans une base,
et classer les résultats à la requête. Chacune de ces étapes a son lot
d&#x27;articles qui débattent de la « bonne » manière.</p>
<p>Le raccourci ici est de laisser Gemini tout faire. L&#x27;API Gemini expose un
<code>FileSearchStore</code> — vous uploadez des fichiers, Google s&#x27;occupe du
découpage, des embeddings et de la récupération. Au moment de la requête,
vous passez le store comme un outil, et le modèle décide quand chercher et
quoi faire des résultats.</p>
<h2>Trois dépendances, un seul fournisseur</h2>
<p>Toute la stack tient dans Google Workspace :</p>
<ul>
<li><strong>API Gemini</strong> — le LLM et le File Search Store. Une clé API.</li>
<li><strong>OAuth Google</strong> — pour que l&#x27;utilisateur autorise l&#x27;app à lire son
Drive. Un client OAuth.</li>
<li><strong>API Google Drive</strong> — pour lister et télécharger les fichiers. Le même
client OAuth.</li>
</ul>
<p>Pas de <a href="https://www.pinecone.io/">Pinecone</a>, pas de <a href="https://www.langchain.com/">LangChain</a>, pas de modèle d&#x27;embedding à choisir. La
seule « infrastructure » nécessaire est une mémoire pour savoir quels fichiers Drive ont été indexés et le nom du store.</p>
<p>Pour le framework j&#x27;ai choisi <a href="https://tanstack.com/start/latest">TanStack Start</a> — un framework React
full-stack où la logique serveur n&#x27;est qu&#x27;une fonction typée que le client
appelle directement. Pas de routes REST à écrire, pas de frontière
client/serveur à concevoir. Une server function ressemble à ça :</p>
<pre><code class="language-ts">const chat = createServerFn({ method: &#x27;POST&#x27; })
  .validator((d: { question: string }) =&gt; d)
  .handler(async ({ data }) =&gt; ask(data.question))
</code></pre>
<p>Et côté client :</p>
<pre><code class="language-ts">await chat({ data: { question: &#x27;...&#x27; } })
</code></pre>
<p>C&#x27;est toute
la couche RPC.</p>
<h2>Étape 1 : chercher dans Drive</h2>
<p>Une fois l&#x27;utilisateur connecté en OAuth (avec le scope <code>drive.readonly</code>),
lister les fichiers tient en un appel :</p>
<pre><code class="language-ts">const res = await drive.files.list({
  q: &quot;trashed = false&quot;,
  pageSize: 50,
  fields: &#x27;files(id, name, mimeType, modifiedTime)&#x27;,
  orderBy: &#x27;modifiedTime desc&#x27;,
})
</code></pre>
<p>La seule subtilité, ce sont les types MIME. Les PDF et fichiers texte
peuvent se télécharger tels quels (<code>alt: &#x27;media&#x27;</code>), mais les Google Docs,
Sheets et Slides sont virtuels — il faut les <em>exporter</em> d&#x27;abord :</p>
<pre><code class="language-ts">const GOOGLE_DOC_EXPORTS = {
  &#x27;application/vnd.google-apps.document&#x27;:     { mimeType: &#x27;application/pdf&#x27; },
  &#x27;application/vnd.google-apps.presentation&#x27;: { mimeType: &#x27;application/pdf&#x27; },
  &#x27;application/vnd.google-apps.spreadsheet&#x27;:  { mimeType: &#x27;text/csv&#x27; },
}
</code></pre>
<p>Un Google Doc est exporté en PDF, un Sheet en CSV. Gemini File Search gère
les deux nativement.</p>
<h2>Étape 2 : indexer un fichier</h2>
<p>Voilà la partie magique. Indexer, c&#x27;est <em>uploader + attendre</em> :</p>
<pre><code class="language-ts">const ai = new GoogleGenAI({ apiKey })

// Crée le store une fois, à la première utilisation
const store = await ai.fileSearchStores.create({
  config: { displayName: &#x27;gemini-rag&#x27; },
})

// Upload d&#x27;un d&#x27;un fichier Drive
let op = await ai.fileSearchStores.uploadToFileSearchStore({
  file: new Blob([bytes], { type: mimeType }),
  fileSearchStoreName: store.name,
  config: { displayName: name, mimeType },
})

// L&#x27;indexation est asynchrone
while (!op.done) {
  await new Promise(r =&gt; setTimeout(r, 2000))
  op = await ai.operations.get({ operation: op })
}
</code></pre>
<p>Voilà tout le pipeline d&#x27;indexation. Google découpe le fichier, calcule les
embeddings et les écrit dans le store. En coulisses c&#x27;est la même mécanique
qu&#x27;une base vectorielle — sauf que vous n&#x27;avez pas à l&#x27;opérer.</p>
<p>Le seul état qu&#x27;on garde nous-mêmes est un petit fichier JSON qui mappe
<code>{ driveId → { name, mimeType } }</code>. Il existe pour deux raisons : éviter de
réindexer des fichiers déjà uploadés, et retenir le nom du store entre les
redémarrages du serveur.</p>
<h2>Étape 3 : discuter</h2>
<pre><code class="language-ts">const res = await ai.models.generateContent({
  model: &#x27;gemini-2.5-flash&#x27;,
  contents: question,
  config: {
    tools: [{ fileSearch: { fileSearchStoreNames: [store.name] } }],
  },
})
return res.text
</code></pre>
<p>Voilà tout le handler de chat. On donne à Gemini la question et un pointeur
vers le store. Gemini décide s&#x27;il faut chercher, exécute la récupération,
choisit les chunks pertinents et écrit la réponse.</p>
<p>Si vous avez déjà construit un RAG à la main, c&#x27;est le moment qui ressemble
à de la triche.</p>
<h2>Ce que ce n&#x27;est pas</h2>
<p>Soyons honnête, nous sommes assez éloigné d&#x27;un projet de production, cependant la logique principale du RAG est présente et fonctionnelle.</p>
<p>Une solution complète aurait besoin de :</p>
<ul>
<li>un stockage en base de donnée</li>
<li>une synchronisation automatique et récursive des dossiers Drive</li>
<li>une interface de tchat complète, multimodale et streamée (https://ai-sdk.dev/)</li>
</ul>
<h2>À retenir</h2>
<p>Si vous construisez un RAG pour un outil interne et que vous êtes déjà dans
l&#x27;écosystème Google, vous n&#x27;avez presque certainement pas besoin de monter
une base vectorielle. À savoir que le free tier de l&#x27;<a href="https://ai.google.dev/gemini-api/docs/file-search#rate-limits">API FileSearch</a> permet un stockage de 1 GB.</p>
<p>Si vous cherchez à discuter avec vos documents avec de l&#x27;AI, aussi dans l&#x27;écosystème Google, je vous recommande chaudement <a href="https://notebooklm.google.com">NotebookLM</a> qui permet en plus de générer du contenu (résumé audio, vidéo, quiz, infographie...) en rapport avec vos documents.</p>
<p>L&#x27;ensemble du code concernant le RAG tient sur ces 500 lignes de code et est disponible sur <a href="https://gist.github.com/tlenclos/a6275becf6aaab82b45b978e8408dceb">GitHub</a>.</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/creer-un-rag-avec-gemini-et-google-drive/cover.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Directus MCP : comment utiliser le Model Context Protocol avec un LLM]]></title>
            <link>https://www.premieroctet.com/blog/directus-mcp-comment-utiliser-le-model-context-protocol-avec-un-llm</link>
            <guid>https://www.premieroctet.com/blog/directus-mcp-comment-utiliser-le-model-context-protocol-avec-un-llm</guid>
            <pubDate>Wed, 10 Jun 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Découvrez le serveur MCP natif de Directus : un game changer ou une simple couche conversationnelle ? Analyse et retour d'expérience.]]></description>
            <content:encoded><![CDATA[<p>En décembre dernier, les équipes de <strong>Directus</strong> ont annoncé l&#x27;arrivée d&#x27;un serveur MCP natif dans leur écosystème.</p>
<p>La promesse est la suivante : permettre à n&#x27;importe quel LLM d&#x27;interagir directement avec une instance Directus de manière conversationnelle.</p>
<p>Mais qu&#x27;est-ce que ce MCP apporte de plus par rapport à l&#x27;API Directus existante, déjà très complète ? Découverte de ce MCP en action.</p>
<h2>Qu&#x27;est-ce que le MCP ?</h2>
<p>Le <em><strong>Model Context Protocol</strong></em> (MCP) est une norme qui permet de standardiser la manière dont un LLM peut interagir avec une application. Il expose les capacités, les actions et les contraintes de cette dernière de façon lisible par un LLM, tout en respectant un cadre strict de permissions et de schéma.
Nous avions déjà abordé ce sujet dans un article précédent : <a href="https://www.premieroctet.com/blog/guide-pratique-du-model-context-protocol-tout-comprendre-sur-les-tools-et-serveurs-mcp">Guide pratique du Model Context Protocol : tout comprendre sur les tools et serveurs MCP</a>.</p>
<h2>Le MCP de Directus</h2>
<p>Le serveur MCP de Directus expose ses capacités sous forme de <strong>tools</strong>, automatiquement découvrables par le LLM. Cela permet à n&#x27;importe quel LLM de lire des collections, modifier des schémas, gérer du contenu ou naviguer dans des relations complexes.</p>
<p>Dans ces tools on retrouve notamment :</p>
<ul>
<li>La gestion des collections et champs</li>
<li>Le CRUD (Create, Read, Update, Delete) sur les items</li>
<li>La manipulation des assets</li>
<li>La lecture des permissions et rôles</li>
<li>Certaines opérations avancées liées au schéma.</li>
</ul>
<p>La liste complète est disponible dans la <a href="https://directus.io/docs/guides/ai/mcp/tools"><strong>documentation officielle</strong></a>, mais un point mérite d&#x27;être souligné : 👉 <strong>rien ici n&#x27;est fondamentalement nouveau</strong> par rapport à l&#x27;API Directus existante.
<img src="https://www.premieroctet.com/blog/directus-mcp-comment-utiliser-le-model-context-protocol-avec-un-llm/all-tools.png" alt="Liste des tools disponibles"/></p>
<p>La vraie valeur ajoutée du MCP réside dans l&#x27;interaction avec le LLM qui peut raisonner sur votre instance Directus en temps réel, orchestrer des migrations, etc.</p>
<div type="info"><p>Le revers du MCP, c&#x27;est la consommation de tokens. Chaque interaction embarque du contexte, des <strong>tools</strong>, leurs descriptions, et souvent plusieurs appels successifs pour une seule intention. <a href="https://www.anthropic.com/engineering/code-execution-with-mcp">Le contexte peut plus facilement se retrouver surchargé.</a>
Le MCP excelle dans les usages conversationnels, ponctuels, exploratoires ou de prototypage.
En revanche, il devient rapidement discutable pour des workflows récurrents, de l&#x27;automatisation silencieuse ou des traitements batch.</p><p>Dans ces cas-là, une intégration plus directe, comme via l&#x27;API, hooks ou scripts d&#x27;automatisation, reste généralement plus efficace et plus prévisible.</p></div>
<h2>Comment utiliser le MCP de Directus</h2>
<p>Il existe <a href="https://directus.io/docs/guides/ai/mcp/installation">une documentation très claire et explicite</a> ainsi que des tutoriels vidéo sur le sujet mais globalement pour le setup vous aurez besoin de :</p>
<ul>
<li>Une instance de Directus &gt;v11.2</li>
<li>Un client AI (Cursor, Claude Desktop, Copilot, ChatGPT, etc.)</li>
<li>Un (nouvel) utilisateur dans votre instance avec les permissions adéquates (ainsi que son token).</li>
</ul>
<div type="warning"><p>Le MCP est <strong>désactivé par défaut</strong> et doit être explicitement activé dans la configuration.</p></div>
<p><img src="https://www.premieroctet.com/blog/directus-mcp-comment-utiliser-le-model-context-protocol-avec-un-llm/mcp-settings-page-enable.png" alt="Configuration du MCP dans Directus"/></p>
<p>Une fois le serveur MCP configuré côté client IA, un premier test très simple consiste à poser une question ouverte sur l&#x27;instance :
&quot;Can you tell me about my Directus project?&quot;</p>
<p>Si la configuration est correcte, le LLM est capable :</p>
<ul>
<li>d&#x27;interroger les collections</li>
<li>de comprendre le schéma</li>
<li>et de restituer une vue cohérente du projet.</li>
</ul>
<p><img src="https://www.premieroctet.com/blog/directus-mcp-comment-utiliser-le-model-context-protocol-avec-un-llm/directus-ask.png" alt="Can you tell me about my Directus project"/></p>
<h2>Test du MCP</h2>
<p>Pour tester le MCP dans un cas concret, j&#x27;ai ajouté une fonctionnalité de tags à un blog existant. L&#x27;objectif était double :</p>
<ul>
<li>créer un nouveau modèle <code>tags</code></li>
<li>analyser le contenu des articles déjà publiés afin d&#x27;y associer automatiquement un ou des tags pertinents.</li>
</ul>
<p><img src="https://www.premieroctet.com/blog/directus-mcp-comment-utiliser-le-model-context-protocol-avec-un-llm/tag-feature-input.png" alt="Fonctionnalité de tags"/></p>
<p>Le test combine plusieurs dimensions : création de schéma, compréhension sémantique du contenu et écriture de relations existantes. Autrement dit, exactement le type de tâche que le MCP prétend simplifier.</p>
<p>Une petite minute plus tard, j&#x27;avais mon nouveau modèle ainsi que mes articles de blog mis à jour.</p>
<p><img src="https://www.premieroctet.com/blog/directus-mcp-comment-utiliser-le-model-context-protocol-avec-un-llm/tag-feature-result.png" alt="Résultat de la fonctionnalité de tags"/></p>
<h2>Use cases pertinents</h2>
<p>Pour aller au-delà du simple effet démo, j&#x27;ai recherché et identifié quelques usages concrets du MCP qui pourraient peut-être convaincre les sceptiques.</p>
<h3>Côté contenu</h3>
<p>Le MCP permet surtout à des profils non techniques de gagner en autonomie. L&#x27;interface conversationnelle réduit fortement la courbe d&#x27;apprentissage de Directus et limite les allers-retours avec les équipes techniques, notamment lors de l&#x27;onboarding ou de tâches ponctuelles mais pénibles.</p>
<ul>
<li>Migration d&#x27;articles depuis Google Docs, Notion ou Markdown brut</li>
<li>Renommage et restructuration d&#x27;assets média</li>
<li>Reprise ou duplication de structures existantes</li>
<li>Recherche conversationnelle dans un catalogue éditorial</li>
</ul>
<blockquote>
<p>“Retrouve-moi l&#x27;article qui parle du produit X publié l&#x27;année dernière, même si le titre exact ne contient pas le nom du produit.”</p>
</blockquote>
<h3>Côté développement</h3>
<p>Pour les profils techniques, le MCP ne remplace pas les outils classiques, mais accélère les phases de réflexion et de prototypage. Il permet d&#x27;explorer un schéma, tester des hypothèses ou ajuster des modèles sans écrire de code ni maîtriser immédiatement l&#x27;existant.</p>
<ul>
<li>Prototypage rapide de nouveaux modèles.</li>
<li>Exploration d&#x27;une base existante sans connaître son historique.</li>
<li>Migration fastidieuse de données.</li>
</ul>
<blockquote>
<p>“Explique-moi comment sont liées les collections projects, clients et invoices, et dis-moi où se trouve la source de vérité pour le statut de facturation.”</p>
</blockquote>
<p>Les usages sont très diversifiés, ce n&#x27;est pas un moteur d&#x27;automatisation, ni un remplaçant de l&#x27;API. Le MCP est surtout utile pour explorer, prototyper et intervenir ponctuellement sur une instance. Dans ces cas précis, il remplit bien sa promesse.</p>
<p>À noter que Directus n&#x27;est pas le seul CMS à explorer ce type d&#x27;intégration. D&#x27;autres CMS comme <a href="https://market.strapi.io/plugins/@sensinum-strapi-plugin-mcp"><strong>Strapi</strong></a> ou <a href="https://www.sanity.io/docs/ai/mcp-server"><strong>Sanity</strong></a> proposent également des intégrations basées sur le protocole MCP.</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/directus-mcp-comment-utiliser-le-model-context-protocol-avec-un-llm/illu.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Notre assistant IA pour Photobooth AI]]></title>
            <link>https://www.premieroctet.com/blog/ai-assistant-photobooth</link>
            <guid>https://www.premieroctet.com/blog/ai-assistant-photobooth</guid>
            <pubDate>Thu, 21 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Simplifier l’expérience utilisateur avec un assistant IA pour Photobooth.]]></description>
            <content:encoded><![CDATA[<p>Depuis quelques temps maintenant, chez Premier Octet, nous mettons en place des agents conversationnels adaptés à nos clients. Fort de cette expérience, nous avons décidé de mettre en place un agent dédié pour <a href="https://photobooth-ai.app/">Photobooth</a>: notre application de génération de photos avec l&#x27;intelligence artificielle.</p>
<p>Dans cet article, je partage avec vous comment nous avons mis en place cet agent conversationnel et pourquoi nous avons choisi cette approche pour gérer une partie de la relation client.</p>
<h2>Contexte</h2>
<p><a href="https://photobooth-ai.app/">Photobooth AI</a> est notre application qui permet de générer des photos personnalisées grâce à l&#x27;intelligence artificielle. Comme toute application SaaS, elle nécessite un support client efficace pour accompagner les utilisateurs dans leur découverte et utilisation de la plateforme.</p>
<p>Jusqu&#x27;à récemment, nous utilisions <a href="https://crisp.chat/">Crisp</a> comme solution de chat support traditionnelle. Cette approche fonctionnait, mais elle présentait certaines limitations : disponibilité limitée aux heures ouvrables, répétition des mêmes questions de base, et charge importante sur notre équipe support pour des demandes souvent similaires.</p>
<p>Nous avons également observé que certains utilisateurs tentaient d&#x27;utiliser le chat support directement pour générer leurs images. Ils y téléchargeaient leurs photos et rédigeaient leurs prompts, s&#x27;attendant à pouvoir obtenir leurs créations via ce canal. Cette approche, bien que témoignant de leur envie d&#x27;interagir facilement avec notre service, était sans issue et pouvait se révéler frustrante.</p>
<p>C&#x27;est en partant de ce constat et fort de notre expertise en développement d&#x27;agents conversationnels que nous avons décidé de développer un assistant IA spécialement conçu pour Photobooth AI. L&#x27;objectif ? Optimiser notre support client tout en améliorant l&#x27;expérience utilisateur.</p>
<h2>Un support automatisé et personnalisé : notre approche</h2>
<p>L&#x27;objectif principal de cette intégration est de <strong>créer un support client automatisé et personnalisé</strong> spécifiquement adapté à notre plateforme. Contrairement à une solution générique, notre assistant IA est configuré avec :</p>
<ul>
<li><strong>Le contenu complet de notre FAQ</strong> : Toutes les questions fréquentes et leurs réponses officielles</li>
<li><strong>Le plan du site</strong> (sitemap) : Pour guider efficacement les utilisateurs vers les bonnes sections</li>
<li><strong>Un prompt système spécialisé</strong> : Conçu spécifiquement pour les enjeux de Photobooth AI</li>
<li><strong>Un outil de feedback intégré</strong> : Qui transmet automatiquement les demandes complexes au support humain</li>
</ul>
<p>Cette approche nous permet de maintenir la qualité du support tout en automatisant une grande partie des interactions client. En somme, un système qui connaît parfaitement notre produit et sait orienter nos utilisateurs.</p>
<h2>Les avantages de cette approche</h2>
<p>Rentrons dans le détail des bénéfices de cette implémentation.</p>
<h3>1. Disponibilité en continu</h3>
<p>Le premier avantage est évident mais crucial : <strong>la disponibilité continue</strong>. Notre assistant répond instantanément aux questions des utilisateurs, qu&#x27;il soit 2h du matin ou pendant un week-end. Cette disponibilité 24/24h et 7/7j améliore considérablement l&#x27;expérience utilisateur et influence positivement les conversions.</p>
<p>Cette dimension internationale est particulièrement importante pour Photobooth AI : nos utilisateurs viennent du monde entier, créant des décalages horaires qui rendaient compliqué un support traditionnel aux heures ouvrables françaises.</p>
<p>Cette automatisation <strong>réduit significativement la charge sur notre équipe</strong>, qui peut désormais se concentrer sur les problèmes complexes nécessitant une intervention humaine. En d&#x27;autres termes, moins de temps passé sur les questions répétitives, plus de temps pour de réels problèmes et améliorations.</p>
<h3>2. Une expertise produit intégrée et toujours à jour</h3>
<p>Notre assistant possède une <strong>connaissance approfondie et toujours à jour</strong> de tous les aspects de Photobooth AI : système de crédits, création d&#x27;événements, gestion des styles de génération, tarification, etc. Cette expertise garantit des <strong>réponses cohérentes et précises</strong>, basées sur notre documentation officielle.</p>
<div class="u-txt-center" style="margin-top:20px"><video class="b-lazy b-loaded" autoplay="" loop="" muted="" title="Assistant Photobooth AI"><source src="https://www.premieroctet.com/blog/ai-assistant-photobooth/chatbot-ai.webm" type="video/webm; codecs=vp9,vorbis"/><source src="https://www.premieroctet.com/blog/ai-assistant-photobooth/chatbot-ai.mp4" type="video/mp4"/></video></div>
<h3>3. Une expérience utilisateur améliorée</h3>
<p>L&#x27;objectif pour l&#x27;expérience utilisateur est clair :</p>
<ul>
<li><strong>Réponses instantanées</strong> : Plus d&#x27;attente pour obtenir des informations de base</li>
<li><strong>Navigation guidée</strong> : L&#x27;assistant dirige intelligemment les utilisateurs vers les bonnes sections de l&#x27;application</li>
<li><strong>Support multilingue</strong> : Le modèle que nous utilisons permet de répondre dans la plupart des langues utilisées par nos utilisateurs</li>
</ul>
<h3>4. Optimisation des coûts opérationnels</h3>
<p>L&#x27;automatisation permet une <strong>réduction significative des coûts de support</strong> en traitant automatiquement les questions récurrentes. Notre équipe peut ainsi se concentrer sur sa valeur ajoutée : résoudre les problèmes complexes et améliorer le produit.</p>
<h3>5. Collecte et traitement intelligent du feedback</h3>
<p>L&#x27;assistant ne se contente pas de répondre aux questions. Il intègre un système intelligent de gestion du feedback qui permet, en cas de besoins spécifiques, de transmettre les demandes au support humain.</p>
<ul>
<li><strong>Escalade automatique</strong> : Création automatique de tickets pour les problèmes nécessitant une intervention humaine</li>
<li><strong>Traduction automatique</strong> : Les feedbacks internationaux sont traduits en français pour notre équipe</li>
</ul>
<div class="u-txt-center" style="margin-top:20px"><video class="b-lazy b-loaded" autoplay="" loop="" muted="" title="Tool usage"><source src="https://www.premieroctet.com/blog/ai-assistant-photobooth/tool-usage.webm" type="video/webm; codecs=vp9,vorbis"/><source src="https://www.premieroctet.com/blog/ai-assistant-photobooth/tool-usage.mp4" type="video/mp4"/></video></div>
<h3>6. Amélioration continue basée sur les données</h3>
<p>Chaque conversation est sauvegardée et analysée, nous permettant de :</p>
<ul>
<li><strong>Identifier les besoins récurrents</strong> de nos utilisateurs</li>
<li><strong>Optimiser notre produit</strong> en fonction des questions les plus fréquentes</li>
<li><strong>Améliorer continuellement</strong> les réponses de l&#x27;assistant en fonction des retours utilisateurs</li>
</ul>
<p>Cette boucle de feedback continu nous aide à faire évoluer Photobooth AI dans la bonne direction. C&#x27;est un atout précieux pour notre roadmap produit.</p>
<h2>Architecture technique : sous le capot</h2>
<p>Rentrons maintenant dans les détails techniques. Notre assistant repose sur une architecture moderne utilisant les dernières innovations en IA conversationnelle :</p>
<h3>Stack technologique</h3>
<ul>
<li><strong>Modèle IA</strong> : <code>gpt-4.1-mini</code> d&#x27;OpenAI, optimisé pour les coûts tout en maintenant une qualité élevée</li>
<li><strong>AI SDK</strong> : Vercel AI SDK pour une intégration des modèles IA et des outils</li>
<li><strong>AI Elements</strong> : pour la création de l&#x27;interface conversationnelle</li>
<li><strong>Base de données</strong> : PostgreSQL avec Prisma pour la persistance et l&#x27;analytics</li>
</ul>
<h3>Système de connaissances dynamique</h3>
<p>L&#x27;assistant construit son contexte en temps réel en combinant :</p>
<ul>
<li>Un prompt système de 25 lignes spécialisé pour Photobooth AI</li>
<li>Notre FAQ complète (203 lignes) mise à jour automatiquement</li>
<li>Le sitemap de navigation adapté selon le statut utilisateur (connecté/anonyme)</li>
<li>Un système de cache intelligent avec revalidation horaire</li>
</ul>
<h2>En conclusion</h2>
<p>Passer de Crisp à un assistant IA personnalisé nous a permis de repenser complètement notre façon de faire du support. Plutôt que d&#x27;attendre les questions des utilisateurs, nous pouvons maintenant les anticiper et les guider plus efficacement.</p>
<p>Pour nous, c&#x27;est aussi d&#x27;exploiter notre expertise en agents conversationnels. Un exemple concret du potentiel d&#x27;impact positif qu&#x27;une IA bien intégrée peut avoir sur l&#x27;expérience utilisateur et les opérations d&#x27;une entreprise.</p>
<p>Si cette approche vous intéresse ou si vous avez des questions sur la mise en place d&#x27;un assistant IA pour votre propre produit, n&#x27;hésitez pas à <a href="https://www.premieroctet.com/contact">nous contacter</a>, nous serons ravis de partager notre expérience et de discuter de votre projet ! 🚀</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/ai-assistant-photobooth/illu.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Guide pratique du Model Context Protocol : tout comprendre sur les tools et serveurs MCP]]></title>
            <link>https://www.premieroctet.com/blog/guide-pratique-du-model-context-protocol-tout-comprendre-sur-les-tools-et-serveurs-mcp</link>
            <guid>https://www.premieroctet.com/blog/guide-pratique-du-model-context-protocol-tout-comprendre-sur-les-tools-et-serveurs-mcp</guid>
            <pubDate>Wed, 29 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Comprenez les tools, la différence avec les serveurs MCP, les transports stdio et HTTP, et comment choisir selon votre contexte (SaaS, desktop, agents IA).]]></description>
            <content:encoded><![CDATA[<p>Depuis presque un an maintenant, <a href="https://www.anthropic.com/">Anthropic</a> a initié un protocole de communication permettant de fournir des informations contextuelles aux modèles et a donné naissance au <a href="https://modelcontextprotocol.io/docs/getting-started/intro"><strong>Model Context Protocol (MCP)</strong></a>. Il est devenu, au fil du temps, un standard dans l&#x27;écosystème IA pour les applications qui souhaitent utiliser des modèles IA de manière plus efficace et les personnes qui souhaitent augmenter les capacités de leurs modèles IA.</p>
<p>Cependant, la diffusion progressive de ce protocole, essentielle à son adoption, s&#x27;accompagne aussi d&#x27;un certain nombre de questions et de confusions. C&#x27;est ce que nous allons tenter de clarifier dans cet article en détaillant les différents cas d&#x27;utilisation possibles et les meilleures pratiques à appliquer en fonction de vos besoins.</p>
<p><strong>À quoi sert le protocole MCP ?</strong> <strong>Quels sont les cas d&#x27;utilisation possibles ?</strong> <strong>Comment utiliser ce protocole dans vos applications ?</strong> <strong>Quelles sont les limites de ce protocole ?</strong> <strong>À quels besoins répond ce protocole ?</strong></p>
<h2>Introduction</h2>
<p>Force est de constater que l&#x27;utilisation des <strong>Large Language Models (LLMs)</strong> augmente de manière significative dans tous les domaines, allant de la simple utilisation par le grand public de chatbots IA comme ChatGPT (<a href="https://gs.statcounter.com/ai-chatbot-market-share">en position de quasi-monopole aujourd&#x27;hui</a>) à l&#x27;utilisation sur-mesure dans des applications complexes, en passant par des assistants de développement comme Cursor, Zed, etc.</p>
<p>Mais ces outils puissants souffraient de deux problèmes majeurs:</p>
<ul>
<li><strong>le manque de contexte</strong> (ex: données de la base de données, documents, etc.)</li>
<li><strong>l&#x27;impossibilité d&#x27;effectuer des actions</strong> (ex: envoyer un email, modifier un document, créer un événement, etc.)</li>
</ul>
<p>Pour pallier au premier problème de contexte, des solutions ont été mises en place comme le <strong>Retrieval-Augmented Generation (RAG)</strong>, que l&#x27;on peut voir comme une sorte de bibliothèque permettant aux LLMs d&#x27;accéder à des informations contextuelles, et le <strong>fine-tuning</strong> de modèles pour permettre aux LLMs d&#x27;être spécialisés dans un domaine précis.
Ces sujets ne seront pas abordés dans cet article, mais si le fonctionnement de ces solutions vous intéresse, vous pouvez consulter <a href="https://www.premieroctet.com/blog/comment-fonctionne-un-rag">Comment fonctionne un RAG ?</a>.</p>
<div type="info"><p>À noter qu&#x27;il n&#x27;existe pas de standard pour les RAG. Chaque implémentation doit être adaptée à la
plateforme utilisée.</p></div>
<p>Enfin, pour pallier l&#x27;impossibilité d&#x27;effectuer des actions, on a vu apparaître la notion de <strong>tools</strong> (ou <strong>outils</strong> en français).</p>
<h2>Qu&#x27;est-ce qu&#x27;un tool ?</h2>
<p>Un <code>tool</code> est une fonctionnalité qui peut être appelée par un LLM pour effectuer une tâche spécifique. Par exemple, un tool pourrait être utilisé pour envoyer un email, modifier un document, créer un événement, etc. L&#x27;appel d&#x27;un <code>tool</code> permet donc d&#x27;exécuter une fonction spécifique sur un serveur et de retourner un résultat qui sera soit affiché dans la conversation, soit utilisé pour donner du contexte à la requête.</p>
<p><img src="https://www.premieroctet.com/blog/guide-pratique-du-model-context-protocol-tout-comprendre-sur-les-tools-et-serveurs-mcp/tools.svg" alt="Tool Schema"/></p>
<ol>
<li>L&#x27;utilisateur fait une demande au LLM</li>
<li>Le LLM analyse la demande et détermine s&#x27;il existe un outil qui pourrait répondre à la demande</li>
<li>Si non : Le LLM génère directement la réponse</li>
<li>Si oui : Le LLM invoque l&#x27;outil</li>
<li>Le serveur qui implémente le tool exécute l&#x27;action demandée par le tool</li>
<li>Le serveur retourne le résultat à l&#x27;utilisateur ou au LLM qui peut alors formuler une réponse complète à l&#x27;utilisateur</li>
</ol>
<div type="info"><p>Ce schéma est une simplification pour illustrer le fonctionnement d&#x27;un tool. En réalité, un tool
peut être composé de plusieurs étapes et de plusieurs types de retours voire même réinterprété par
le LLM.</p></div>
<h3>Où s&#x27;exécute un tool ?</h3>
<p>Comme on peut le voir sur le schéma ci-dessus, un <code>tool</code> est appelé par un LLM et exécuté sur le serveur qui l&#x27;a implémenté. C&#x27;est ce serveur qui va exécuter la fonctionnalité spécifique et retourner le résultat.</p>
<p>C&#x27;est pour cette raison que les <code>tools</code> &quot;classiques&quot; ne peuvent pas être exécutés directement par un agent distant, par &quot;distant&quot; on entend un agent qui n&#x27;est pas hébergé sur le même serveur que le serveur qui implémente le tool ou dans un processus différent. Ils sont donc très utiles pour les applications qui implémentent leur propre agent IA.</p>
<h2>Qu&#x27;est-ce que le protocole MCP ?</h2>
<p>On l&#x27;a vu précédemment, l&#x27;infrastructure des <code>tools</code> est très simple, mais elle souffre du fait que leur exécution est limitée à leur propre environnement et ils ne peuvent être appelés et exécutés que dans celui-ci.</p>
<p>Pour pouvoir exposer des tools (entre autres) à un agent IA, on a vu apparaître le protocole <a href="https://modelcontextprotocol.io/">Model Context Protocol (MCP)</a>. Il s&#x27;agit d&#x27;un protocole de communication qui permet de faire le pont entre des agents IA et des tools présents sur un serveur MCP.</p>
<p>Ce protocole repose sur deux composantes principales:</p>
<ul>
<li>Le <strong>serveur MCP</strong> qui implémente et expose les <code>tools</code> disponibles</li>
<li>Le <strong>client MCP</strong> qui appelle les <code>tools</code> disponibles et reçoit les résultats</li>
</ul>
<div type="info"><p>Les serveurs MCP peuvent aussi exposer des <code>resources</code> et des <code>prompts</code> en plus des <code>tools</code>.
Cependant, beaucoup d&#x27;hôtes MCP distants ne supportent pas ces fonctionnalités.</p></div>
<p>La communication entre ces deux entités est standardisée via un format <a href="https://www.jsonrpc.org/">JSON-RPC</a> qui permet de faire transiter des ordres d&#x27;instructions (actions/procédures) entre un client et un serveur.</p>
<p>Le fait d&#x27;avoir deux entités distinctes nous donne la flexibilité d&#x27;utiliser le protocole MCP dans différents contextes et d&#x27;avoir nos <code>tools</code> accessibles depuis des environnements différents.</p>
<h2>Les hôtes MCP</h2>
<p>Dans la suite de cet article, nous allons parler de <strong>hôte MCP</strong> pour désigner l&#x27;entité qui implémente à la fois la communication avec un LLM et un client MCP. On pourra ainsi faire la distinction entre un <strong>hôte MCP distant</strong> et un <strong>hôte MCP local</strong>. Il est important de les différencier car ils n&#x27;auront pas les mêmes contraintes et possibilités.</p>
<h3>Hôte MCP distant</h3>
<p>Un hôte MCP distant ou <code>remote</code> est souvent un service web qui propose un chatbot IA comme ChatGPT ou Claude AI (version web). Pour savoir si un agent IA supporte le protocole MCP, il faut vérifier ses capacités dans l&#x27;interface web de l&#x27;agent IA et vérifier s&#x27;il est fait mention de <code>Connected apps</code> ou <code>Connecteurs</code>.</p>
<div type="info"><p>Même si le protocole MCP devient un standard dans l&#x27;écosystème IA, il n&#x27;est pas encore supporté
par tous les services web qui proposent un chatbot IA.</p></div>
<h3>Hôte MCP local</h3>
<p>Un hôte MCP local est une entité qui a pour environnement votre propre machine. Il tourne sur votre propre machine et peut donc effectuer des actions dessus. Dans cette catégorie, on peut citer les assistants de développement comme Cursor, Zed, etc., mais aussi des versions desktop d&#x27;agents conversationnels comme <a href="https://claude.com/download">Claude Desktop</a> par exemple.</p>
<h2>Les différents types de transports</h2>
<p>Il existe différents types de transports pour la communication entre le client et le serveur MCP:</p>
<ul>
<li><a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio">Stdio</a></li>
<li><a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http">Streamable HTTP</a></li>
</ul>
<p>Le choix du transport va dépendre des besoins et des contraintes de l&#x27;application.</p>
<h3>Stdio</h3>
<p>Le transport <code>stdio</code> est un transport qui utilise le standard input/output (stdin/stdout) pour la communication entre le client et le serveur. Avec ce mode de transport, tout se passe en local, c&#x27;est-à-dire que le client et le serveur sont hébergés localement et le serveur peut effectuer des actions sur la machine locale comme lire des fichiers, écrire des fichiers, etc.</p>
<p>Il va de pair avec un hôte MCP local. Les hôtes MCP distants n&#x27;utilisent pas ce type de transport.</p>
<p>Ce transport est idéal pour les assistants de développement et les applications desktop qui ont besoin d&#x27;accéder aux ressources locales de l&#x27;utilisateur.</p>
<p><img src="https://www.premieroctet.com/blog/guide-pratique-du-model-context-protocol-tout-comprendre-sur-les-tools-et-serveurs-mcp/stdio.svg" alt="Stdio Transport"/></p>
<p>Le serveur MCP est lancé en local en tant que sous-processus de l&#x27;hôte MCP local.</p>
<div type="warning"><p>Le transport <code>stdio</code> expose des API natives de l&#x27;hôte MCP local. Il est donc important de
s&#x27;assurer que l&#x27;hôte MCP local provient d&#x27;une source fiable et de confiance.</p></div>
<p>Si l&#x27;implémentation d&#x27;un serveur MCP en transport <code>stdio</code> vous intéresse, vous pouvez consulter <a href="https://www.premieroctet.com/blog/integration-mcp-dans-une-app-react">Intégration d&#x27;un MCP dans une application React</a>.</p>
<h3>Streamable HTTP</h3>
<p>Le transport <code>streamable HTTP</code> est un transport qui utilise le protocole HTTP pour la communication entre le client MCP et le serveur MCP. Il nous permet de découpler l&#x27;hôte et le serveur et de les héberger sur des serveurs différents, permettant ainsi d&#x27;utiliser des serveurs MCP avec des hôtes MCP distants et locaux.</p>
<p>Le serveur MCP doit être déployé sur un serveur web et accessible via une URL. L&#x27;hôte pourra ainsi appeler les tools du serveur MCP via une requête HTTP. Le transport <code>streamable HTTP</code> est idéal pour les hôtes MCP distants.</p>
<p><img src="https://www.premieroctet.com/blog/guide-pratique-du-model-context-protocol-tout-comprendre-sur-les-tools-et-serveurs-mcp/streamable-http.svg" alt="Streamable HTTP Transport"/></p>
<p>Dans le cadre d&#x27;un SaaS par exemple, on pourra héberger le serveur MCP sur un serveur web ou créer un endpoint <code>/mcp</code> dans notre API, et un utilisateur aura la possibilité d&#x27;ajouter un connecteur à son hôte MCP distant, par exemple Claude AI ou ChatGPT, pour utiliser nos <code>tools</code>.</p>
<h4>Sécurité</h4>
<p>Dans certains cas, on peut avoir besoin d&#x27;authentifier ou d&#x27;autoriser l&#x27;utilisation des outils exposés. Dans ce cas, on peut utiliser un token d&#x27;authentification ou une clé API pour sécuriser l&#x27;accès au serveur MCP. L&#x27;utilisation de ce transport nous permet donc de sécuriser l&#x27;accès aux outils exposés par le serveur MCP avec une authentification basique ou plus avancée avec <a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization">OAuth 2</a>.</p>
<h2>Comment utiliser le protocole MCP dans vos applications ?</h2>
<p>Tout d&#x27;abord, avant de commencer à utiliser le protocole MCP dans vos applications, il est important de comprendre comment il fonctionne et quelles sont les différentes possibilités offertes par le protocole. Ensuite, il faut définir le besoin de votre application et choisir le transport qui correspond le mieux à vos besoins.</p>
<p><strong>Si votre application est un SaaS</strong>, alors vous souhaitez probablement exposer vos <code>tools</code> au grand public. Dans ce cas, vous aurez besoin d&#x27;utiliser le transport <code>streamable HTTP</code> afin de permettre aux utilisateurs de connecter leur hôte MCP distant, par exemple Claude AI ou ChatGPT, à votre serveur MCP.</p>
<blockquote>
<p>Les utilisateurs d&#x27;hôtes locaux pourront aussi utiliser votre solution avec ce mode de transport.</p>
</blockquote>
<p><strong>Si votre solution est un script ou une application autonome</strong>, alors vous aurez besoin d&#x27;utiliser le transport <code>stdio</code> afin de permettre aux utilisateurs de connecter leur hôte MCP local à votre serveur MCP.</p>
<p><strong>Si votre solution a un usage exclusivement interne</strong>, alors vous n&#x27;avez probablement pas besoin d&#x27;utiliser le protocole MCP. Exception faite si vous souhaitez utiliser des <code>tools</code> sur plusieurs agents IA internes. Dans ce cas, il faudra utiliser le transport <code>stdio</code> pour permettre aux agents IA de communiquer avec le serveur MCP (si celui-ci est sur le même serveur).</p>
<h2>Adhésion au protocole</h2>
<p>L&#x27;efficacité d&#x27;un MCP est directement liée à l&#x27;adhésion au protocole par les hôtes MCP distants. En effet, si un hôte MCP distant ne supporte pas le protocole MCP, alors le client MCP ne pourra pas utiliser les <code>tools</code> exposés par le serveur MCP. À ce jour, de plus en plus d&#x27;hôtes MCP distants supportent le protocole MCP, mais la plupart du temps l&#x27;ajout de connecteurs fait partie de fonctionnalités payantes, c&#x27;est le cas de ChatGPT et Claude AI par exemple. Ce qui freine donc la démocratisation de l&#x27;utilisation du protocole MCP par le grand public.</p>
<h2>Registre</h2>
<p>Il existe un registre officiel des serveurs MCP disponibles. Il est disponible sur le site de <a href="https://github.com/modelcontextprotocol/servers">Model Context Protocol</a> et permet de trouver les serveurs MCP disponibles et de les utiliser dans vos agents IA.</p>
<p>Si la mise en place de serveurs MCP par les entreprises continue de se démocratiser, il faudra s&#x27;attendre à ce que d&#x27;autres registres voient le jour pour permettre aux utilisateurs de trouver les serveurs MCP à la manière des moteurs de recherche traditionnels. Si c&#x27;est le cas, le référencement auprès de ces registres pourra devenir un enjeu majeur pour les entreprises.</p>
<h2>Conclusion</h2>
<p>Vous l&#x27;aurez compris, le protocole MCP est un standard qui va changer la donne dans l&#x27;écosystème IA. En permettant aux LLMs d&#x27;accéder à des données contextuelles et d&#x27;effectuer des actions concrètes, il transforme ces modèles de simples générateurs de texte en véritables assistants capables d&#x27;agir dans le monde réel.</p>
<p>Le choix du transport dépendra de votre contexte : <code>stdio</code> pour les assistants de développement et applications desktop, <code>streamable HTTP</code> pour les solutions SaaS et l&#x27;intégration avec des hôtes distants comme Claude AI ou ChatGPT.</p>
<p>Même si l&#x27;adoption par les hôtes MCP distants est encore limitée et souvent payante, la tendance est clairement à la démocratisation.</p>
<p>Chez Premier Octet, nous suivons de près ces évolutions. Si vous souhaitez explorer cette technologie pour vos besoins ou avez des questions sur l&#x27;implémentation, n&#x27;hésitez pas à <a href="https://www.premieroctet.com/contact">nous contacter</a>.</p>
<h2>Ressources</h2>
<ul>
<li><a href="https://modelcontextprotocol.io/">Model Context Protocol</a></li>
<li><a href="https://github.com/modelcontextprotocol/servers">Serveurs MCP officiels</a></li>
<li><a href="https://www.premieroctet.com/blog/integration-mcp-dans-une-app-react">Intégration d&#x27;un MCP dans une application React</a></li>
<li><a href="https://jolicode.com/blog/mcp-the-open-protocol-that-turns-llm-chatbots-into-intelligent-agents">MCP: The Open Protocol That Turns LLM Chatbots into Intelligent Agents</a></li>
</ul>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/guide-pratique-du-model-context-protocol-tout-comprendre-sur-les-tools-et-serveurs-mcp/illu.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Better Auth : structure et permissions avec le plugin Organization]]></title>
            <link>https://www.premieroctet.com/blog/better-auth-structure-et-permissions-avec-le-plugin-organization</link>
            <guid>https://www.premieroctet.com/blog/better-auth-structure-et-permissions-avec-le-plugin-organization</guid>
            <pubDate>Wed, 15 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Découvrez en profondeur le plugin Organization de Better Auth : gestion du state client/serveur, personnalisation du modèle, permissions avancées et la configuration Teams.]]></description>
            <content:encoded><![CDATA[<p>Après avoir exploré <a href="https://www.premieroctet.com/blog/better-auth-futur-authjs">les bases de <strong>Better Auth</strong></a>, nous allons aujourd&#x27;hui nous intéresser à l’un des plugins que nous avions mentionné : <a href="https://www.better-auth.com/docs/plugins/organization"><strong>Organization</strong></a>.</p>
<p>Gérer une hiérarchie d’utilisateurs et de permissions dans une application peut vite devenir un casse-tête. Le plugin Organization de Better Auth propose une approche élégante et modulaire : il permet de gérer des structures multi-utilisateurs (équipes, entreprises, projets...), d’y associer des rôles et d’implémenter des permissions.</p>
<p>Voici la liste des points que nous allons aborder :</p>
<ul>
<li>la gestion du <strong>state entre client et serveur</strong>,</li>
<li>la <strong>personnalisation du modèle Organization</strong>,</li>
<li>la <strong>mise en place d’un système de permissions avancé</strong>,</li>
<li>et enfin l’activation du mode <strong>Teams</strong>, pour les cas les plus complexes.</li>
</ul>
<h2>Séparation du state entre serveur et client</h2>
<p>Un point de base essentiel à comprendre est que <code>auth</code> (serveur) et <code>authClient</code> (client) fonctionnent comme deux systèmes séparés, ils ne partagent <strong>pas d’état synchronisé automatiquement</strong>.</p>
<h3>👉 En pratique :</h3>
<ul>
<li>Les actions exécutées via <code>auth.api</code> ne mettent pas à jour automatiquement les hooks client (ex. <code>useSession</code>, <code>useActiveOrganization</code>).</li>
<li>Il faut rafraîchir explicitement l’état client après toute action ayant un impact sur la session, l’organisation ou les rôles. Par exemple, le hook <code>useActiveOrganization()</code> propose un <code>refetch()</code> pour rafraîchir les données de l’organisation active côté client.</li>
</ul>
<p>Ce comportement découle de la séparation entre client et serveur : Better Auth ne synchronise pas automatiquement ces deux états. Cela donne au développeur un contrôle total sur le rafraîchissement côté client. Similaire à d’autres librairies d’authentification modernes, <strong>la synchronisation automatique du state n’est généralement pas implémentée pour des raisons de performance et de sécurité.</strong></p>
<h2>Personnaliser le modèle Organization</h2>
<p>Les tables nécessaires au plugin Organization sont personnalisables : il est possible de les <a href="https://www.better-auth.com/docs/plugins/organization#customizing-the-schema">renommer ainsi que leurs champs</a> ou <a href="https://www.better-auth.com/docs/plugins/organization#additional-fields-1">d&#x27;ajouter des champs supplémentaires</a>. Le CLI de Better Auth se chargera ensuite de générer le schéma correspondant dans votre base de données via la commande <code>npx @better-auth/cli generate</code> (ici, nous utilisons <a href="https://www.premieroctet.com/blog/better-auth-futur-authjs#installation-et-gestion-de-la-base-de-donnees">Prisma avec son adapter</a>).</p>
<p>Dans cet exemple, nous allons renommer <code>Organization</code> en <code>Project</code> et son champ <code>name</code> en <code>title</code> (mais dans le reste de l&#x27;article, nous continuerons à parler de <code>organization</code>).</p>
<div><div><div value="tab-1">auth.ts</div><div value="tab-2">schema.prisma.ts</div></div><div value="tab-1"><pre><code class="language-ts">plugins: [
  organization({
    schema: {
      organization: {
        modelName: &#x27;project&#x27;, // `Organization` sera renommé `Project`
        fields: {
          name: &#x27;title&#x27;, // `name` sera renommé `title`
        },
      },
      member: {
        additionalFields: {
          name: {
            // On ajoute un champ `name` à la table `Member`
            type: &#x27;string&#x27;,
            input: true,
            required: false,
          },
        },
      },
    },
  }),
]
</code></pre></div><div value="tab-2"><pre><code class="language-ts">// Voici les tables générées

model Project {
  id          String       @id
  title       String
  slug        String?
  logo        String?
  createdAt   DateTime
  metadata    String?
  members     Member[]
  invitations Invitation[]

  @@unique([slug])
  @@map(&quot;project&quot;)
}

model Member {
  id             String   @id
  organizationId String
  project        Project  @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  userId         String
  user           User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  role           String
  createdAt      DateTime
  name           String?

  @@map(&quot;member&quot;)
}

model Invitation {
  id             String   @id
  organizationId String
  project        Project  @relation(fields: [organizationId], references: [id], onDelete: Cascade)
  email          String
  role           String?
  status         String
  expiresAt      DateTime
  inviterId      String
  user           User     @relation(fields: [inviterId], references: [id], onDelete: Cascade)

  @@map(&quot;invitation&quot;)
}

</code></pre></div></div>
<p>⚠️ Le renommage n’affecte <strong>pas les noms des méthodes SDK</strong> (<code>authClient.organization.*</code>, <code>auth.api.*</code>).</p>
<pre><code class="language-ts">// ✅ Correct
const { data, error } = await authClient.organization.delete({
  organizationId: activeOrganization!.id,
})

// ❌ Incorrect
const { data, error } = await authClient.project.delete({
  projectId: activeProject!.id,
})
</code></pre>
<h2>Mettre en place un système de permissions</h2>
<p><img src="https://www.premieroctet.com/blog/better-auth-structure-et-permissions-avec-le-plugin-organization/manage-roles.jpg" alt="Gestion des rôles : capture de l’UI des permissions"/></p>
<p>C’est sans doute l&#x27;une des parties les plus intéressantes du plugin Organization : Better Auth fournit un système d’<a href="https://www.better-auth.com/docs/plugins/organization#access-control"><strong>Access Control</strong></a> flexible avec son <code>createAccessControl</code>. Il s’appuie sur une approche déclarative de la sécurité, où chaque <strong>rôle</strong> se voit attribuer un ensemble d’<strong>actions autorisées</strong> sur des <strong>entités</strong> précises.</p>
<h3>Résumé des termes :</h3>
<ul>
<li><strong>Entité</strong> → représente un objet sur lequel on agit (<code>organization</code>, <code>member</code>, <code>invitation</code>).</li>
<li><strong>Action</strong> → représente ce qu’on peut faire sur une entité (<code>update</code>, <code>delete</code>, etc.).</li>
<li><strong>Permission</strong> → est la combinaison entité + action (un <code>member</code> avec l&#x27;action <code>create</code> sur l&#x27;entité <code>invitation</code> peut créer une invitation).</li>
<li><strong>Rôle</strong> → est un ensemble de permissions accordées à un utilisateur.</li>
</ul>
<h3>Exemple de fichier de configuration :</h3>
<p>Voici un exemple minimal pour déclarer des actions par entité et composer des rôles.</p>
<pre><code class="language-ts">

const statement = {
  organization: [&#x27;update&#x27;, &#x27;delete&#x27;],
  member: [&#x27;create&#x27;, &#x27;update&#x27;, &#x27;delete&#x27;, &#x27;update-name&#x27;],
  invitation: [&#x27;create&#x27;, &#x27;cancel&#x27;],
} as const

const ac = createAccessControl(statement)

const member = ac.newRole({
  member: [&#x27;update-name&#x27;],
})

const admin = ac.newRole({
  member: [&#x27;update&#x27;, &#x27;delete&#x27;, &#x27;update-name&#x27;],
  invitation: [&#x27;create&#x27;, &#x27;cancel&#x27;],
})

const owner = ac.newRole({
  organization: [&#x27;update&#x27;, &#x27;delete&#x27;],
  member: [&#x27;update&#x27;, &#x27;delete&#x27;, &#x27;update-name&#x27;],
  invitation: [&#x27;create&#x27;, &#x27;cancel&#x27;],
})

export { statement, ac, owner, admin, member }
</code></pre>
<h3>Points importants</h3>
<ul>
<li>Un <strong>utilisateur</strong> peut avoir <a href="https://www.better-auth.com/docs/plugins/organization#roles"><strong>plusieurs rôles à la fois</strong></a>.</li>
<li>Vous pouvez <a href="https://www.better-auth.com/docs/plugins/organization#custom-permissions"><strong>définir vos propres permissions</strong></a> (ex. <code>update-name</code>).</li>
<li>Si vous définissez un <code>ac</code> et des <code>roles</code> personnalisés, <strong>les permissions par défaut de Better Auth <a href="https://www.better-auth.com/docs/plugins/organization#create-roles">sont écrasées</a></strong>. Better Auth s’attend à retrouver certaines actions pour ses checks internes ; il faut donc réintroduire les <a href="https://www.better-auth.com/docs/plugins/organization#permissions"><strong>actions de base</strong></a> si vous souhaitez utiliser les méthodes natives du plugin :<!-- -->
<ul>
<li><code>organization: [&quot;update&quot;, &quot;delete&quot;]</code></li>
<li><code>member: [&quot;create&quot;, &quot;update&quot;, &quot;delete&quot;]</code></li>
<li><code>invitation: [&quot;create&quot;, &quot;cancel&quot;]</code></li>
</ul>
</li>
</ul>
<p>Si une de ces actions est absente, <strong>les rôles</strong> qui ne la possèdent pas ne pourront pas exécuter les méthodes correspondantes (<code>auth.organization.update</code>, <code>auth.organization.inviteMember</code>, etc.) : elles seront <strong>considérées comme non autorisées</strong>.</p>
<h3>Permission personnalisée</h3>
<p>Vous vous demandez peut-être pourquoi nous avons à la fois <code>update</code> et <code>update-name</code> dans nos permissions. Intuitivement, on pourrait penser que <code>update</code> couvre toutes les actions de mise à jour mais dans le plugin Organization, <code>update</code> fait uniquement référence à la méthode <a href="https://www.better-auth.com/docs/plugins/organization#update-member-role"><strong>updateMemberRole</strong></a>, c&#x27;est-à-dire au changement de rôle d&#x27;un membre.</p>
<p>Cela s&#x27;explique par le fait que la table <code>Member</code> ne contient <a href="https://www.better-auth.com/docs/plugins/organization#member">aucun champ que l&#x27;on souhaiterait modifier par défaut</a> autre que <code>role</code>. Notre permission personnalisée <code>update-name</code> n&#x27;aura donc de sens que si nous ajoutons un champ <code>name</code> à la table <code>Member</code> et que nous implémentons la logique métier correspondante (via nos propres routes ou actions serveur).</p>
<h2>Vérifier les permissions côté client et côté serveur</h2>
<p>Une bonne pratique consiste à effectuer <strong>un double check</strong> :</p>
<ul>
<li><strong>Côté client</strong> → pour la logique d’interface (afficher/masquer un bouton)</li>
<li><strong>Côté serveur</strong> → pour la sécurité et la source de vérité</li>
</ul>
<h3>Côté client</h3>
<pre><code class="language-ts">authClient.organization.checkRolePermission({
  permissions: { organization: [&#x27;update&#x27;] },
  role: &#x27;owner&#x27;,
})
</code></pre>
<p>Cette méthode est <strong>synchrone</strong> et ne dépend pas du serveur.
Elle est idéale pour ajuster l’UI selon les permissions du rôle mais ne prend pas en compte les éventuelles mises à jour récentes (changement de rôle, révocation, etc.).</p>
<h3>Côté serveur</h3>
<p>Le serveur reste <strong>la source de vérité</strong>.<br/>
<!-- -->Les rôles et permissions étant stockés en base, il faut toujours valider côté serveur :</p>
<pre><code class="language-ts">await auth.api.hasPermission({
  headers: await headers(),
  body: {
    permissions: { organization: [&#x27;update&#x27;] },
  },
})
</code></pre>
<p>Voici un petit utilitaire ainsi que notre implémentation côtés client/serveur :</p>
<div><div><div value="tab-1">types/permissions.ts</div><div value="tab-2">utils/permissions.ts</div><div value="tab-3">actions/permission.ts</div></div><div value="tab-1"><pre><code class="language-ts">

export type Entities = keyof typeof statement
export type PermissionFor&lt;E extends Entities&gt; = (typeof statement)[E][number]
</code></pre></div><div value="tab-2"><pre><code class="language-ts">// Côté Client
export const hasClientPermission = &lt;E extends Entities, P extends PermissionFor&lt;E&gt;&gt;(
  role: Role,
  entity: E,
  permission: P
) =&gt; {
  return authClient.organization.checkRolePermission({
    permissions: { [entity]: [permission] },
    role: role!,
  })
}
</code></pre></div><div value="tab-3"><pre><code class="language-ts">// Côté Serveur
&#x27;use server&#x27;






export const hasServerPermission = async &lt;E extends Entities, P extends PermissionFor&lt;E&gt;&gt;(
  entity: E,
  permission: P
) =&gt; {
  try {
    const { error, success } = await auth.api.hasPermission({
      headers: await headers(),
      body: {
        permissions: { [entity]: [permission] },
      },
    })

    if (error) {
      throw new Error(error)
    }

    if (success === false) {
      throw new Error(ERROR_MESSAGES.YOU_DONT_HAVE_PERMISSION_TO_DO_THIS_ACTION)
    }

    return success
  } catch (error) {
    if (error instanceof Error) {
      throw new Error(error.message)
    }

    console.error(&#x27;[🔴 hasPermission]: &#x27;, error)
    throw new Error(ERROR_MESSAGES.AN_ERROR_OCCURRED)
  }
}
</code></pre></div></div>
<h2>Gérer les invitations</h2>
<p>Better Auth fournit un système intégré de gestion des <a href="https://www.better-auth.com/docs/plugins/organization#invitations">invitations</a> qui permet d’ajouter de nouveaux membres à une organisation.</p>
<p><img src="https://www.premieroctet.com/blog/better-auth-structure-et-permissions-avec-le-plugin-organization/invitations.jpg" alt="Invitations : capture de l’interface"/></p>
<h3>Les méthodes SDK</h3>
<p>Côté client (<code>authClient</code>) comme côté serveur (<code>auth</code>), le fonctionnement est identique :</p>
<div><div><div value="tab-1">clients/auth.ts</div><div value="tab-2">auth.ts</div></div><div value="tab-1"><pre><code class="language-ts">// Côté client
// ⚠️ Pensez à rafraîchir l’état côté client si nécessaire

const { data, error } = await authClient.organization.listInvitations({
  query: {
  organizationId: &quot;organization-id&quot;,
  },
});

// Côté serveur, la méthode équivalente à inviteMember s’appelle `createInvitation`
const { data, error } = await authClient.organization.inviteMember({
  email: &quot;example@gmail.com&quot;,
  role: &quot;member&quot;,
  organizationId: &quot;org-id&quot;,
  resend: true,
  teamId: &quot;team-id&quot;,
});

const { data, error } = await authClient.organization.acceptInvitation({
  invitationId: &quot;invitation-id&quot;,
});

await authClient.organization.rejectInvitation({
  invitationId: &quot;invitation-id&quot;,
});

await authClient.organization.cancelInvitation({
  invitationId: &quot;invitation-id&quot;,
});

</code></pre></div><div value="tab-2"><pre><code class="language-ts">// Côté serveur
// ⚠️ N&#x27;oubliez pas d&#x27;inclure les headers
// Sinon la méthode renverra un statut 401 &quot;UNAUTHORIZED&quot;

const data = await auth.api.listInvitations({
  headers: await headers(),
  query: {
      organizationId: &quot;organization-id&quot;,
  },
});

await auth.api.createInvitation({
  headers: await headers(),
  body: {
      email: &quot;example@gmail.com&quot;,
      role: &quot;member&quot;,
      organizationId: &quot;org-id&quot;,
      resend: true,
      teamId: &quot;team-id&quot;,
  },
});

await auth.api.acceptInvitation({
  headers: await headers(),
  body: {
  invitationId: &quot;invitation-id&quot;,
  },
});

await auth.api.rejectInvitation({
  headers: await headers(),
  body: {
  invitationId: &quot;invitation-id&quot;,
  },
});

await auth.api.cancelInvitation({
  headers: await headers(),
  body: {
  invitationId: &quot;invitation-id&quot;,
  },
});
</code></pre></div></div>
<h3>Callbacks</h3>
<p>Après la création d&#x27;une invitation avec <code>inviteMember</code> (<strong>authClient</strong>) ou <code>createInvitation</code> (<strong>auth</strong>), Better Auth exécute automatiquement le callback <a href="https://www.better-auth.com/docs/plugins/organization#setup-invitation-email"><strong>sendInvitationEmail</strong></a> qui permet d’envoyer un e-mail personnalisé contenant l’<code>invitationId</code>.</p>
<p>Il existe également un callback <a href="https://www.better-auth.com/docs/plugins/organization#invitation-accepted-callback"><strong>onInvitationAccepted</strong></a> déclenché lorsque l&#x27;utilisateur accepte une invitation :</p>
<pre><code class="language-ts">


export const auth = betterAuth({
  plugins: [
    organization({
      async sendInvitationEmail(data) {
        // Gestion de votre envoi d&#x27;invitation par email
      },
      async onInvitationAccepted(data) {
        // Gestion après qu&#x27;un utilisateur accepte une invitation
      },
    }),
  ],
})
</code></pre>
<p>⚙️ <strong>De notre côté</strong>, dans le mail d&#x27;invitation, nous avons inclus un lien vers une page de redirection <code>/accept-invitation/[invitationId]</code>. Elle redirige l&#x27;utilisateur vers la page de connexion s’il n’est pas authentifié ou vers une page de gestion dédiée où il peut <a href="https://www.better-auth.com/docs/plugins/organization#accept-invitation">accepter</a> ou <a href="https://www.better-auth.com/docs/plugins/organization#cancel-invitation">refuser</a> son invitation.</p>
<h2>Pour aller plus loin : le mode Teams</h2>
<p>Better Auth permet d’ajouter <strong>un niveau hiérarchique supplémentaire</strong> grâce au flag de configuration <a href="https://www.better-auth.com/docs/plugins/organization#teams"><strong>teams</strong></a>. Chaque organisation pourra ainsi avoir différentes équipes, chacune avec ses propres membres et rôles.</p>
<p>C’est particulièrement utile pour les applications complexes où :</p>
<ul>
<li>une entreprise regroupe plusieurs départements,</li>
<li>un utilisateur a des rôles différents selon l’équipe,</li>
<li>on souhaite affiner les permissions sans multiplier les organisations.</li>
</ul>
<pre><code class="language-ts">


export const auth = betterAuth({
  plugins: [
    organization({
      teams: { enabled: true },
    }),
  ],
})
</code></pre>
<p>Better Auth gère automatiquement les relations entre <code>organization</code>, <code>team</code> et <code>member</code> : les utilisateurs pourront faire partie de plusieurs équipes au sein d&#x27;une même organisation. <a href="https://www.better-auth.com/docs/plugins/organization#teams">Le système d’Access Control</a> s’applique également <strong>au niveau de l’équipe</strong>, permettant par exemple de limiter une action à un rôle dans une seule équipe.</p>
<h2>Conclusion</h2>
<p>Le plugin <a href="https://www.better-auth.com/docs/plugins/organization"><strong>Organization</strong></a> de Better Auth offre une flexibilité impressionnante, il permet de construire un système d’accès robuste et lisible. Le système de permissions est <strong>entièrement extensible</strong>, vous pouvez créer vos propres entités (<code>project</code>, <code>team</code>, <code>workspace</code>) et définir autant de types d’actions que nécessaire pour coller à vos besoins métier !</p>
<p>Avec la configuration <strong>Teams</strong>, on peut modéliser des structures hiérarchiques avancées sans perdre la simplicité de son API.</p>
<p>Better Auth ne se limite donc pas à l’authentification, <strong>c’est un véritable socle d’autorisation moderne</strong>, extensible et agréable à utiliser.</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/better-auth-structure-et-permissions-avec-le-plugin-organization/illu.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[OpenAI Apps SDK : développer des apps natives dans ChatGPT]]></title>
            <link>https://www.premieroctet.com/blog/openai-apps-sdk-developper-des-apps-natives-dans-chatgpt</link>
            <guid>https://www.premieroctet.com/blog/openai-apps-sdk-developper-des-apps-natives-dans-chatgpt</guid>
            <pubDate>Tue, 07 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Découvrez l'Apps SDK d'OpenAI : développez des applications natives dans ChatGPT avec TypeScript, React et le Model Context Protocol.]]></description>
            <content:encoded><![CDATA[<p>Imaginez proposer à vos utilisateurs de commander un repas, réserver un trajet ou interagir avec vos APIs directement dans ChatGPT, sans jamais quitter la conversation. C&#x27;est exactement ce que permet l&#x27;<a href="https://developers.openai.com/apps-sdk">Apps SDK</a> d&#x27;OpenAI, fraîchement annoncé.</p>
<p>ChatGPT ne se contente plus d&#x27;être un assistant textuel : il devient une <strong>plateforme d&#x27;applications</strong>, comparable à l&#x27;App Store pour iOS ou au Play Store pour Android.</p>
<h2>Les Apps ChatGPT : pourquoi ?</h2>
<p>L&#x27;Apps SDK d&#x27;OpenAI transforme ChatGPT d&#x27;un simple assistant conversationnel en une <strong>plateforme d&#x27;applications</strong>. Avec plus de 100 millions d&#x27;utilisateurs actifs hebdomadaires, ChatGPT représente désormais un canal de distribution sans précédent pour les entreprises.</p>
<p>Cette transformation ouvre des perspectives commerciales. Les développeurs peuvent désormais créer des applications natives qui s&#x27;intègrent parfaitement dans l&#x27;expérience utilisateur de ChatGPT, offrant un accès direct à une audience engagée.</p>
<p>Comme toute technologie émergente, l&#x27;Apps SDK a ses contraintes. OpenAI est très clair sur les cas d&#x27;usage à éviter :</p>
<ul>
<li>Des contenus longs et complexes (mieux vaut un site web classique)</li>
<li>Des workflows multi-étapes complexes (ChatGPT n&#x27;est pas optimisé pour ça)</li>
<li>De la publicité ou du contenu promotionnel non sollicité (ça dégrade l&#x27;expérience utilisateur)</li>
<li>L&#x27;affichage d&#x27;informations sensibles directement dans la conversation</li>
</ul>
<h2>L&#x27;architecture technique : MCP au cœur</h2>
<h3>Le protocole au cœur de l&#x27;architecture : MCP</h3>
<p>Si vous êtes développeur, voici la bonne nouvelle : l&#x27;Apps SDK ne réinvente pas la roue. Elle s&#x27;appuie sur le <a href="https://modelcontextprotocol.io/">Model Context Protocol</a>, un <strong>standard ouvert</strong> créé par Anthropic et adopté par OpenAI.</p>
<p>Qu&#x27;est-ce que cela signifie concrètement ? Que le code que vous écrivez aujourd&#x27;hui pour ChatGPT pourrait fonctionner demain avec d&#x27;autres clients MCP. C&#x27;est rare dans l&#x27;écosystème IA, et c&#x27;est une excellente nouvelle pour pérenniser vos développements.</p>
<p>MCP définit trois primitives essentielles :</p>
<ol>
<li><strong>List tools</strong> : votre serveur expose les outils disponibles avec leurs schémas</li>
<li><strong>Call tools</strong> : ChatGPT invoque vos outils avec les arguments appropriés</li>
<li><strong>Return components</strong> : chaque outil peut retourner une interface HTML à rendre</li>
</ol>
<p>L&#x27;avantage ? Votre connecteur fonctionnera automatiquement sur ChatGPT web, mobile, et potentiellement sur tous les futurs clients compatibles MCP. Vous construisez une fois, vous déployez partout.</p>
<h3>Choisir sa stack : TypeScript ou Python ?</h3>
<p>Pour développer votre app, vous pouvez utiliser les SDKs MCP officiels :</p>
<p><strong>TypeScript</strong> (notre préféré bien sûr ❤️) :</p>
<ul>
<li><a href="https://github.com/modelcontextprotocol/typescript-sdk">@modelcontextprotocol/sdk</a></li>
<li>Parfait si vous avez déjà une équipe Node.js/React</li>
<li>S&#x27;intègre naturellement avec vos composants web existants</li>
<li>Excellent outillage avec TypeScript, Zod, et l&#x27;écosystème moderne</li>
</ul>
<p><strong>Python</strong> :</p>
<ul>
<li><a href="https://github.com/modelcontextprotocol/python-sdk">modelcontextprotocol/python-sdk</a></li>
<li>Inclut FastMCP pour démarrer rapidement</li>
<li>Idéal pour les équipes data science ou machine learning</li>
<li>Pratique si votre backend est déjà en Python</li>
</ul>
<p>Pour la suite de cet article, nous allons nous concentrer sur <strong>TypeScript</strong>, car c&#x27;est la stack que nous privilégions chez Premier Octet.</p>
<h2>Implémentation technique : passons à la pratique</h2>
<p>Construisons ensemble une app ChatGPT concrète. Nous allons créer un widget d&#x27;articles de blog Premier Octet, du serveur au composant React.</p>
<h3>Étape 1 : Monter le serveur MCP</h3>
<p>La première étape consiste à créer un serveur qui expose nos outils. Voici le code de démarrage :</p>
<pre><code class="language-typescript">



// Initialiser le serveur MCP
const server = new McpServer({
  name: &#x27;premier-octet-blog&#x27;,
  version: &#x27;1.0.0&#x27;,
})

// Charger les assets compilés du composant React
const BLOG_JS = readFileSync(&#x27;web/dist/blog-widget.js&#x27;, &#x27;utf8&#x27;)
const BLOG_CSS = readFileSync(&#x27;web/dist/blog-widget.css&#x27;, &#x27;utf8&#x27;)

// Enregistrer la ressource UI (template HTML)
server.registerResource(&#x27;blog-widget&#x27;, &#x27;ui://widget/blog-articles.html&#x27;, {}, async () =&gt; ({
  contents: [
    {
      uri: &#x27;ui://widget/blog-articles.html&#x27;,
      mimeType: &#x27;text/html+skybridge&#x27;,
      text: `
        &lt;div id=&quot;blog-root&quot;&gt;&lt;/div&gt;
        ${BLOG_CSS ? `&lt;style&gt;${BLOG_CSS}&lt;/style&gt;` : &#x27;&#x27;}
        &lt;script type=&quot;module&quot;&gt;${BLOG_JS}&lt;/script&gt;
      `.trim(),
    },
  ],
}))
</code></pre>
<p>Rien de compliqué ici : on initialise le serveur, on charge notre bundle React compilé, et on l&#x27;enregistre comme ressource. Le <code>mimeType: &#x27;text/html+skybridge&#x27;</code> est crucial : c&#x27;est le signal qui dit à ChatGPT &quot;voici une interface à afficher&quot;. Skybridge est le runtime sandboxé de ChatGPT.</p>
<h3>Étape 2 : Définir un outil</h3>
<p>Maintenant, créons un outil que ChatGPT pourra invoquer. Vous définissez ce que votre app peut faire, et ChatGPT décide quand l&#x27;utiliser en fonction du contexte de la conversation.</p>
<pre><code class="language-typescript">server.registerTool(
  &#x27;show_blog_articles&#x27;,
  {
    title: &#x27;Afficher les articles du blog Premier Octet&#x27;,
    description:
      &quot;Utilisez cet outil quand l&#x27;utilisateur veut découvrir les derniers articles du blog Premier Octet ou rechercher des articles par thème&quot;,
    inputSchema: {
      type: &#x27;object&#x27;,
      properties: {
        category: {
          type: &#x27;string&#x27;,
          description: &quot;Catégorie d&#x27;articles (ex: react, typescript, openai)&quot;,
        },
        limit: { type: &#x27;number&#x27;, description: &quot;Nombre d&#x27;articles à afficher (défaut: 6)&quot; },
      },
      required: [],
    },
    _meta: {
      &#x27;openai/outputTemplate&#x27;: &#x27;ui://widget/blog-articles.html&#x27;,
      &#x27;openai/widgetAccessible&#x27;: true, // Permet les appels depuis le composant
      &#x27;openai/toolInvocation/invoking&#x27;: &#x27;Chargement des articles...&#x27;,
      &#x27;openai/toolInvocation/invoked&#x27;: &#x27;Articles affichés&#x27;,
    },
  },
  async ({ category, limit = 6 }, context) =&gt; {
    // Récupérer les articles depuis votre API
    const articles = await fetchBlogArticles({ category, limit })

    return {
      // Données structurées pour le modèle ET le composant
      structuredContent: {
        articles: articles.map((article) =&gt; ({
          id: article.id,
          title: article.title,
          excerpt: article.excerpt,
          date: article.date,
          author: article.author,
          tags: article.tags,
          slug: article.slug,
        })),
        total: articles.length,
        category: category || &#x27;tous&#x27;,
      },
      // Texte pour le transcript de conversation
      content: [
        {
          type: &#x27;text&#x27;,
          text: `Voici ${articles.length} articles du blog Premier Octet${
            category ? ` sur le thème &quot;${category}&quot;` : &#x27;&#x27;
          }.`,
        },
      ],
      // Métadonnées privées (invisibles au modèle)
      _meta: {
        fullArticles: articles, // Données complètes pour le UI
        searchQuery: category,
      },
    }
  }
)
</code></pre>
<p>Remarquez comment nous séparons clairement les données :</p>
<ul>
<li><code>structuredContent</code> : ce que le modèle ET le composant voient (gardez ça concis !)</li>
<li><code>content</code> : le texte qui apparaîtra dans la conversation</li>
<li><code>_meta</code> : les données privées, invisibles au modèle mais disponibles pour votre UI</li>
</ul>
<h3>Étape 3 : Construire le composant React</h3>
<p>Vous développez déjà des composants React ? Bonne nouvelle : vous pouvez les utiliser ! La seule différence, c&#x27;est qu&#x27;ils tournent dans une iframe sandboxée et communiquent avec ChatGPT via l&#x27;API <code>window.openai</code>.</p>
<pre><code class="language-tsx">


interface BlogData {
  articles: Array&lt;{
    id: string
    title: string
    excerpt: string
    date: string
    author: string
    tags: string[]
    slug: string
  }&gt;
  total: number
  category: string
}

function BlogWidget() {
  // Récupérer les données initiales du tool
  const initialData = window.openai?.toolOutput as BlogData
  const [articles, setArticles] = useState(initialData)

  // Persister l&#x27;état local dans ChatGPT
  const saveState = async (newArticles: BlogData) =&gt; {
    setArticles(newArticles)
    await window.openai?.setWidgetState?.({
      version: 1,
      articles: newArticles,
      lastModified: Date.now(),
    })
  }

  // Appeler un outil depuis le composant
  const searchArticles = async (category: string) =&gt; {
    await window.openai?.callTool(&#x27;show_blog_articles&#x27;, {
      category,
      limit: 6,
    })
  }

  // Gérer les changements de layout
  const displayMode = window.openai?.displayMode || &#x27;inline&#x27;
  const maxHeight = window.openai?.maxHeight

  return (
    &lt;div
      style={{ maxHeight }}
      className={displayMode === &#x27;fullscreen&#x27; ? &#x27;fullscreen-layout&#x27; : &#x27;inline-layout&#x27;}
    &gt;
      &lt;div className=&quot;blog-header&quot;&gt;
        &lt;h2&gt;Articles Premier Octet&lt;/h2&gt;
        &lt;div className=&quot;category-filters&quot;&gt;
          &lt;button onClick={() =&gt; searchArticles(&#x27;&#x27;)}&gt;Tous&lt;/button&gt;
          &lt;button onClick={() =&gt; searchArticles(&#x27;react&#x27;)}&gt;React&lt;/button&gt;
          &lt;button onClick={() =&gt; searchArticles(&#x27;typescript&#x27;)}&gt;TypeScript&lt;/button&gt;
          &lt;button onClick={() =&gt; searchArticles(&#x27;openai&#x27;)}&gt;OpenAI&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div className=&quot;articles-grid&quot;&gt;
        {articles.articles.map((article) =&gt; (
          &lt;article key={article.id} className=&quot;article-card&quot;&gt;
            &lt;h3&gt;{article.title}&lt;/h3&gt;
            &lt;p className=&quot;article-excerpt&quot;&gt;{article.excerpt}&lt;/p&gt;
            &lt;div className=&quot;article-meta&quot;&gt;
              &lt;span className=&quot;author&quot;&gt;{article.author}&lt;/span&gt;
              &lt;span className=&quot;date&quot;&gt;{new Date(article.date).toLocaleDateString(&#x27;fr-FR&#x27;)}&lt;/span&gt;
            &lt;/div&gt;
            &lt;div className=&quot;article-tags&quot;&gt;
              {article.tags.map((tag) =&gt; (
                &lt;span key={tag} className=&quot;tag&quot;&gt;
                  {tag}
                &lt;/span&gt;
              ))}
            &lt;/div&gt;
          &lt;/article&gt;
        ))}
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

// Monter le composant
createRoot(document.getElementById(&#x27;blog-root&#x27;)!).render(&lt;BlogWidget /&gt;)
</code></pre>
<p>Ce composant illustre les concepts clés :</p>
<ul>
<li>Lecture des données initiales depuis <code>toolOutput</code></li>
<li>Persistance de l&#x27;état avec <code>setWidgetState</code> (pour que l&#x27;état survive aux re-rendus)</li>
<li>Appels d&#x27;outils depuis le composant avec <code>callTool</code> pour filtrer par catégorie</li>
<li>Adaptation au layout (inline vs fullscreen)</li>
<li>Interface utilisateur avec filtres et grille d&#x27;articles</li>
</ul>
<h3>Étape 4 : l&#x27;API window.openai</h3>
<p>L&#x27;API <code>window.openai</code> est votre pont entre le composant et ChatGPT :</p>
<pre><code class="language-typescript">// Données
window.openai.toolInput // Arguments passés à l&#x27;outil
window.openai.toolOutput // Réponse de l&#x27;outil (structuredContent)
window.openai.widgetState // État persisté entre les rendus

// Actions
await window.openai.setWidgetState({
  /* state */
})
await window.openai.callTool(&#x27;tool_name&#x27;, {
  /* args */
})
await window.openai.sendFollowupTurn({ prompt: &#x27;...&#x27; })
await window.openai.requestDisplayMode({ mode: &#x27;fullscreen&#x27; })

// Layout &amp; contexte
window.openai.displayMode // &quot;inline&quot; | &quot;pip&quot; | &quot;fullscreen&quot;
window.openai.maxHeight // Hauteur max disponible
window.openai.locale // &quot;fr-FR&quot;, &quot;en-US&quot;...
window.openai.theme // &quot;light&quot; | &quot;dark&quot;
</code></pre>
<p>Ces méthodes couvrent 90% de vos besoins. Le reste de la documentation officielle complètera pour les cas avancés.</p>
<h3>Étape 5 : sécuriser avec OAuth 2.1</h3>
<p>Si votre app accède à des données utilisateur (ce qui sera souvent le cas), vous devez implémenter l&#x27;authentification. Bonne nouvelle : l&#x27;Apps SDK supporte OAuth 2.1 de bout en bout, avec vérification automatique des tokens.</p>
<pre><code class="language-typescript">

// Configurer l&#x27;authentification
const mcp = new FastMCP({
  name: &#x27;secure-blog&#x27;,
  auth: {
    issuerUrl: &#x27;https://your-tenant.auth0.com&#x27;,
    resourceServerUrl: &#x27;https://api.example.com/mcp&#x27;,
    requiredScopes: [&#x27;blog:read&#x27;, &#x27;blog:write&#x27;],
  },
})

// Vérifier les tokens sur chaque appel
mcp.registerTool(
  &#x27;show_blog_articles&#x27;,
  {
    /* ... */
  },
  async ({ projectId }, { token }) =&gt; {
    // Le token est automatiquement vérifié
    const userId = token.subject
    const hasAccess = token.scopes.includes(&#x27;blog:read&#x27;)

    if (!hasAccess) {
      throw new Error(&#x27;Insufficient permissions&#x27;)
    }

    // Charger les données de l&#x27;utilisateur
    return await loadUserBoard(userId, projectId)
  }
)
</code></pre>
<p>Pour accélérer l&#x27;implémentation, utilisez <a href="https://www.premieroctet.com/blog/better-auth-futur-authjs">Better Auth</a>. Il supporte OAuth 2.1, l&#x27;enregistrement dynamique des clients, et la gestion des scopes nativement.</p>
<h2>Rendre votre app découvrable</h2>
<p>La découverte de votre app par les utilisateurs est un enjeu crucial.</p>
<h3>Les différents chemins vers votre app</h3>
<p>ChatGPT offre plusieurs façons aux utilisateurs de découvrir et d&#x27;utiliser votre app :</p>
<ol>
<li><strong>Mention explicite</strong> : &quot;Montre-moi les articles du blog de Premier Octet&quot;</li>
<li><strong>Découverte conversationnelle</strong> : le modèle choisit votre app selon le contexte</li>
<li><strong>Directory</strong> : répertoire des apps avec métadonnées et captures d&#x27;écran</li>
<li><strong>Launcher</strong> : bouton + dans le composer</li>
</ol>
<p>La <strong>qualité de vos métadonnées</strong> fait toute la différence. ChatGPT décide d&#x27;appeler votre outil en analysant sa description. Comparez :</p>
<pre><code class="language-typescript">// ✅ Bon : description action-oriented et contextuelle
description: &quot;Utilisez cet outil quand l&#x27;utilisateur veut visualiser les articles du blog de Premier Octet&quot;

// ❌ Mauvais : trop vague, le modèle ne saura pas quand l&#x27;utiliser
description: &#x27;Un outil pour afficher des articles&#x27;
</code></pre>
<h2>Sécurité et vie privée</h2>
<p>Une app ChatGPT a accès à des données utilisateur sensibles. OpenAI impose des standards stricts de sécurité, et vous devriez en faire autant.</p>
<h3>Les principes de base</h3>
<p>OpenAI impose des standards stricts :</p>
<ol>
<li><strong>Principe du moindre privilège</strong> : ne demandez que les scopes nécessaires</li>
<li><strong>Consentement explicite</strong> : les utilisateurs doivent comprendre ce qu&#x27;ils autorisent</li>
<li><strong>Défense en profondeur</strong> : validez tout côté serveur, même si le modèle l&#x27;a fourni</li>
<li><strong>Minimisation des données</strong> : ne collectez que ce qui est strictement nécessaire</li>
</ol>
<h3>Sandboxing des composants</h3>
<p>Les composants s&#x27;exécutent dans une iframe avec CSP stricte :</p>
<pre><code class="language-typescript">// Définir votre CSP
server.registerResource(&#x27;blog-widget&#x27;, &#x27;ui://widget/blog-articles.html&#x27;, {}, async () =&gt; ({
  contents: [
    {
      uri: &#x27;ui://widget/blog-articles.html&#x27;,
      mimeType: &#x27;text/html&#x27;,
      text: componentHtml,
      _meta: {
        &#x27;openai/widgetCSP&#x27;: {
          connect_domains: [&#x27;https://api.example.com&#x27;],
          resource_domains: [&#x27;https://cdn.example.com&#x27;],
        },
      },
    },
  ],
}))
</code></pre>
<h2>Pourquoi TypeScript est votre meilleur allié</h2>
<p>On en a parlé au début, mais revenons-y : <strong>TypeScript est vraiment le choix optimal</strong> pour l&#x27;Apps SDK. Voici pourquoi :</p>
<h3>Les avantages concrets</h3>
<ol>
<li><strong>Type safety</strong> : Zod + TypeScript vous évitent les bugs. Vos schémas serveur et client sont toujours cohérents.</li>
<li><strong>Réutilisabilité</strong> : vos composants React existants fonctionnent directement.</li>
<li><strong>Tooling performant</strong> : esbuild compile en millisecondes, TypeScript détecte les erreurs avant l&#x27;exécution</li>
<li><strong>Isomorphisme</strong> : un seul langage, des types partagés serveur/client, zéro friction</li>
</ol>
<h3>Exemple : types partagés</h3>
<pre><code class="language-typescript">// shared/types.ts


export const ArticleSchema = z.object({
  id: z.string(),
  title: z.string(),
  excerpt: z.string(),
  date: z.string(),
  author: z.string(),
  tags: z.array(z.string()),
  slug: z.string(),
})

export const BlogDataSchema = z.object({
  articles: z.array(ArticleSchema),
  total: z.number(),
  category: z.string(),
})

export type Article = z.infer&lt;typeof ArticleSchema&gt;
export type BlogData = z.infer&lt;typeof BlogDataSchema&gt;

// Serveur
server.registerTool(
  &#x27;show_blog_articles&#x27;,
  {
    inputSchema: z.object({
      category: z.string().optional(),
      limit: z.number().optional(),
    }),
  },
  async ({ category, limit = 6 }) =&gt; {
    const blogData: BlogData = await fetchBlogArticles({ category, limit })
    return { structuredContent: blogData }
  }
)

// Client React
function BlogWidget() {
  const blogData = window.openai?.toolOutput as BlogData
  // TypeScript sait que blogData.articles existe !
}
</code></pre>
<p>Vous voyez l&#x27;idée : un type défini une fois, utilisé partout. Zéro duplication, zéro désynchronisation.</p>
<h2>L&#x27;UX comme critère de succès</h2>
<p>Une excellente tech ne suffit pas si l&#x27;expérience utilisateur est mauvaise. OpenAI a publié des <a href="https://developers.openai.com/apps-sdk/concepts/design-guidelines">design guidelines</a> très complètes. Voici ce qu&#x27;il faut retenir.</p>
<h3>Les trois modes d&#x27;affichage</h3>
<p>L&#x27;Apps SDK vous permet de choisir comment votre app s&#x27;affiche. Chaque mode a son usage :</p>
<ol>
<li><strong>Inline</strong> : une carte légère dans la conversation — parfait pour une action simple (confirmer une réservation)</li>
<li><strong>Picture-in-Picture</strong> : une fenêtre flottante qui reste visible — idéal pour du contenu continu (vidéo, jeu)</li>
<li><strong>Fullscreen</strong> : une vue immersive avec le composer ChatGPT intégré — pour de l&#x27;édition complexe ou de l&#x27;exploration</li>
</ol>
<pre><code class="language-tsx">function BlogWidget() {
  const displayMode = window.openai?.displayMode

  if (displayMode === &#x27;fullscreen&#x27;) {
    return &lt;FullscreenBlogView /&gt;
  }

  return (
    &lt;InlineCard&gt;
      &lt;button
        onClick={() =&gt; {
          window.openai?.requestDisplayMode({ mode: &#x27;fullscreen&#x27; })
        }}
      &gt;
        Ouvrir en plein écran
      &lt;/button&gt;
    &lt;/InlineCard&gt;
  )
}
</code></pre>
<p>Le passage d&#x27;un mode à l&#x27;autre doit être fluide. Testez bien les trois cas.</p>
<h3>Les principes de design à respecter</h3>
<p>OpenAI insiste sur 5 principes fondamentaux. Ils peuvent sembler évidents, mais en pratique, beaucoup d&#x27;apps les ignorent :</p>
<ul>
<li><strong>Conversationnel</strong> : votre app doit prolonger ChatGPT, pas créer une rupture</li>
<li><strong>Intelligent</strong> : utilisez le contexte de la conversation pour adapter l&#x27;interface</li>
<li><strong>Simple</strong> : une action claire par interaction — ne surchargez pas</li>
<li><strong>Responsive</strong> : ça doit être rapide. Si votre tool met 3 secondes à répondre, c&#x27;est trop</li>
<li><strong>Accessible</strong> : dark mode, tailles de texte, lecteurs d&#x27;écran — faites les choses bien</li>
</ul>
<p>Gardez ces 5 principes constamment à l&#x27;esprit lors du développement.</p>
<h2>Tester et débugger efficacement</h2>
<h3>MCP Inspector : votre meilleur ami</h3>
<p>Avant même de toucher à ChatGPT, vous devez tester localement. Le <strong>MCP Inspector</strong> est l&#x27;outil indispensable :</p>
<pre><code class="language-bash">npx @modelcontextprotocol/inspector@latest
# Pointer vers http://localhost:3000/mcp
</code></pre>
<p>L&#x27;inspector permet de :</p>
<ul>
<li>Lister tous les outils exposés</li>
<li>Appeler les outils avec des paramètres</li>
<li>Visualiser les réponses JSON</li>
<li>Tester le rendu des composants</li>
</ul>
<h3>Tests automatisés</h3>
<pre><code class="language-typescript">


describe(&#x27;Blog Articles Tool&#x27;, () =&gt; {
  it(&#x27;should return articles data&#x27;, async () =&gt; {
    const result = await server.callTool(&#x27;show_blog_articles&#x27;, {
      category: &#x27;react&#x27;,
      limit: 3,
    })

    expect(result.structuredContent).toHaveProperty(&#x27;articles&#x27;)
    expect(result.structuredContent.articles).toBeInstanceOf(Array)
    expect(result.structuredContent.total).toBeGreaterThan(0)
  })

  it(&#x27;should filter by category&#x27;, async () =&gt; {
    const result = await server.callTool(&#x27;show_blog_articles&#x27;, {
      category: &#x27;typescript&#x27;,
    })

    expect(result.structuredContent.category).toBe(&#x27;typescript&#x27;)
  })
})
</code></pre>
<h3>Debug dans ChatGPT</h3>
<p>En mode développeur :</p>
<ol>
<li>Ouvrir les DevTools du navigateur</li>
<li>Les composants s&#x27;affichent dans une iframe</li>
<li>Les erreurs apparaissent dans la console</li>
<li>Utiliser <code>console.log</code> dans vos composants</li>
</ol>
<h2>Cas d&#x27;usage réels : inspiration</h2>
<p>OpenAI fournit l&#x27;app de démonstration <strong>Pizzaz</strong> avec plusieurs exemples :</p>
<ul>
<li><strong>Pizzaz List</strong> : liste classée de restaurants</li>
<li><strong>Pizzaz Map</strong> : carte interactive Mapbox</li>
<li><strong>Pizzaz Carousel</strong> : galerie horizontale</li>
<li><strong>Pizzaz Video</strong> : lecteur vidéo avec timeline</li>
<li><strong>Pizzaz Album</strong> : vue détaillée d&#x27;un lieu</li>
</ul>
<p><a href="https://github.com/openai/openai-apps-sdk-examples">Code source disponible</a></p>
<h2>Pour conclure</h2>
<p>L&#x27;<strong>Apps SDK</strong> transforme ChatGPT : d&#x27;un chatbot à une <strong>plateforme d&#x27;applications conversationnelles</strong>.</p>
<p>Techniquement, c&#x27;est bien pensé. L&#x27;architecture MCP, le support TypeScript, et l&#x27;intégration React rendent le développement accessible à toute équipe web moderne. Les guidelines strictes, bien que contraignantes, garantissent une expérience utilisateur cohérente.</p>
<p>Si vous envisagez de construire une app ChatGPT, ou simplement d&#x27;explorer le sujet, <a href="https://www.premieroctet.com/contact">parlons-en ensemble</a>.</p>
<p>👉 Ressources utiles :</p>
<ul>
<li><a href="https://developers.openai.com/apps-sdk">Documentation officielle Apps SDK</a></li>
<li><a href="https://modelcontextprotocol.io/">Model Context Protocol</a></li>
<li><a href="https://github.com/openai/openai-apps-sdk-examples">Exemples GitHub</a></li>
<li><a href="https://github.com/modelcontextprotocol/typescript-sdk">TypeScript SDK</a></li>
</ul>
<p>👋</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/openai-apps-sdk-developper-des-apps-natives-dans-chatgpt/illu.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Better Auth : le futur d’Auth.js]]></title>
            <link>https://www.premieroctet.com/blog/better-auth-futur-authjs</link>
            <guid>https://www.premieroctet.com/blog/better-auth-futur-authjs</guid>
            <pubDate>Mon, 06 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Retour d’expérience sur Better Auth : installation, plugins et migration depuis Supabase.]]></description>
            <content:encoded><![CDATA[<p>Chez <strong>Premier Octet</strong>, on explore en continu de nouvelles solutions pour simplifier et sécuriser nos développements. Pendant longtemps, <a href="https://next-auth.js.org/">NextAuth.js</a> (devenu <a href="https://authjs.dev/">Auth.js</a>) a été notre référence pour gérer l’authentification dans nos projets <a href="https://nextjs.org/"><strong>Next.js</strong></a>.</p>
<p>Très récemment, un tournant important a eu lieu : <a href="https://www.better-auth.com/blog/authjs-joins-better-auth"><strong>Auth.js rejoint Better Auth</strong></a>.</p>
<p><a href="https://www.better-auth.com/">Better Auth</a> devient donc le successeur officiel d&#x27;<strong>Auth.js</strong> et nous propose une vision plus large ainsi qu&#x27;une approche plus moderne de l’authentification.</p>
<p>Dans cet article, nous partageons notre retour d’expérience : installation, migration depuis Supabase et découverte des plugins.</p>
<h2>Sessions : une évolution d’approche</h2>
<p>Historiquement, <strong>Auth.js</strong> proposait <a href="https://authjs.dev/concepts/session-strategies#jwt-session">deux stratégies</a> :</p>
<ol>
<li>des <strong>sessions stateless</strong> avec un token JWT qui ne nécessitent pas de base de données. Une stratégie légère et pratique, mais si les données de la session évoluent (rôles, permissions…), il faut mettre en place des mécanismes supplémentaires pour invalider les tokens devenus obsolètes. <strong>Cette stratégie est celle appliquée par défaut.</strong></li>
<li>des <strong>sessions stateful</strong> stockées en base de données via un adapter (Prisma, Mongo…) et vérifiées à chaque requête. Elles permettent de garder les permissions constamment à jour, d’invalider immédiatement une session (ex. logout forcé) et de gérer facilement plusieurs appareils. En contrepartie, cette stratégie nécessite plus de ressources, puisqu’elle implique des appels serveur plus fréquents.</li>
</ol>
<p><strong>Better Auth</strong>, lui, a fait le choix d’un modèle basé avant tout sur des <strong>sessions stateful</strong>. Il propose toutefois un plugin <a href="https://www.better-auth.com/docs/plugins/bearer">Bearer Token Authentication</a> qui permet de mettre en place des <strong>sessions stateless</strong>.</p>
<h2>Installation et gestion de la base de données</h2>
<p>Là où Auth.js reposait sur des schémas à intégrer manuellement, Better Auth fournit un <strong>CLI qui génère directement les tables nécessaires</strong>. Résultat : une installation rapide, standardisée et avec moins de risques d’erreurs.</p>
<p>En pratique, voici la mise en place avec Prisma après avoir installé la librairie :</p>
<pre><code class="language-bash">npx @better-auth/cli generate
npx prisma migrate dev
</code></pre>
<div><div><div value="auth.ts">auth.ts</div><div value="clients/auth.ts">clients/auth.ts</div><div value="api/auth/[...all]/route.ts">api/auth/[...all]/route.ts</div></div><div value="auth.ts"><pre><code class="language-tsx">



const prisma = new PrismaClient();

export const auth = betterAuth({
  secret: process.env.BETTER_AUTH_SECRET,
  database: prismaAdapter(prisma, {
    provider: &quot;postgresql&quot;,
  }),
});
</code></pre></div><div value="clients/auth.ts"><pre><code class="language-tsx">

const authClient = createAuthClient();

export const {
  useSession,
  getSession,
  signIn,
  signOut,
  changeEmail,
  changePassword,
  updateUser,
} = authClient;
</code></pre></div><div value="api/auth/[...all]/route.ts"><pre><code class="language-tsx">


export const { GET, POST } = toNextJsHandler(auth.handler);
</code></pre></div></div>
<p>Et voilà. ✨ Inscription, connexion, gestion des sessions, modification d’email, réinitialisation de mot de passe, <strong>tout est fonctionnel</strong> et accessible depuis l&#x27;API via les appels <code>auth.api</code> pour le côté serveur et <code>authClient</code> pour le côté client.</p>
<p>Better Auth ne fournit pas d&#x27;UI prête à l&#x27;emploi mais après une <a href="https://www.better-auth.com/docs/authentication/email-password#enable-email-and-password">configuration rapide</a>, ses méthodes permettent une intégration sans prise de tête, que ce soit pour la connexion par <a href="https://www.better-auth.com/docs/authentication/email-password">credentials</a> ou via des providers sociaux comme <a href="https://www.better-auth.com/docs/authentication/google">Google</a>, <a href="https://www.better-auth.com/docs/authentication/apple">Apple</a>, <a href="https://www.better-auth.com/docs/authentication/github">GitHub</a>, etc.</p>
<h2>Migration depuis Supabase</h2>
<p>Nous avons également testé la <a href="https://www.better-auth.com/docs/guides/supabase-migration-guide">migration depuis Supabase</a> sur l&#x27;une de nos applications. Encore une fois, le processus est très simple :</p>
<ol>
<li>Récupérer l’URL de la base de données sur Supabase.</li>
<li>Générer les tables Better Auth avec le CLI.</li>
<li>Lancer le <a href="https://www.better-auth.com/docs/guides/supabase-migration-guide#copy-the-migration-script">script officiel</a> pour convertir les utilisateurs.</li>
</ol>
<p>⚠️ <strong>Limite actuelle</strong> : <a href="https://supabase.com/docs/guides/auth/password-security#how-are-passwords-stored">Supabase utilise <strong>bcrypt</strong></a> pour le hachage des mots de passe tandis que <a href="https://www.better-auth.com/docs/authentication/email-password#configuration">Better Auth repose sur <strong>scrypt</strong></a>. Les utilisateurs devront donc <strong>réinitialiser leur mot de passe</strong> après migration. En production, une communication claire (mailing, bannière, etc.) est indispensable.</p>
<h2>Plugins : aller plus loin</h2>
<p>Tandis qu&#x27;Auth.js se concentrait sur les providers (Google, GitHub, Credentials, etc.), Better Auth propose en plus des <strong>plugins</strong> qui adressent des <strong>besoins métiers concrets</strong> : organisations, rôles avancés, 2FA, magic links.</p>
<p><strong>Exemple</strong> : <a href="https://www.better-auth.com/docs/plugins/organization"><strong>Organization</strong></a> est un plugin qui facilite la gestion d’équipes ou de structures dans une application. Il génère automatiquement les tables nécessaires et expose des méthodes prêtes à l’emploi pour :</p>
<ul>
<li>créer une organisation,</li>
<li>inviter des membres,</li>
<li>attribuer et modifier des rôles,</li>
<li>gérer les droits d’accès associés.</li>
</ul>
<p>En pratique, cela permet de mettre en place un système complet de gestion multi-utilisateurs sans avoir à réécrire nous-mêmes toute la logique métier.</p>
<div><div><div value="auth.ts">auth.ts</div><div value="clients/auth.ts">clients/auth.ts</div></div><div value="auth.ts"><pre><code class="language-tsx">


export const auth = betterAuth({
    plugins: [organization()]
})
</code></pre></div><div value="clients/auth.ts"><pre><code class="language-tsx">


export const authClient = createAuthClient({
    plugins: [organizationClient()]
})
</code></pre></div></div>
<p>En quelques lignes seulement, les plugins ouvrent la porte à des fonctionnalités avancées : <a href="https://www.better-auth.com/docs/plugins/2fa">2FA</a> pour renforcer la sécurité, <a href="https://www.better-auth.com/docs/plugins/magic-link">magic links</a> pour simplifier la connexion, des <a href="https://www.better-auth.com/docs/plugins/admin">outils de gestion administrateur</a> (dont la très utile <a href="https://www.better-auth.com/docs/plugins/admin#impersonate-user">impersonification</a>), et bien plus encore !</p>
<h2>Conclusion</h2>
<p>Tout au long de cet article, le mot <strong>simple</strong> est revenu plus d’une fois et ce n’est pas un hasard. Better Auth a été conçu avec la <strong>Developper Experience</strong> en tête et cela se ressent dès la première installation : tout est fluide et agréable à utiliser.</p>
<p>Au-delà de cette facilité d’usage, Better Auth est désormais le <strong>successeur officiel d’Auth.js</strong>. C’est un outil solide qui combine <strong>sécurité, extensibilité et rapidité de mise en place</strong>.</p>
<p>Chez Premier Octet, Better Auth est désormais notre solution de référence pour gérer l’authentification dans les projets clients. Sa simplicité d’intégration et sa flexibilité en font un choix naturel pour nos développements.</p>
<p>👉 Si vous utilisez déjà Auth.js, <a href="https://www.better-auth.com/"><strong>Better Auth</strong></a> vaut clairement la <a href="https://www.better-auth.com/docs/guides/next-auth-migration-guide">migration</a>.</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/better-auth-futur-authjs/illu.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[E-commerce conversationnel avec OpenAI et Stripe : guide complet de l’Agentic Commerce Protocol]]></title>
            <link>https://www.premieroctet.com/blog/ecommerce-conversationnel-avec-openai-et-stripe-guide-complet-de-l-agentic-commerce-protocol</link>
            <guid>https://www.premieroctet.com/blog/ecommerce-conversationnel-avec-openai-et-stripe-guide-complet-de-l-agentic-commerce-protocol</guid>
            <pubDate>Wed, 01 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Découvrez comment intégrer l’Agentic Commerce Protocol d’OpenAI et Stripe pour vendre directement dans ChatGPT. Guide technique, roadmap et bonnes pratiques.]]></description>
            <content:encoded><![CDATA[<p>OpenAI et Stripe ont annoncé l’<strong>Agentic Commerce Protocol (ACP)</strong>, une spécification open source (Apache 2.0) qui permet d’acheter directement depuis une conversation dans <strong>ChatGPT</strong>, sans passer par un site tiers.</p>
<p>Concrètement, un utilisateur peut demander un produit dans ChatGPT, voir une fiche détaillée et finaliser son achat via un bouton “Acheter”. Côté marchand, cela implique de mettre en place trois briques techniques :</p>
<ul>
<li>un <strong>flux produit</strong> (Product Feed) pour rendre le catalogue accessible,</li>
<li>une <strong>API Checkout</strong> pour gérer les sessions et les commandes,</li>
<li>un mécanisme de <strong>paiement délégué</strong> qui s’appuie notamment sur Stripe.</li>
</ul>
<p>L’accès est pour l’instant limité aux États-Unis et nécessite une validation d’OpenAI, mais l’annonce donne une idée claire de la direction que prend l’e-commerce conversationnel.</p>
<p>Dans cet article, nous expliquons en détail comment fonctionne le protocole, ses trois piliers techniques, et proposons une <strong>roadmap de préparation</strong> pour anticiper une intégration future.</p>
<h2>Qu’est-ce que l’Agentic Commerce Protocol ?</h2>
<p>L’ACP est une spécification ouverte qui définit comment un système tiers (ici ChatGPT) peut <strong>afficher un catalogue produit, initier un checkout et déléguer le paiement</strong> à un PSP (Payment Service Provider) comme <strong>Stripe</strong>.</p>
<p>👉 L’objectif : permettre à vos clients de passer <strong>de la recherche au paiement en un seul flux conversationnel</strong>.</p>
<h3>Comment fonctionne l’e-commerce conversationnel dans ChatGPT ?</h3>
<p>Prenons l&#x27;exemple d&#x27;un utilisateur qui demanderait : &quot;Trouve-moi des chaussures de running à moins de 100 €&quot;.</p>
<p>ChatGPT peut alors :</p>
<ol>
<li>Afficher les produits de votre catalogue (via le Product Feed)</li>
<li>Proposer un bouton “Acheter”</li>
<li>Déclencher un <strong>checkout complet directement dans la conversation</strong></li>
</ol>
<p>Trois flux de données orchestrent ce parcours :</p>
<ul>
<li><strong>Product Feed</strong> : ChatGPT indexe vos produits pour les rendre découvrables</li>
<li><strong>API Checkout</strong> : vous gérez les sessions panier et la finalisation des commandes</li>
<li><strong>Paiement délégué</strong> : vos PSP gèrent la transaction en toute sécurité</li>
</ul>
<h2>Pilier 1 : Le Product Feed 📦</h2>
<p>Premier élément à mettre en place : le <strong>Product Feed</strong>. Avant que ChatGPT puisse recommander vos produits, il faut lui fournir un catalogue structuré et à jour. Sans ça, même si votre API Checkout est parfaite, vos produits resteront invisibles.</p>
<h3>Le format</h3>
<p>Vous avez le choix : <strong>TSV, CSV, XML ou JSON</strong>. Prenez celui qui s&#x27;intègre le mieux à votre système existant. Nous recommandons JSON pour sa flexibilité et sa lisibilité, mais CSV fera très bien l&#x27;affaire si c&#x27;est ce que votre système génère naturellement.</p>
<p>Le feed est poussé vers un endpoint OpenAI qui vous sera fourni lors de votre onboarding. Pas de pull côté OpenAI, c&#x27;est vous qui envoyez les mises à jour.</p>
<div type="info"><p>Fréquence de mise à jour : toutes les 15 minutes si nécessaire. C&#x27;est important pour maintenir la
cohérence entre votre stock réel et ce que ChatGPT affiche. Un produit en rupture ne doit pas être
proposé à l&#x27;achat.</p></div>
<h3>Les champs</h3>
<p>La structure est assez classique. Voici un exemple de produit que OpenAI attend :</p>
<pre><code class="language-json">[
  {
    &quot;id&quot;: &quot;SKU12345&quot;,
    &quot;title&quot;: &quot;Chaussures de running Nike Air Zoom Pegasus&quot;,
    &quot;description&quot;: &quot;Chaussures légères avec amorti réactif...&quot;,
    &quot;link&quot;: &quot;https://yourshop.com/products/SKU12345&quot;,
    &quot;price&quot;: &quot;89.99 EUR&quot;,
    &quot;currency&quot;: &quot;eur&quot;,
    &quot;availability&quot;: &quot;in_stock&quot;,
    &quot;image_link&quot;: &quot;https://yourshop.com/images/SKU12345.jpg&quot;,
    &quot;brand&quot;: &quot;Nike&quot;,
    &quot;condition&quot;: &quot;new&quot;,
    &quot;weight&quot;: &quot;280 g&quot;,
    &quot;enable_search&quot;: true,
    &quot;enable_checkout&quot;: true
  }
]
</code></pre>
<p>Les deux derniers flags (<code>enable_search</code> et <code>enable_checkout</code>) sont spécifiques à OpenAI et vous donnent un contrôle granulaire : un produit peut être découvrable dans la recherche sans être achetable directement.</p>
<p>Vous pouvez retrouver la liste complète des champs dans la <a href="https://developers.openai.com/commerce/specs/feed">documentation d&#x27;OpenAI</a>.</p>
<h3>Gérer les variantes</h3>
<p>Point important si vous vendez des produits déclinables : les variantes. Si vous vendez une chaussure en plusieurs tailles et couleurs, utilisez <code>item_group_id</code> pour regrouper les variantes. Concrètement, ça ressemble à ça :</p>
<pre><code class="language-json">[
  {
    &quot;id&quot;: &quot;SKU12345-42-BLUE&quot;,
    &quot;item_group_id&quot;: &quot;SKU12345&quot;,
    &quot;title&quot;: &quot;Nike Air Zoom Pegasus - Bleu&quot;,
    &quot;color&quot;: &quot;Bleu&quot;,
    &quot;size&quot;: &quot;42&quot;
    // ...
  },
  {
    &quot;id&quot;: &quot;SKU12345-43-BLUE&quot;,
    &quot;item_group_id&quot;: &quot;SKU12345&quot;,
    &quot;title&quot;: &quot;Nike Air Zoom Pegasus - Bleu&quot;,
    &quot;color&quot;: &quot;Bleu&quot;,
    &quot;size&quot;: &quot;43&quot;
    // ...
  }
]
</code></pre>
<p>ChatGPT saura alors proposer les différentes options au moment de l&#x27;achat.</p>
<h2>Pilier 2 : L&#x27;API Checkout 🛍️</h2>
<p>Une fois vos produits indexés dans ChatGPT, il faut gérer la partie achat. C&#x27;est le cœur du système, et c&#x27;est là que vous allez passer le plus de temps de développement.</p>
<p>Vous devez exposer 5 endpoints que ChatGPT va appeler pour gérer le cycle de vie d&#x27;un achat. L&#x27;idée est simple : ChatGPT orchestre, mais c&#x27;est vous qui calculez tout (prix, taxes, shipping) et qui décidez in fine d&#x27;accepter ou non la commande.</p>
<h3>1. Créer une session</h3>
<pre><code>POST /checkout_sessions
</code></pre>
<p>C&#x27;est le point d&#x27;entrée. Quand l&#x27;utilisateur clique sur &quot;Acheter&quot; dans ChatGPT, cet endpoint est appelé avec les items que l&#x27;utilisateur veut acheter et éventuellement son adresse de livraison :</p>
<p><strong>Requête :</strong></p>
<pre><code class="language-json">{
  &quot;items&quot;: [{ &quot;id&quot;: &quot;SKU12345-42-BLUE&quot;, &quot;quantity&quot;: 1 }],
  &quot;fulfillment_address&quot;: {
    &quot;name&quot;: &quot;Bptiste Adrien&quot;,
    &quot;line_one&quot;: &quot;18 avenue Parementier&quot;,
    &quot;city&quot;: &quot;Paris&quot;,
    &quot;postal_code&quot;: &quot;75018&quot;,
    &quot;country&quot;: &quot;FR&quot;
  }
}
</code></pre>
<h3>2. Mettre à jour la session</h3>
<pre><code>POST /checkout_sessions/{checkout_session_id}
</code></pre>
<p>L&#x27;utilisateur change d&#x27;avis sur son mode de livraison ou modifie son adresse ? Pas de problème. ChatGPT appelle cet endpoint avec les nouvelles données, vous recalculez taxes et frais de port, et vous retournez le même format de réponse que lors de la création.</p>
<p>C&#x27;est important que ce endpoint soit rapide, car l&#x27;utilisateur peut le déclencher plusieurs fois en ajustant ses choix.</p>
<h3>3. Finaliser l&#x27;achat</h3>
<pre><code>POST /checkout_sessions/{checkout_session_id}/complete
</code></pre>
<p>C&#x27;est le moment critique : l&#x27;utilisateur a validé son paiement.</p>
<p><strong>Requête :</strong></p>
<pre><code class="language-json">{
  &quot;buyer&quot;: {
    &quot;first_name&quot;: &quot;Baptiste&quot;,
    &quot;last_name&quot;: &quot;Adrien&quot;,
    &quot;email&quot;: &quot;hello@premieroctet.com&quot;
  },
  &quot;payment_data&quot;: {
    &quot;token&quot;: &quot;spt_1234567890&quot;,
    &quot;provider&quot;: &quot;stripe&quot;,
    &quot;billing_address&quot;: {
      /* ... */
    }
  }
}
</code></pre>
<p>À ce moment, vous :</p>
<ol>
<li>Chargez le moyen de paiement via votre PSP (Stripe, etc.)</li>
<li>Créez la commande dans votre système</li>
<li>Retournez l&#x27;ID de commande et un lien de suivi</li>
</ol>
<p><strong>Réponse :</strong></p>
<pre><code class="language-json">{
  &quot;id&quot;: &quot;checkout_session_abc123&quot;,
  &quot;status&quot;: &quot;completed&quot;,
  &quot;order&quot;: {
    &quot;id&quot;: &quot;order_789&quot;,
    &quot;permalink_url&quot;: &quot;https://yourshop.com/orders/order_789&quot;
  }
  // ... reste de l&#x27;état du checkout
}
</code></pre>
<p>Le <code>permalink_url</code> est crucial : c&#x27;est là que l&#x27;utilisateur pourra suivre sa commande.</p>
<h3>4 &amp; 5. Récupérer et annuler</h3>
<pre><code>GET /checkout_sessions/{checkout_session_id}
POST /checkout_sessions/{checkout_session_id}/cancel
</code></pre>
<p>Ces deux derniers endpoints sont plus simples. Le GET permet à ChatGPT de vérifier l&#x27;état actuel d&#x27;une session (utile en cas de reconnexion ou de perte de contexte). Le POST cancel permet à l&#x27;utilisateur d&#x27;annuler explicitement une session en cours.</p>
<p>Dans les deux cas, retournez l&#x27;état complet de la session avec le bon statut.</p>
<h3>Les webhooks</h3>
<p>L&#x27;API Checkout n&#x27;est pas unidirectionnelle. Vous devez aussi envoyer des webhooks vers OpenAI pour notifier des changements d&#x27;état de la commande. C&#x27;est comme ça que ChatGPT saura que la commande est confirmée, expédiée, livrée, ou annulée.</p>
<pre><code class="language-json">{
  &quot;type&quot;: &quot;order_updated&quot;,
  &quot;data&quot;: {
    &quot;type&quot;: &quot;order&quot;,
    &quot;checkout_session_id&quot;: &quot;checkout_session_abc123&quot;,
    &quot;permalink_url&quot;: &quot;https://yourshop.com/orders/order_789&quot;,
    &quot;status&quot;: &quot;shipped&quot;,
    &quot;refunds&quot;: []
  }
}
</code></pre>
<h2>Pilier 3 : Le paiement 💳</h2>
<p>Dernier pilier, et non des moindres : le paiement. C&#x27;est là que beaucoup se posent des questions légitimes sur la sécurité et la conformité PCI. OpenAI ne touche jamais directement à l&#x27;argent. Le protocole définit simplement comment transmettre de manière sécurisée les informations de paiement entre l&#x27;utilisateur, ChatGPT, et votre système. Vous restez en contrôle, et vous utilisez votre PSP habituel.</p>
<h3>Option 1 : Vous utilisez déjà Stripe</h3>
<p>C&#x27;est l&#x27;option la plus simple. Stripe a développé un mode &quot;agentic payments&quot; qui s&#x27;active en une ligne de code :</p>
<pre><code class="language-javascript">stripe.agentic_payments.enable()
</code></pre>
<p>Stripe gère alors la création et la transmission du token de paiement. Côté checkout, vous recevez un <code>payment_data.token</code> classique que vous chargez comme d&#x27;habitude.</p>
<h3>Option 2 : Vous utilisez un autre PSP</h3>
<p>Pas de Stripe dans votre stack ? Pas de panique, vous avez deux solutions.</p>
<p><strong>A. Via Stripe Shared Payment Token API</strong></p>
<p>Même si vous ne processez pas les paiements avec Stripe, vous pouvez utiliser leur API uniquement pour recevoir le token sécurisé. C&#x27;est une couche intermédiaire qui ne change rien à votre PSP principal (Adyen, Braintree, etc.). Vous recevez un token via Stripe, puis vous chargez le paiement avec votre PSP habituel.</p>
<p><strong>B. Implémentation directe (PSP ou PCI DSS Level 1 uniquement)</strong></p>
<p>Si vous êtes un PSP ou un merchant PCI DSS Level 1 avec votre propre vault, vous pouvez implémenter directement l&#x27;endpoint du protocole. Attention, cette option implique de manipuler directement les données de carte. Ne vous lancez là-dedans que si vous avez déjà l&#x27;infrastructure et la conformité PCI en place.</p>
<h2>Quelques points d&#x27;attention</h2>
<p>Avant de vous lancer, voici quelques limitations (à date) à garder en tête :</p>
<ul>
<li><strong>Géographie</strong> : USA uniquement pour l&#x27;instant</li>
<li><strong>Approbation</strong> : pas d&#x27;accès automatique, il faut postuler</li>
<li><strong>Single-item</strong> : pas de panier multi-produits (pour le moment)</li>
<li><strong>Pas de promo codes complexes</strong> : gardez la logique simple au début</li>
</ul>
<h2>Le mot de la fin</h2>
<p>Voilà, vous avez maintenant une vision complète de ce qu&#x27;implique l&#x27;implémentation de l&#x27;Agentic Commerce Protocol.</p>
<p><strong>Pour les développeurs</strong>, c&#x27;est une intégration REST classique. Rien de sorcier si vous avez déjà un système de checkout robuste. L&#x27;effort principal sera d&#x27;adapter votre logique métier (pricing, taxes, shipping) pour qu&#x27;elle soit exposable via une API.</p>
<p><strong>Pour les business</strong>, c&#x27;est un nouveau canal à tester. 700 millions d&#x27;utilisateurs de ChatGPT représentent une audience considérable. Même si l&#x27;accès est limité pour le moment, préparer votre infrastructure maintenant vous donnera un avantage quand les vannes s&#x27;ouvriront.</p>
<p>Chez Premier Octet, nous suivons de près cette évolution et commençons à accompagner nos premiers clients sur le sujet. Si vous envisagez d&#x27;implémenter l&#x27;Agentic Commerce Protocol et avez besoin d&#x27;accompagnement technique, n&#x27;hésitez pas à nous contacter. On serait ravis d&#x27;échanger sur votre projet !</p>
<p><strong>Ressources utiles :</strong></p>
<ul>
<li><a href="https://developers.openai.com/commerce">Documentation officielle OpenAI</a></li>
<li><a href="https://www.agenticcommerce.dev/">Documentation de l&#x27;Agentic Commerce Protocol</a></li>
<li><a href="https://docs.stripe.com/agentic-commerce">Documentation du Stripe</a></li>
</ul>
<p>👋</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/ecommerce-conversationnel-avec-openai-et-stripe-guide-complet-de-l-agentic-commerce-protocol/illu.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Activity, le nouveau composant React]]></title>
            <link>https://www.premieroctet.com/blog/activity-nouveau-composant-react</link>
            <guid>https://www.premieroctet.com/blog/activity-nouveau-composant-react</guid>
            <pubDate>Tue, 30 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Fraichement sorti sur le canal canary de React, le composant Activity est une nouveauté ayant pour but de faciliter...]]></description>
            <content:encoded><![CDATA[<p>Fraichement sorti sur le canal canary de React, le composant Activity est une nouveauté ayant pour but de faciliter la gestion d&#x27;affichage conditionnel de composants, permettant de conserver leur état tout en les cachant visuellement. Découvrons ensemble son utilisation.</p>
<h2>Installation</h2>
<p>Créons un simple projet React avec Vite :</p>
<pre><code class="language-sh">bun create vite@latest
</code></pre>
<p>Nous devrons modifier le <code>package.json</code> afin de faire pointer les versions de React et React DOM vers la version canary.</p>
<h2>Cas d&#x27;utilisation</h2>
<p>Nous allons réaliser un cas d&#x27;utilisation qui peut s&#x27;avérer fréquent : la gestion d&#x27;un formulaire en plusieurs étapes.</p>
<h3>Utilisation basique</h3>
<p>Actuellement, l&#x27;utilisation la plus classique serait la suivante :</p>
<div><div><div value="App.tsx">App.tsx</div><div value="step1.tsx">step1.tsx</div><div value="step2.tsx">step2.tsx</div></div><div value="App.tsx"><pre><code class="language-tsx">function App() {
  const [step, setStep] = useState(1);

return (

&lt;div
  style={{
    margin: &#x27;1rem&#x27;,
    display: &#x27;flex&#x27;,
    alignItems: &#x27;center&#x27;,
    flex: 1,
    flexDirection: &#x27;column&#x27;,
    gap: &#x27;1rem&#x27;,
  }}
&gt;
  &lt;div style={{ display: &#x27;flex&#x27;, gap: &#x27;1rem&#x27; }}&gt;
    &lt;button type=&quot;button&quot; onClick={() =&gt; setStep(1)}&gt;
      Step 1
    &lt;/button&gt;
    &lt;button type=&quot;button&quot; onClick={() =&gt; setStep(2)}&gt;
      Step 2
    &lt;/button&gt;
  &lt;/div&gt;
  {step === 1 &amp;&amp; &lt;Step1 /&gt;}
  {step === 2 &amp;&amp; &lt;Step2 /&gt;}
&lt;/div&gt;
); }

</code></pre></div><div value="step1.tsx"><pre><code class="language-tsx">const Step1 = () =&gt; {
  const [name, setName] = useState(&quot;&quot;);

  return (
    &lt;input
      value={name}
      onChange={(e) =&gt; setName(e.target.value)}
      name=&quot;name&quot;
      placeholder=&quot;Name&quot;
    /&gt;
  );
};
</code></pre></div><div value="step2.tsx"><pre><code class="language-tsx">const Step2 = () =&gt; {
  const [address, setAddress] = useState(&quot;&quot;);

return (

&lt;input
  value={address}
  onChange={(e) =&gt; setAddress(e.target.value)}
  name=&quot;address&quot;
  placeholder=&quot;Address&quot;
/&gt;
); };

</code></pre></div></div>
<p>En l&#x27;état, ce bout de code est fonctionnel mais présente un inconvénient en terme d&#x27;UX : lorsque l&#x27;on remplit un champ puis que l&#x27;on change d&#x27;étape, l&#x27;état de ce champ est perdu. C&#x27;est logique : notre composant a été démonté. Pour palier à ça, on pourrait tout à fait modifier le code afin de cacher le composant, de sorte qu&#x27;il soit toujours présent dans l&#x27;arbre React.</p>
<pre><code class="language-tsx:App.tsx">function App() {
  const [step, setStep] = useState(1);

  return (
    &lt;div
      style={{
        margin: &quot;1rem&quot;,
        display: &quot;flex&quot;,
        alignItems: &quot;center&quot;,
        flex: 1,
        flexDirection: &quot;column&quot;,
        gap: &quot;1rem&quot;,
      }}
    &gt;
      &lt;div style={{ display: &quot;flex&quot;, gap: &quot;1rem&quot; }}&gt;
        &lt;button type=&quot;button&quot; onClick={() =&gt; setStep(1)}&gt;
          Step 1
        &lt;/button&gt;
        &lt;button type=&quot;button&quot; onClick={() =&gt; setStep(2)}&gt;
          Step 2
        &lt;/button&gt;
      &lt;/div&gt;
      &lt;div style={step === 2 ? { display: &quot;none&quot; } : {}}&gt;
        &lt;Step1 /&gt;
      &lt;/div&gt;
      &lt;div style={step === 1 ? { display: &quot;none&quot; } : {}}&gt;
        &lt;Step2 /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>C&#x27;est fonctionnel mais pas idéal, imaginons que nos composants doivent gérer des événements, ou du contenu interactif comme une vidéo, nous devrions quand même passer une prop afin de gérer leur activation / désactivation. De même, la présence d&#x27;un effect (<code>useEffect</code> ou <code>useLayoutEffect</code>) devrait avoir son déclenchement réglé selon cette prop. Bien que cela soit possible, cela rajouterait de la complexité à notre composant.</p>
<h3>Le composant Activity à la rescousse</h3>
<p>C&#x27;est dans ce cadre qu&#x27;intervient le composant <code>Activity</code>. Son rôle est simple : cacher le noeud du DOM avec un <code>display: none</code>, conserver l&#x27;état du noeud React, et n&#x27;exécuter les effects uniquement quand notre composant est visible. En quelque sorte, c&#x27;est comme si notre composant était partiellement monté et démonté.</p>
<pre><code class="language-tsx:App.tsx">function App() {
  const [step, setStep] = useState(1);

  return (
    &lt;div
      style={{
        margin: &quot;1rem&quot;,
        display: &quot;flex&quot;,
        alignItems: &quot;center&quot;,
        flex: 1,
        flexDirection: &quot;column&quot;,
        gap: &quot;1rem&quot;,
      }}
    &gt;
      &lt;div style={{ display: &quot;flex&quot;, gap: &quot;1rem&quot; }}&gt;
        &lt;button type=&quot;button&quot; onClick={() =&gt; setStep(1)}&gt;
          Step 1
        &lt;/button&gt;
        &lt;button type=&quot;button&quot; onClick={() =&gt; setStep(2)}&gt;
          Step 2
        &lt;/button&gt;
      &lt;/div&gt;
      &lt;Activity mode={step === 1 ? &quot;visible&quot; : &quot;hidden&quot;}&gt;
        &lt;Step1 /&gt;
      &lt;/Activity&gt;
      &lt;Activity mode={step === 2 ? &quot;visible&quot; : &quot;hidden&quot;}&gt;
        &lt;Step2 /&gt;
      &lt;/Activity&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>La saisie de texte est maintenant conservée entre chaque changement d&#x27;étape. En inspectant le DOM, on peut voir qu&#x27;un style <code>display: none</code> est appliqué à notre input caché.</p>
<p>Pour aller plus loin, on peut observer le comportement des effects, à l&#x27;aide de simples <code>console.log</code>.</p>
<pre><code class="language-tsx:step2.tsx">const Step2 = () =&gt; {
  const [address, setAddress] = useState(&quot;&quot;);

  useEffect(() =&gt; {
    console.log(&quot;Step2 mounted&quot;);

    return () =&gt; {
      console.log(&quot;Step2 unmounted&quot;);
    };
  }, []);

  useLayoutEffect(() =&gt; {
    console.log(&quot;Step2 layout mounted&quot;);

    return () =&gt; {
      console.log(&quot;Step2 layout unmounted&quot;);
    };
  }, []);

  return (
    &lt;input
      value={address}
      onChange={(e) =&gt; setAddress(e.target.value)}
      name=&quot;address&quot;
      placeholder=&quot;Address&quot;
    /&gt;
  );
};
</code></pre>
<p>En rafraichissant la page, on peut voir qu&#x27;aucun des logs ne s&#x27;affiche. Pourtant, notre input est bien dans le DOM. En cliquant sur notre étape 2, nos effects sont bien déclenchés, et les fonctions de cleanup sont exécutées si l&#x27;on revient à l&#x27;étape 1.</p>
<h3>Pré-rendu et données distantes</h3>
<p>Afin de récupérer des données distantes, un cas d&#x27;utilisation courant serait d&#x27;effectuer une requête dans un <code>useEffect</code>. Or on a vu précédemment que les effects ne sont pas exécutés dans le cadre de l&#x27;utilisation d&#x27;<code>Activity</code>. Pour palier à cela, nous allons utiliser le hook <code>use</code>.</p>
<pre><code class="language-tsx:step2.tsx">const fetchAddress = new Promise&lt;string&gt;((resolve) =&gt; {
  resolve(&quot;Some street name&quot;);
});

const Step2 = () =&gt; {
  const addressData = use(fetchAddress);
  const [address, setAddress] = useState(addressData);

  return (
    &lt;input
      value={address}
      onChange={(e) =&gt; setAddress(e.target.value)}
      name=&quot;address&quot;
      placeholder=&quot;Address&quot;
    /&gt;
  );
};
</code></pre>
<p>Maintenant, au chargement de la page, notre promesse <code>fetchAddress</code> est exécutée dès lors que <code>Step2</code> est instancié, et on retrouve la valeur résolue affichée dans notre input.</p>

<h2>Conclusion</h2>
<p>Ce tout nouveau composant <code>Activity</code> va simplifier certains cas complexes que l&#x27;on pouvait rencontrer jusqu&#x27;à présent dans nos applications. Son utilisation est très intuitive, mais nécessitera toutefois un peu de refactorisation dans les projets existants, notamment par rapport à la modification du comportement des effects.</p>
<p>Une idée de projet React, besoin d&#x27;accompagnement ? N&#x27;hésitez pas à <a href="https://www.premieroctet.com/contact">nous contacter</a> !</p>
<p>Ressources:</p>
<ul>
<li><a href="https://react.dev/reference/react/Activity">Documentation officielle Activity</a></li>
</ul>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/activity-nouveau-composant-react/illu.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Comment utiliser Rive app pour animer vos interfaces Web et mobiles]]></title>
            <link>https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles</link>
            <guid>https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles</guid>
            <pubDate>Tue, 23 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Découvrez comment créer des animations interactives avec Rive app et les intégrer dans vos applications React pour des interfaces dynamiques et engageantes.]]></description>
            <content:encoded><![CDATA[<p>Si vous avez déjà joué avec <a href="https://www.premieroctet.com/estimateur-projet-web-mobile">notre estimateur web et mobile</a>, vous avez peut-être remarqué que l’illustration à l’écran évolue en fonction de vos choix. Une option cochée, et hop ! un petit élément s’affiche avec une animation fluide. Ce petit tour de magie, on le doit à Rive app.</p>
<div style="margin-top:20px"><video class="b-lazy b-loaded" muted="" controls="" title="Timeline animation"><source src="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/demo.mp4" type="video/mp4"/></video></div>
<p>Dans cet article, je vous propose de voir comment mettre en place ces animations — et surtout, pourquoi on a choisi cet outil plutôt qu’un autre.</p>
<h2>Qu&#x27;est-ce que Rive App ?</h2>
<p>Rive est un outil de création d’animations vectorielles interactives. Là où d’autres génèrent des vidéos ou des gifs, Rive produit des fichiers <code>.riv</code>, très légers, qui peuvent réagir aux actions de l’utilisateur.</p>
<h3>Les avantages de Rive App</h3>
<p>Rive App a plusieurs avantages :</p>
<ul>
<li><strong>Animations interactives</strong> : vos animations peuvent changer d&#x27;état selon les interactions utilisateur (clics, survols, données dynamiques)</li>
<li><strong>Fichiers ultra-légers</strong> : les animations Rive sont des fichiers vectoriels optimisés, bien plus petits que des vidéos équivalentes ou des gifs</li>
<li><strong>Multi-plateforme</strong> : une seule animation fonctionne sur Web ou mobile grâce aux différents runtimes (et bien entendu React / React Native)</li>
<li><strong>État dynamique</strong> : possibilité de créer des state machines complexes avec conditions et transitions et faire du data-binding</li>
</ul>
<h3>Pourquoi choisir Rive plutôt que Lottie ou des animations CSS ?</h3>
<p>Rive se distingue par sa capacité à créer des animations <strong>véritablement interactives</strong>. Là où Lottie se limite à des animations linéaires et le CSS pour des transitions simples, Rive permet de créer des expériences où l&#x27;animation évolue en fonction du contexte applicatif.</p>
<p>Pour les anciens, Rive se rapproche de Flash, qui était un outil de création d’animations vectorielles interactives.</p>
<h2>Deux modes, une interface</h2>
<p>L’interface de Rive est découpée en deux parties : Design et Animation. Une pour préparer les assets, l’autre pour leur donner vie !</p>
<p>Je vous propose de détailler l&#x27;animation d&#x27;un élément de notre estimateur web et mobile.</p>
<h3>Étape 1 : préparer le terrain</h3>
<p>On commence par importer notre illustration (préparée dans Figma), et on isole chaque élément sur un calque séparé. Cette étape est un peu fastidieuse mais cruciale. Un calque = un élément animable. Sans ça, on se prend vite les pieds dans le tapis.</p>
<p>Voici mon illustration préparée avec chaque élément isolé, visible dans le panel de gauche <code>Hierarchy</code> :</p>
<p><img src="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/rive-app-ui-design.png" alt="Interface de design Rive app avec panel Hierarchy et découpage des éléments"/></p>
<h3>Étape 2 : mode animation</h3>
<p>Une fois l’illustration prête, on passe en mode animation grâce au switch en haut à droite de l&#x27;interface.</p>
<p>C’est là que les timelines, state machines et interpolations entrent en scène. Les animations avec Rive app se basent sur des timelines d&#x27;interpolation de propriétés (position, opacité, coordonnées, etc.) assez semblables aux interfaces de motion design comme After Effects.</p>
<p>Prenons l’exemple d’un élément que l’on veut faire apparaître dynamiquement : ici, une paire de guillemets en haut à droite de l’illustration.</p>
<p>On commence par créer un input de type boolean dans la state machine : <code>has_blog</code>. Il est à <code>false</code> par défaut, et c’est notre code React que nous écrirons plus tard qui viendra le passer à <code>true</code> :</p>
<p><img src="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/rive-app-panel-animation.png" alt="Interface d&#x27;animation Rive app avec panel Inputs et création d&#x27;un input de type boolean"/></p>
<p>On peut maintenant passer à l&#x27;animation !</p>
<p>On crée une nouvelle timeline dédiée à cette animation sur laquelle on va interpoler deux propriétés : position et opacité.</p>
<p>Ce n&#x27;est pas du Miyazaki mais suffisant pour donner un effet vivant à notre élément :</p>
<div style="margin-top:20px"><video class="b-lazy b-loaded" autoplay="" loop="" muted="" controls="" title="Timeline animation"><source src="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/rive-app-timeline-animation.mp4" type="video/mp4"/></video></div>
<h3>Mettre en place la state machine</h3>
<p>Une fois l&#x27;animation créée, on peut maintenant créer notre state machine spécifique à cette animation.</p>
<p>Les states machines Rive permettent de gérer l&#x27;état de l&#x27;animation en fonction de conditions. Ici, on va créer deux états : l&#x27;animation qui se joue et l&#x27;animation qui disparaît (si l&#x27;utilisateur décoche l&#x27;option).</p>
<p><img src="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/rive-app-state-machine-step-1.png" alt="State machine étape 1"/></p>
<p>Pour l&#x27;animation de disparition, j&#x27;ai utilisé une astuce qui permet de jouer l&#x27;animation dans le sens inverse (notez le speed -1 dans le panel en bas à droite) :</p>
<p><img src="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/rive.png" alt="State machine étape 2"/></p>
<p>Il ne reste plus qu&#x27;à ajouter mes conditions qui permettent de passer d&#x27;un état à l&#x27;autre simplement basé sur ma condition <code>has_blog</code> :</p>
<p><img src="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/rive-app-state-machine-step-2.png" alt="State machine étape 2"/></p>
<p>L&#x27;animation est maintenant prête ! Chaque animation de notre estimateur repose sur ce même pattern. C’est un peu répétitif, mais le résultat final vaut l’investissement.</p>
<h2>Intégrer votre animation Rive dans React</h2>
<p>On peut maintenant intégrer notre animation dans notre code React. Pour cela on peut utiliser <a href="https://rive.app/docs/runtimes/react/react">le runtime React</a> officiel de Rive.</p>
<p>Avant toute chose, il vous faut exporter votre animation au format <code>.riv</code> afin de le passer au composant <code>&lt;Rive/&gt;</code> :</p>
<pre><code class="language-jsx">

export const App = () =&gt; {
  // Configuration de l&#x27;animation Rive
  const { rive, RiveComponent } = useRive({
    src: &#x27;/animations/estimator.riv&#x27;, // Chemin vers votre fichier .riv
    artboard: &#x27;web&#x27;, // Nom de l&#x27;artboard à utiliser
    stateMachines: &#x27;state_machine&#x27;, // Nom de la state machine
    autoplay: true, // Démarre automatiquement l&#x27;animation
  })

  // Récupération de l&#x27;input de la state machine
  const hasBlog = useStateMachineInput(rive, &#x27;state_machine&#x27;, &#x27;has_blog&#x27;)

  return (
    &lt;&gt;
      &lt;RiveComponent /&gt;
      &lt;button onClick={() =&gt; (hasBlog.value = true)}&gt;Afficher l&#x27;animation Blog&lt;/button&gt;
    &lt;/&gt;
  )
}
</code></pre>
<p>Voici une petite explication du code :</p>
<ul>
<li>
<p><strong><code>useRive</code></strong> : ce hook configure et initialise votre animation Rive. Il retourne l&#x27;instance <code>rive</code> et le composant <code>RiveComponent</code> à afficher.</p>
</li>
<li>
<p><strong><code>useStateMachineInput</code></strong> : ce hook permet de récupérer et manipuler les inputs de votre state machine. Ici, on récupère l&#x27;input <code>has_blog</code> qu&#x27;on avait configuré dans Rive app.</p>
</li>
<li>
<p><strong>Interaction</strong> : en cliquant sur le bouton, on modifie la valeur de <code>hasBlog.value</code> à <code>true</code>, ce qui déclenche l&#x27;animation dans Rive.</p>
</li>
</ul>
<p>Votre animation est ainsi prête à être utilisée dans votre application !</p>
<h2>En résumé</h2>
<p>Rive permettet d&#x27;apporter un supplément d&#x27;âme à vos projets, sans sacrifier la performance. Son modèle basé sur des state machines permet de créer des comportements fins, réactifs et parfaitement intégrés à nos apps.</p>
<p>L&#x27;intégration avec React est fluide, sans friction. Et surtout, elle garde les responsabilités bien séparées : Rive pour l’animation, React pour le déclenchement.</p>
<p>C’est un outil qu’on adopte rapidement une fois qu’on a mis les mains dedans. Mon conseil : commencez simple et explorez petit à petit.</p>
<p>Voici quelques use cases concrets :</p>
<ul>
<li>Animer des éléments d&#x27;une interface comme notre estimateur Web et mobile</li>
<li>Intégrer des animations dans une application React Native (écran de onboarding, etc.)</li>
<li>Créer des apps à part entière (jeux, etc.)</li>
<li>Créer des layouts interactifs pour des streaming videos (via OBS, etc.)</li>
</ul>
<p>Si vous avez des projets qui nécessitent des animations interactives, n&#x27;hésitez pas à <a href="https://www.premieroctet.com/contact">nous contacter</a>, nous serons ravis de vous accompagner dans la mise en place de <a href="https://rive.app/">Rive app</a>.</p>
<p>Enfin, vous pouvez jouer avec <a href="https://www.premieroctet.com/estimateur-projet-web-mobile">nos estimateurs Web et mobile ici</a>.</p>
<p>Ressources utiles :</p>
<ul>
<li><a href="https://rive.app/docs/getting-started/introduction">Documentation officielle Rive app</a></li>
<li><a href="https://rive.app/docs/runtimes/react/react">Runtime React</a></li>
</ul>
<p>👋</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/comment-utiliser-rive-app-pour-animer-vos-interfaces-web-et-mobiles/illu.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Stylisez votre application React Native avec Unistyles]]></title>
            <link>https://www.premieroctet.com/blog/stylisez-votre-application-react-native-avec-unistyles</link>
            <guid>https://www.premieroctet.com/blog/stylisez-votre-application-react-native-avec-unistyles</guid>
            <pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Alors que la nouvelle architecture de React Native est maintenant dans un état stable, la quête à la performance elle n'est jamais terminée...]]></description>
            <content:encoded><![CDATA[<p>Alors que la <a href="https://reactnative.dev/architecture/landing-page">nouvelle architecture de React Native</a> est maintenant dans un état stable, la quête à la performance elle n&#x27;est jamais terminée. Dans ce cadre, utiliser LA librairie de style pour notre application est n&#x27;est pas toujours évident. En effet celle-ci doit allier performance, flexibilité et simplicité d&#x27;utilisation. Nous avons exploré dans un précédent article <a href="https://www.premieroctet.com/blog/decouverte-tamagui">Tamagui</a> qui offre une certaine simplicité d&#x27;utilisation, mais qui est assez fastidieux à configurer lorsque l&#x27;on souhaite obtenir un rendu plus personnalisé.</p>
<p>Plus récemment, <a href="https://www.nativewind.dev">NativeWind</a> a prit aussi beaucoup en popularité, profitant d&#x27;une certaine standardisation de l&#x27;utilisation de <a href="https://tailwindcss.com/">Tailwind</a> côté web. Simple à configurer, avec une API quasi identique à celle de Tailwind, il est assez simple pour un développeur Web de se lancer dans la création d&#x27;une application React Native avec NativeWind. Néanmoins, son fonctionnement interne n&#x27;est pas toujours évident à comprendre et demande parfois d&#x27;être assez verbeux quand il s&#x27;agit de créer des composants très génériques (via l&#x27;utilisation de <a href="https://www.nativewind.dev/docs/api/remap-props"><code>remapProps</code></a> ou <a href="https://www.nativewind.dev/docs/api/css-interop"><code>cssInterop</code></a> par exemple).</p>
<p>C&#x27;est dans ce contexte de course à la performance et à la meilleure DX qu&#x27;Unistyles a sorti sa version 3, dont nous allons dans cet article explorer les fonctionnalités et les avantages de cette librairie.</p>
<h2>Comment ça marche ?</h2>
<p>Unistyles a pour vocation de fournir une API très proche de l&#x27;API <a href="https://reactnative.dev/docs/stylesheet">StyleSheet</a> de React Native, tout en y ajoutant des fonctionnalités supplémentaires comme la gestion de thème, le support des breakpoints, l&#x27;ajout de variants. Pour aller plus loin, Unistyles ajoute un aspect performance en gérant les styles directement au sein de la partie C++ de l&#x27;application. Concrètement, cela permet d&#x27;associer un style à un noeud natif de notre arbre de composants, et de faire en sorte que ce noeud réagisse aux changements de style qui peuvent se produire (changement de thème, propriété dépendante du viewport, etc.), le tout sans provoquer de re-rendu de notre arbre de composants côté JavaScript. Toutes ces optimisations sont orchestrées par un <a href="https://www.unistyl.es/v3/other/babel-plugin">plugin Babel</a>.</p>
<h2>Utilisation</h2>
<p>Explorons Unistyles en créant un projet Expo. Nous utiliserons le template &quot;Blank&quot; :</p>
<pre><code class="language-sh">bun create expo-app demo-unistyles -t
cd demo-unistyles
npx expo install react-native-unistyles react-native-nitro-modules react-native-reanimated react-native-edge-to-edge
</code></pre>
<p>Ajoutons le plugin Babel :</p>
<pre><code class="language-sh">npx expo customize babel.config.js
</code></pre>
<pre><code class="language-js:babel.config.js">module.exports = function (api) {
  api.cache(true);
  return {
    presets: [&quot;babel-preset-expo&quot;],
    plugins: [
      [
        &quot;react-native-unistyles/plugin&quot;,
        {
          root: &quot;src&quot;, // Dossier racine contenant les composants stylisés
        },
      ],
    ],
  };
};
</code></pre>
<p>Créons maintenant un style très simple dans le composant <code>App</code> :</p>
<pre><code class="language-js:src/app.tsx">


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: &#x27;center&#x27;,
    alignItems: &#x27;center&#x27;,
    backgroundColor: &#x27;#FFFFFF&#x27;,
  },
});

const App = () =&gt; {
  return &lt;View style={styles.container} /&gt;
}

export default App;
</code></pre>
<p>On constate finalement que, en l&#x27;état, l&#x27;API de Unistyles offre une parité 1:1 avec l&#x27;API <code>StyleSheet</code> de React Native, rendant ainsi une migration vers Unistyles très simple.</p>
<h3>Theming</h3>
<p>Nous allons maintenant utiliser ce qui est un atout d&#x27;Unistyles par rapport à <code>StyleSheet</code> : la possibilité de définir des thèmes.</p>
<p>Créons un fichier <code>src/themes.ts</code> :</p>
<pre><code class="language-ts:src/themes.ts">

// Quelques utilitaires
const spacing = (size: number) =&gt; size * 4;
const radius = (size: number) =&gt; size * 4;

const utils = {
  spacing,
  radius,
};

export const light = {
  colors: {
    primary: &quot;#007AFF&quot;,
    red: &quot;#ed3e3e&quot;,
    background: &quot;#ffffff&quot;,
  },
  utils,
};

export const dark = {
  colors: {
    primary: &quot;#4da6ff&quot;,
    red: &quot;#fa645c&quot;,
    background: &quot;#3E3E3E&quot;,
  },
  utils,
};

StyleSheet.configure({
  themes: {
    light,
    dark,
  },
  settings: {
    /**
     * L&#x27;utilisation de thèmes nommés &quot;dark&quot; et &quot;light&quot; permet de les utiliser en fonction
     * du thème utilisé par le système d&#x27;exploitation. Pour cela, il faut s&#x27;assurer que
     * la propriété `userInterfaceStyle` de la configuration Expo soit définie en &quot;automatic&quot;.
     */
    adaptiveThemes: true,
  },
});

type AppThemes = {
  light: typeof light;
  dark: typeof dark;
};

declare module &quot;react-native-unistyles&quot; {
  export interface UnistylesThemes extends AppThemes {}
}
</code></pre>
<p>Nous avons ici défini 2 thèmes de couleur différent, chacun destiné à s&#x27;adapter selon le thème du système d&#x27;exploitation. Il est évidemment possible de définir d&#x27;autres thèmes avec des noms différents.
Pour que ces thèmes soient bien pris en compte avant le premier rendu, il est impératif d&#x27;importer ce fichier dans notre <code>index.ts</code> :</p>
<pre><code class="language-js:index.ts">



registerRootComponent(App);
</code></pre>
<p>Faisons maintenant usage de ces thèmes. Pour ce faire, il va falloir transformer légèrement notre fonction <code>StyleSheet.create</code>. Celle-ci prendra maintenant en paramètre une fonction recevant 2 arguments : le thème actuel et des variables de styling liées à l&#x27;appareil (nous aborderons cela plus tard).</p>
<pre><code class="language-tsx:src/app.tsx">


const styles = StyleSheet.create((theme, rt) =&gt; ({
  container: {
    flex: 1,
    justifyContent: &#x27;center&#x27;,
    alignItems: &#x27;center&#x27;,
    backgroundColor: theme.colors.background,
    padding: theme.utils.spacing(4)
  },
}));

const App = () =&gt; {
  return &lt;View style={styles.container} /&gt;
}

export default App;
</code></pre>
<p>Si nous alternons entre le light et dark mode sur notre appareil, nous verrons la couleur de fond changer, le tout sans que notre arbre de composants React ne soit re-rendu !</p>
<h3>Responsive</h3>
<p>Il est possible de définir des styles selon la taille ou l&#x27;orientation de l&#x27;appareil. Par défaut, Unistyles fournit la possibilité de fournir des styles selon des breakpoints liés à l&#x27;orientation, mais il est tout à fait possible de définir <a href="https://www.unistyl.es/v3/references/breakpoints">nos propres breakpoints</a> liés à la taille de l&#x27;écran. Essayons par exemple de changer la couleur de fond en fonction de l&#x27;orientation :</p>
<pre><code class="language-tsx:src/app.tsx">


const styles = StyleSheet.create((theme, rt) =&gt; ({
  container: {
    flex: 1,
    justifyContent: &#x27;center&#x27;,
    alignItems: &#x27;center&#x27;,
    backgroundColor: {
      portrait: theme.colors.background,
      landscape: theme.colors.primary,
    },
    padding: theme.utils.spacing(4)
  },
}));

const App = () =&gt; {
  return &lt;View style={styles.container} /&gt;
}

export default App;
</code></pre>
<p>Maintenant, lorsque nous changeons l&#x27;orientation de notre appareil, la couleur de fond change, toujours sans provoquer de re-rendu !</p>
<h3>Runtime Unistyles</h3>
<p>Évoquons maintenant le sujet du runtime que l&#x27;on a vu précédemment. Unistyles expose des variables de runtime, qui sont des valeurs purement liées à l&#x27;appareil et qui sont accessibles à tout moment. Il existe 2 types de runtime :</p>
<ul>
<li><a href="https://www.unistyl.es/v3/references/unistyles-runtime">le runtime global</a>, exposant des variables accessibles à tout moment, mais aussi des fonctions permettant d&#x27;altérer certains aspects comme le thème utilisé, le contenu du thème, la couleur de fond de notre vue native, etc.</li>
<li><a href="https://www.unistyl.es/v3/references/mini-runtime">le runtime de style</a>, appelé mini runtime, qui lui aussi expose des variables similaires à celles du runtime global, mais qui ont pour but d&#x27;être utilisées dans les styles. Ainsi, nos styles seront réactifs aux changements de valeurs de ces variables.</li>
</ul>
<p>Faisons usage du mini runtime pour ajouter un espacement en haut de notre vue en fonction de la safe area :</p>
<pre><code class="language-tsx:src/app.tsx">


const styles = StyleSheet.create((theme, rt) =&gt; ({
  container: {
    flex: 1,
    justifyContent: &#x27;center&#x27;,
    alignItems: &#x27;center&#x27;,
    backgroundColor: {
      portrait: theme.colors.background,
      landscape: theme.colors.primary,
    },
    padding: theme.utils.spacing(4),
    paddingTop: rt.insets.top,
  },
}));

const App = () =&gt; {
  return &lt;View style={styles.container} /&gt;
}

export default App;
</code></pre>
<h3>Variants</h3>
<p>À la manière de Tamagui et de bien d&#x27;autres librairies de style, Unistyles permet d&#x27;ajouter des variants pour un style précis. Un cas d&#x27;utilisation parfait pour cela est la création d&#x27;un bouton. Pour un aspect plus joli, nous allons même nous permettre d&#x27;animer la couleur du bouton quand un changement de couleur est détecté.</p>
<pre><code class="language-tsx:src/button.tsx">

  Pressable,
  type PressableProps,
  StyleProp,
  Text,
  ViewStyle,
} from &quot;react-native&quot;;

  useAnimatedStyle,
  withTiming,
} from &quot;react-native-reanimated&quot;;



type Props = UnistylesVariants&lt;typeof styles&gt; &amp;
  Omit&lt;PressableProps, &quot;style&quot;&gt; &amp; {
    label: string;
    style?: StyleProp&lt;ViewStyle&gt;;
  };

const Button = ({
  type = &quot;primary&quot;,
  style,
  label,
  children,
  ...props
}: Props) =&gt; {
  // Activation du variant pour le composant Button
  styles.useVariants({ type });
  // Récupération de la couleur de fond sous forme de shared value
  const color = useAnimatedVariantColor(styles.container, &quot;backgroundColor&quot;);
  // Animation de la couleur de fond
  const animatedStyle = useAnimatedStyle(() =&gt; {
    return {
      backgroundColor: withTiming(color.value, { duration: 500 }),
    };
  });

  return (
    &lt;Pressable {...props}&gt;
      &lt;Animated.View style={[styles.container, animatedStyle, style]}&gt;
        &lt;ButtonText type={type}&gt;{label}&lt;/ButtonText&gt;
      &lt;/Animated.View&gt;
    &lt;/Pressable&gt;
  );
};

const ButtonText = ({
  children,
  type,
}: PropsWithChildren&lt;UnistylesVariants&lt;typeof styles&gt;&gt;) =&gt; {
  // Activation du variant pour le composant ButtonText
  styles.useVariants({ type });

  return &lt;Text style={styles.text}&gt;{children}&lt;/Text&gt;;
};

export default Button;

const styles = StyleSheet.create((theme, rt) =&gt; ({
  container: {
    borderRadius: theme.utils.radius(2),
    flexDirection: &quot;row&quot;,
    paddingHorizontal: theme.utils.spacing(4),
    paddingVertical: theme.utils.spacing(3),

    variants: {
      type: {
        primary: {
          backgroundColor: theme.colors.primary,
        },
        destructive: {
          backgroundColor: theme.colors.red,
        },
      },
    },
  },
  text: {
    fontSize: 14,
    variants: {
      type: {
        primary: {
          color: &quot;white&quot;,
        },
        destructive: {
          color: &quot;white&quot;,
        },
      },
    },
  },
}));
</code></pre>
<p>Nous avons défini ici 2 variants pour nos boutons. Ces variants peuvent être passé en props de notre composant <code>Button</code>.</p>
<pre><code class="language-tsx:src/app.tsx">



const styles = StyleSheet.create((theme, rt) =&gt; ({
  container: {
    flex: 1,
    justifyContent: &#x27;center&#x27;,
    alignItems: &#x27;center&#x27;,
    backgroundColor: {
      portrait: theme.colors.background,
      landscape: theme.colors.primary,
    },
    padding: theme.utils.spacing(4),
    paddingTop: rt.insets.top,
    gap: theme.utils.spacing(4),
  },
}));

const App = () =&gt; {
  return (
    &lt;View style={styles.container}&gt;
      &lt;Button type=&quot;primary&quot; label=&quot;Primary Button&quot; /&gt;
      &lt;Button type=&quot;destructive&quot; label=&quot;Destructive Button&quot; /&gt;
    &lt;/View&gt;
  )
}

export default App;
</code></pre>
<p>Profitons-en pour faire usage du runtime global. Nous allons faire en sorte que nos boutons changent le thème de couleur utilisé.</p>
<pre><code class="language-tsx:src/app.tsx">



const styles = StyleSheet.create((theme, rt) =&gt; ({
  container: {
    flex: 1,
    justifyContent: &#x27;center&#x27;,
    alignItems: &#x27;center&#x27;,
    backgroundColor: {
      portrait: theme.colors.background,
      landscape: theme.colors.primary,
    },
    padding: theme.utils.spacing(4),
    paddingTop: rt.insets.top,
    gap: theme.utils.spacing(4),
  },
}));

const App = () =&gt; {
  return (
    &lt;View style={styles.container}&gt;
      &lt;Button type=&quot;primary&quot; label=&quot;Light mode&quot; onPress={() =&gt; UnistylesRuntime.setTheme(&#x27;light&#x27;)} /&gt;
      &lt;Button type=&quot;destructive&quot; label=&quot;Dark mode&quot; onPress={() =&gt; UnistylesRuntime.setTheme(&#x27;dark&#x27;)} /&gt;
    &lt;/View&gt;
  )
}

export default App;
</code></pre>
<p>Maintenant au clic sur les boutons, leur couleur de fond change, encore une fois sans provoquer de re-rendu.</p>
<p>En plus des variants, Unistyles offre la possibilité d&#x27;ajouter des styles selon les valeurs de plusieurs variants : <a href="https://www.unistyl.es/v3/references/compound-variants">les variants composés</a>.</p>
<h2>Conclusion</h2>
<p>Unistyles est une alternative très intéressante aux solutions existantes, de part son aspect performance, sa simplicité d&#x27;utilisation et ses similitudes avec l&#x27;API StyleSheet. À noter qu&#x27;un <a href="https://www.unistyl.es/v3/references/web-styles">support Web</a> est lui aussi disponible. Que choisir entre Unistyles et Tamagui ? Le choix est discutable mais de part sa facilité de configuration, Unistyles me semble être un bon choix. Si toutefois vous êtes un grand fan de Tailwind, le créateur d&#x27;Unistyles a créé <a href="https://uniwind.dev/">Uniwind</a> (pas encore sorti à l&#x27;heure de l&#x27;écriture de cet article), ayant pour but de combiner les performances de Unistyles avec les avantages de Tailwind.</p>
<p>Des idées de projet en React Native, une envie de migrer votre application vers Unistyles ? N&#x27;hésitez pas à <a href="https://www.premieroctet.com/contact">nous contacter</a> !</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/stylisez-votre-application-react-native-avec-unistyles/illu.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Intégration d'un MCP dans une application React]]></title>
            <link>https://www.premieroctet.com/blog/integration-mcp-dans-une-app-react</link>
            <guid>https://www.premieroctet.com/blog/integration-mcp-dans-une-app-react</guid>
            <pubDate>Tue, 26 Aug 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[La démocratisation de l'IA via les LLM a rendu l'accès à tout type d'information extrêmement facile et rapide...]]></description>
            <content:encoded><![CDATA[<p>La démocratisation de l&#x27;IA via les LLM a rendu l&#x27;accès à tout type d&#x27;information extrêmement facile et rapide, sans nécessiter de connaissances techniques avancées. Cependant dans certains cas, les modèles n&#x27;ont tout simplement pas le contexte nécessaire pour répondre à des demandes spécifiques. Par exemple, répondre à une question liée à de la facturation liée à un compte Stripe est impossible sans devoir développer un assistant spécifique, via l&#x27;intégration de <a href="https://ai-sdk.dev/docs/foundations/tools#tools">tools</a>. Pour palier à cela, <a href="https://www.anthropic.com/">Anthropic</a>, à l&#x27;origine de <a href="https://www.anthropic.com/claude">Claude</a>,
a mis en place un protocole de communication permettant de fournir des informations contextuelles aux modèles sans nécessiter de développements spécifiques à un contexte particulier.</p>
<h2>Model Context Protocol (MCP)</h2>
<p><a href="https://modelcontextprotocol.io/">Model Context Protocol (MCP)</a> est un standard de communication permettant de fournir un contexte à un LLM. En tant que développeur, vous êtes probablement familier avec MCP si vous utilisez un IDE supportant les agents IA (Cursor, Zed, etc.). Un serveur implémentant MCP peut fournir plusieurs types de données :</p>
<ul>
<li><a href="https://modelcontextprotocol.io/docs/concepts/tools">un ensemble de tools à exécuter</a></li>
<li><a href="https://modelcontextprotocol.io/docs/concepts/prompts">des prompts prédéfinis</a></li>
<li><a href="https://modelcontextprotocol.io/docs/concepts/resources">des données précises venant d&#x27;une source externe (BDD, API, etc.)</a></li>
</ul>
<p>En réalité, l&#x27;usage le plus courant est d&#x27;utiliser les tools exposés par le serveur MCP.</p>
<p>Ce serveur peut ensuite être exposé soit via une URL, soit un exécutable.</p>
<h2>Implémentation</h2>
<p>Nous allons créer une petite application Next.js utilisant un MCP Postgres. Le but est de pouvoir répondre à une requête dans un langage naturel puis d&#x27;afficher les informations issues de la base de données.</p>
<pre><code class="language-sh">bun create next-app
</code></pre>
<p>Installons les dépendances nécessaires :</p>
<pre><code class="language-sh">bun add ai @ai-sdk/anthropic @modelcontextprotocol/sdk pg react-markdown
</code></pre>
<h3>Implémentation du serveur MCP</h3>
<p>Pour le serveur MCP, nous allons réutiliser le serveur <a href="https://github.com/zed-industries/postgres-context-server/blob/main/index.mjs">Zed Postgres</a>, que nous allons légèrement modifier.</p>
<pre><code class="language-ts:mcp-server.ts">


  CallToolRequestSchema,
  ListToolsRequestSchema,
} from &quot;@modelcontextprotocol/sdk/types.js&quot;;


const { server } = new McpServer(
  {
    name: &quot;postgres&quot;,
    version: &quot;1.0.0&quot;,
  },
  {
    capabilities: {
      tools: {},
    },
  },
);

const stdioTransport = new StdioServerTransport();
</code></pre>
<p>On initialise ici un serveur MCP qui sera en capacité d&#x27;exposer uniquement les données liées aux tools. D&#x27;autre types de capabilities sont disponibles: <code>resources</code>, <code>prompts</code>, <code>completions</code>.
Nous utiliserons notre MCP sous forme d&#x27;exécutable. Par conséquent, la communication s&#x27;effectuera via la lecture et l&#x27;écriture sur <code>stdin</code> et <code>stdout</code>, d&#x27;où l&#x27;utilisation du transport <code>StdioServerTransport</code>. Si l&#x27;on souhaite utiliser le transport HTTP, nous devront utiliser <code>StreamableHTTPClientTransport</code>, et exposer notre serveur MCP à un port, en utilisant <code>express</code> par exemple.</p>
<pre><code class="language-ts:mcp-server.ts">const dbUrl = Bun.env.PG_URL;
const resourceBaseUrl = new URL(dbUrl!);
resourceBaseUrl.protocol = &quot;postgres:&quot;;
resourceBaseUrl.password = &quot;&quot;;

const pool = new pg.Pool({
  connectionString: Bun.env.PG_URL,
});
</code></pre>
<p>On initialise maintenant notre instance Postgres, afin d&#x27;établir une connexion plus tard.</p>
<p>Définissons maintenant l&#x27;accès à nos tools. Dans un premier temps nous devons en exposer la liste :</p>
<pre><code class="language-ts:mcp-server.ts">server.setRequestHandler(ListToolsRequestSchema, async () =&gt; {
  return {
    tools: [
      {
        name: &quot;pg-schema&quot;,
        description: &quot;Returns the schema for a Postgres database.&quot;,
        inputSchema: {
          type: &quot;object&quot;,
          properties: {
            mode: {
              type: &quot;string&quot;,
              enum: [&quot;all&quot;, &quot;specific&quot;],
              description: &quot;Mode of schema retrieval&quot;,
            },
            tableName: {
              type: &quot;string&quot;,
              description:
                &quot;Name of the specific table (required if mode is &#x27;specific&#x27;)&quot;,
            },
          },
          required: [&quot;mode&quot;],
          if: {
            properties: { mode: { const: &quot;specific&quot; } },
          },
          then: {
            required: [&quot;tableName&quot;],
          },
        },
      },
      {
        name: &quot;query&quot;,
        description: &quot;Run a read-only SQL query&quot;,
        inputSchema: {
          type: &quot;object&quot;,
          properties: {
            sql: { type: &quot;string&quot; },
          },
        },
      },
    ],
  };
});
</code></pre>
<p><code>ListToolsRequestSchema</code> est un schéma <a href="https://zod.dev/">Zod</a>. Si le serveur MCP reçoit une requête correspondant à ce schéma, alors il doit exécuter la fonction passée en second argument. Cette fonction doit retourner une liste de tools contenant un nom (servant d&#x27;identifiant théoriquement unique), une description qui sera lue par le modèle ainsi qu&#x27;un <a href="https://json-schema.org/">JSON Schema</a> permettant d&#x27;indiquer au modèle la structure de donnée attendue pour l&#x27;exécution du tool.</p>
<p>Maintenant pour l&#x27;exécution de nos tools :</p>
<pre><code class="language-ts:mcp-server.ts">server.setRequestHandler(CallToolRequestSchema, async (request) =&gt; {
  if (request.params.name === &quot;pg-schema&quot;) {
    const mode = request.params.arguments?.mode;

    const tableName = (() =&gt; {
      switch (mode) {
        case &quot;specific&quot;: {
          const tableName = request.params.arguments?.tableName;

          if (typeof tableName !== &quot;string&quot; || tableName.length === 0) {
            throw new Error(`Invalid tableName: ${tableName}`);
          }

          return tableName;
        }
        case &quot;all&quot;: {
          return ALL_TABLES;
        }
        default:
          throw new Error(`Invalid mode: ${mode}`);
      }
    })();

    const client = await pool.connect();

    try {
      const sql = await getSchema(client, tableName);

      return {
        content: [{ type: &quot;text&quot;, text: sql }],
      };
    } finally {
      client.release();
    }
  }

  if (request.params.name === &quot;query&quot;) {
    const sql = request.params.arguments?.sql as string;

    const client = await pool.connect();
    try {
      await client.query(&quot;BEGIN TRANSACTION READ ONLY&quot;);
      // Force a prepared statement: Prevents multiple statements in the same query.
      // Name is unique per session, but we use a single session per query.
      const result = await client.query({
        name: &quot;sandboxed-statement&quot;,
        text: sql,
        values: [],
      });
      return {
        content: [
          { type: &quot;text&quot;, text: JSON.stringify(result.rows, undefined, 2) },
        ],
      };
    } finally {
      client
        .query(&quot;ROLLBACK&quot;)
        .catch((error) =&gt;
          console.warn(&quot;Could not roll back transaction:&quot;, error),
        );

      // Destroy session to clean up resources.
      client.release(true);
    }
  }

  throw new Error(&quot;Tool not found&quot;);
});

/**
 * @param tableNameOrAll {string}
 */
async function getSchema(client, tableNameOrAll) {
  const select =
    &quot;SELECT column_name, data_type, is_nullable, column_default, table_name FROM information_schema.columns&quot;;

  let result;
  if (tableNameOrAll === ALL_TABLES) {
    result = await client.query(
      `${select} WHERE table_schema NOT IN (&#x27;pg_catalog&#x27;, &#x27;information_schema&#x27;)`,
    );
  } else {
    result = await client.query(`${select} WHERE table_name = $1`, [
      tableNameOrAll,
    ]);
  }

  const allTableNames = Array.from(
    new Set(result.rows.map((row) =&gt; row.table_name).sort()),
  );

  let sql = &quot;```sql\n&quot;;
  for (let i = 0, len = allTableNames.length; i &lt; len; i++) {
    const tableName = allTableNames[i];
    if (i &gt; 0) {
      sql += &quot;\n&quot;;
    }

    sql += [
      `create table &quot;${tableName}&quot; (`,
      result.rows
        .filter((row) =&gt; row.table_name === tableName)
        .map((row) =&gt; {
          const notNull = row.is_nullable === &quot;NO&quot; ? &quot;&quot; : &quot; not null&quot;;
          const defaultValue =
            row.column_default != null ? ` default ${row.column_default}` : &quot;&quot;;
          return `    &quot;${row.column_name}&quot; ${row.data_type}${notNull}${defaultValue}`;
        })
        .join(&quot;,\n&quot;),
      &quot;);&quot;,
    ].join(&quot;\n&quot;);
    sql += &quot;\n&quot;;
  }
  sql += &quot;```&quot;;

  return sql;
}
</code></pre>
<p>De la même manière que pour lister les tools, nous avons le schéma <code>CallToolRequestSchema</code> pour intercepter la requête de leur exécution.</p>
<p>On peut maintenant démarrer notre serveur :</p>
<pre><code class="language-ts:mcp-server.ts">const main = async () =&gt; {
  await server.connect(stdioTransport);

  // Dans le cas d&#x27;un transport HTTP
  // app.post(&quot;/mcp&quot;, async (req, res) =&gt; {
  //   await transport.handleRequest(req, res);
  // });

  // app.listen(Number(Bun.env.PORT ?? 3001));
};

main();
</code></pre>
<h3>Implémentation Next (Client)</h3>
<p>Créons maintenant notre page sur notre application Next.js, en utilisant le SDK AI de Vercel :</p>
<pre><code class="language-tsx:app/page.tsx">&quot;use client&quot;;



export default function HomePage() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();

  return (
    &lt;div className=&quot;flex flex-col h-screen max-w-4xl mx-auto p-4&quot;&gt;
      &lt;div className=&quot;flex-1 overflow-y-auto mb-4&quot;&gt;
        {messages.map((message, index) =&gt; (
          &lt;div
            key={index}
            className={`mb-4 p-3 rounded-lg ${
              message.role === &quot;user&quot; ? &quot;bg-blue-100 ml-8&quot; : &quot;bg-gray-100 mr-8&quot;
            }`}
          &gt;
            &lt;div className=&quot;font-semibold text-sm text-gray-600 mb-1&quot;&gt;
              {message.role === &quot;user&quot; ? &quot;You&quot; : &quot;Assistant&quot;}
            &lt;/div&gt;
            &lt;div className=&quot;text-gray-800&quot;&gt;
              &lt;Markdown&gt;{message.content}&lt;/Markdown&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        ))}
      &lt;/div&gt;

      &lt;form onSubmit={handleSubmit} className=&quot;flex gap-2&quot;&gt;
        &lt;input
          value={input}
          onChange={handleInputChange}
          placeholder=&quot;Type your message...&quot;
          className=&quot;flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500&quot;
        /&gt;
        &lt;button
          type=&quot;submit&quot;
          className=&quot;px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500&quot;
        &gt;
          Send
        &lt;/button&gt;
      &lt;/form&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<h3>Implémentation Next (Serveur)</h3>
<p>Créons maintenant notre route API <code>/api/chat</code>, utilisée par le hook <code>useChat</code>.</p>
<pre><code class="language-ts:app/api/chat/route.ts">




export async function POST(req: NextRequest) {
  const { messages } = await req.json();
  const mcpTransport = new Experimental_StdioMCPTransport({
    command: &quot;bun&quot;,
    args: [&quot;pg:mcp&quot;],
  });

  const mcpClient = await experimental_createMCPClient({
    transport: mcpTransport,
    name: &quot;postgres-mcp-client&quot;,
  });

  try {
    const tools = await mcpClient.tools();

    const result = streamText({
      model: anthropic(&quot;claude-4-sonnet-20250514&quot;),
      messages,
      tools,
      maxSteps: 3,
      onError: async (error) =&gt; {
        await mcpClient.close();
      },
      onFinish: async () =&gt; {
        await mcpClient.close();
      },
    });

    return result.toDataStreamResponse();
  } catch (error) {
    console.error(&quot;Error:&quot;, error);
  }
}
</code></pre>
<p>Le SDK AI fournit des outils actuellement au stade expérimental permettant d&#x27;intéragir avec des serveurs MCP et d&#x27;en récupérer les tools.</p>
<pre><code class="language-ts">const mcpTransport = new Experimental_StdioMCPTransport({
  command: &#x27;bun&#x27;,
  args: [&#x27;pg:mcp&#x27;],
})

const mcpClient = await experimental_createMCPClient({
  transport: mcpTransport,
  name: &#x27;postgres-mcp-client&#x27;,
})
</code></pre>
<p>Ici on crée un transport lié à notre sortie standard utilisée par le binaire de notre serveur MCP. Ce binaire est exécuté via la commande <code>bun pg:mcp</code> que nous avons définie dans notre <code>package.json</code> :</p>
<pre><code class="language-json">&quot;pg:mcp&quot;: &quot;dotenv -e .env -- bun ./mcp-server.ts&quot;
</code></pre>
<p>L&#x27;appel à <code>mcpClient.tools()</code> renvoie une liste de tools compatible avec les API de tools du SDK AI, avec le bon nom, la bonne description, les bons paramètres, ainsi qu&#x27;une fonction d&#x27;exécution. Il est bien évidemment possible de se brancher à plusieurs serveurs MCP en même temps et d&#x27;en combiner les tools. Toutefois, il faut faire attention aux potentielles collisions qui peuvent survenir pour les noms de chacun. La propriété <code>maxSteps: 3</code> permet de continuer l&#x27;exécution de notre stream après l&#x27;appel à un tool. Par défaut, cette valeur est de 1. Ici on l&#x27;a définie à 3 pour permettre à nos tools de récupération du schéma et d&#x27;exécution de requêtes SQL d&#x27;être bien exécutés et renvoyés au client.</p>
<p>Testons maintenant l&#x27;ensemble ! Lançons notre serveur de développement Next :</p>
<pre><code class="language-sh">bun dev
</code></pre>
<p>Puis rendons-nous sur http://localhost:3000 pour voir notre application en action. Une page avec un simple champ de saisie en bas apparait. Dans mon cas, je choisis de faire la demande suivante :</p>
<blockquote>
<p>Donne moi la liste des 5 premiers email d&#x27;utilisateurs de ma base de données</p>
</blockquote>
<p><img src="https://www.premieroctet.com/blog/integration-mcp-dans-une-app-react/prompt.png" alt="liste des 5 premiers email d&#x27;utilisateurs de ma base de données"/></p>
<p>Le LLM a bien été capable de me sortir des données dont elle n&#x27;a, de base, pas connaissance, tout ça grâce à l&#x27;utilisations de tools récupérés depuis notre serveur MCP.</p>
<h2>Conclusion</h2>
<p>MCP a en quelques sortes révolutionné notre manière d&#x27;utiliser les LLMs. Leur capacité de s&#x27;interfacer entre les modèles et des API tierces rend l&#x27;accès aux données encore plus puissant qu&#x27;il ne l&#x27;était déjà. Ainsi on peut imaginer des cas d&#x27;utilisation sur des back-offices par exemple, simplifiant l&#x27;aggrégation de données provenant de sources multiples, le tout à partir d&#x27;une simple requête dans un language naturel. Les serveurs MCP peuvent aussi être installés sur des clients qui les supportent, par exemples les IDE mais aussi les applications de LLM (Claude, Chat GPT). Une liste très exhaustive de serveurs est disponible sur <a href="https://mcp.so/">mcp.so</a>, et vous trouverez surement le MCP qui répond à vos besoins.</p>
<p>Vous souhaitez implémenter un serveur MCP pour votre application ? N&#x27;hésitez pas à <a href="https://www.premieroctet.com/contact">nous contacter</a> !</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/integration-mcp-dans-une-app-react/illu.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Doter votre site d'un agent IA sur mesure, c'est simple (et c'est souvent par là que tout commence)]]></title>
            <link>https://www.premieroctet.com/blog/doter-votre-site-d-un-agent-ia-sur-mesure-cest-simple-et-souvent-par-la-que-tout-commence</link>
            <guid>https://www.premieroctet.com/blog/doter-votre-site-d-un-agent-ia-sur-mesure-cest-simple-et-souvent-par-la-que-tout-commence</guid>
            <pubDate>Fri, 20 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Découvrez comment créer un agent IA personnalisé pour votre site web. Une solution simple et efficace pour intégrer l'intelligence artificielle dans vos projets.]]></description>
            <content:encoded><![CDATA[<p>Depuis quelques mois, on nous demande régulièrement si on est capable de développer un agent « à la ChatGPT » directement intégré à un site. La réponse est oui. Et mieux : c&#x27;est souvent le premier pas idéal pour introduire de l&#x27;IA dans votre projet.</p>
<p>Grâce à la popularité de ChatGPT, tout le monde connaît désormais cette interface en mode chat. Elle est simple à prendre en main, naturelle pour l&#x27;utilisateur, et c&#x27;est devenu un standard.</p>
<p>Chez Premier Octet, on s&#x27;appuie sur le <a href="https://ai-sdk.dev/">Vercel AI SDK</a> pour développer ce type d&#x27;interface. C&#x27;est l&#x27;état de l&#x27;art pour créer des expériences réactives, élégantes, avec un support natif du streaming des réponses et du tool calling (on y revient juste après).</p>
<h2>Qu&#x27;est-ce qu&#x27;un agent IA ?</h2>
<p>Un agent IA, c&#x27;est tout simplement une interface qui peut comprendre vos demandes en langage naturel et agir en conséquence. Contrairement à un chatbot classique avec des réponses préprogrammées, l&#x27;agent peut analyser le contexte, prendre des décisions et utiliser des outils pour accomplir des tâches concrètes.</p>
<p>Concrètement, l&#x27;agent peut tourner avec différents modèles de langage : GPT (OpenAI), Claude (Anthropic), Mistral, Gemini… On se connecte à leur API et on garde la liberté de personnaliser l&#x27;interface pour coller à l&#x27;identité de nos clients.</p>
<p>Pour rendre cet agent vraiment utile, il y a deux façons principales de le connecter à vos données métiers.</p>
<p>Et bonne nouvelle : elles sont complémentaires.</p>
<h2>Connecter l&#x27;agent à vos données métiers</h2>
<h3>La boite à outils</h3>
<p>La première approche consiste à donner des outils à l&#x27;agent. C&#x27;est ce qui va lui permettre d&#x27;agir : consulter une commande, chercher un bien immobilier, créer un ticket, ajouter un produit au panier…</p>
<p>Concrètement, comment ça marche ?</p>
<p>On commence par définir ensemble ce que l&#x27;agent doit savoir faire. Ensuite, on développe la &quot;boîte à outils&quot; sur mesure, que le modèle pourra appeler selon les besoins exprimés dans la conversation.</p>
<p>Chaque outil a un contrat clair : il attend certains paramètres (certains obligatoires, d&#x27;autres optionnels), et il renvoie une réponse structurée.</p>
<p>Prenons l&#x27;exemple d&#x27;un outil permetant de lancer <strong>une recherche d&#x27;annonces immobilières</strong>. Cet outil attend au minimum :</p>
<ul>
<li>un lieu</li>
<li>une surface minimale</li>
<li>un budget maximum</li>
</ul>
<p>Si l&#x27;utilisateur ne donne pas toutes les infos d&#x27;emblée, l&#x27;agent <strong>va naturellement poser les questions manquantes</strong>. Une fois les critères réunis, il déclenche l&#x27;outil, qui interroge votre API ou base de données. L&#x27;agent interprète la réponse et répond dans la foulée.</p>
<p>On a alors deux options :</p>
<ul>
<li>Soit on reste dans le chat pur : l&#x27;agent répond avec du texte, qu&#x27;on peut formater, styliser, cadrer.</li>
<li>Soit on va plus loin <strong>avec des widgets personnalisés dans le flux du chat</strong> : par exemple des cards reprenant l&#x27;UI du site, avec les annonces directement affichées.</li>
</ul>
<p><strong>Cette combinaison de langage naturel et de widgets métiers permet d&#x27;avoir une expérience utilisateur moderne, fluide, et surtout utile.</strong></p>
<p>Et évidemment, on peut aller très loin : prise de rendez-vous, ajout au panier, paiement, génération de documents… C&#x27;est une interface conversationnelle sur mesure.</p>
<h3>Transmettre de la connaissance métier</h3>
<p>La deuxième approche consiste à enrichir l&#x27;agent avec du savoir métier.</p>
<p>On parle ici de RAG (Retrieval-Augmented Generation). Pour résumé, on vectorise votre contenu (FAQ, fiches produits, documentation…) pour qu&#x27;il devienne compréhensible par le modèle de langage.</p>
<p>Une fois vectorisé, l&#x27;agent pourra répondre à des questions précises sur vos services, vos produits, vos conditions générales, etc., avec un niveau de détail et de pertinence qui dépasse les simples modèles &quot;out of the box&quot;.</p>
<p>Si vous souhaitez approfondir le sujet du RAG, je vous invite à lire <a href="https://www.premieroctet.com/blog/comment-fonctionne-un-rag">notre article sur le sujet</a>.</p>
<p>Ces deux approches sont bien sûr complémentaires. Elles permettent de couvrir des cas d&#x27;usage différents :</p>
<ul>
<li>un agent IA avec des outils pour interagir avec vos services</li>
<li>un agent IA avec du savoir métier pour répondre à des questions précises</li>
</ul>
<p>C&#x27;est souvent la combinaison de ces deux approches qui permet de couvrir la majorité des besoins.</p>
<p><img src="https://www.premieroctet.com/blog/doter-votre-site-d-un-agent-ia-sur-mesure-cest-simple-et-souvent-par-la-que-tout-commence/agent.png" alt="Agent IA"/></p>
<h2>Et le timing dans tout ça ?</h2>
<p>C&#x27;est souvent la question qui suit : combien de temps faut-il pour mettre ça en place ?</p>
<p><strong>5 à 10 jours suffisent pour une première version fonctionnelle, intégrée à votre site, avec une interface sur mesure et quelques outils bien choisis.</strong></p>
<p>On commence par cadrer ensemble : quelles capacités ? quels outils ? quelles données ? Une fois les specs définies, on développe, on teste, on connecte. Et on laisse l&#x27;IA faire sa part.</p>
<h2>Et le coût d&#x27;utilisation ?</h2>
<p>L&#x27;agent repose sur une API tierce (OpenAI, Anthropic, Mistral…). Ces APIs sont aujourd&#x27;hui accessibles et scalables. À titre d&#x27;exemple, voici les tarifs de l&#x27;API OpenAI GPT-4 Mini (en juin 2025) :</p>
<ul>
<li>0,40 $ / million de tokens en entrée</li>
<li>1,60 $ / million de tokens en sortie</li>
</ul>
<p>Pour une session de 10 échanges (Q/R), on parle généralement de moins d&#x27;un centime.</p>
<p>Il est aussi possible d&#x27;auto-héberger un modèle open source, mais cela nécessite une machine avec GPU. On peut aussi passer par des APIs sur étagères comme le propose OVH si l&#x27;on souhaite garder un contrôle total sur les données.</p>
<h2>En résumé</h2>
<p>Créer un agent IA pour votre site n&#x27;est plus un sujet &quot;futuriste&quot; ou &quot;expérimental&quot;. C&#x27;est concret, rapide à mettre en place, et surtout utile.</p>
<p>C&#x27;est aussi souvent une première brique pour aller plus loin ensuite : classification de contenu, génération de texte, aide à la rédaction, automatisation de tâches internes, génération de documents et assets…</p>
<p>Envie de tenter l&#x27;expérience ?</p>
<p><strong>Chez Premier Octet, on vous accompagne pour concevoir un agent IA aligné avec vos besoins, intégré à votre site, connecté à vos données.</strong></p>
<p><a href="https://www.premieroctet.com/contact">Contactez-nous</a> si vous avez envie d&#x27;en discuter ou si vous avez déjà une idée d&#x27;usage. On vous aide à la concrétiser en quelques jours.</p>
<p>👋</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/doter-votre-site-d-un-agent-ia-sur-mesure-cest-simple-et-souvent-par-la-que-tout-commence/illu.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[TanStack Start: la relève des frameworks React ?]]></title>
            <link>https://www.premieroctet.com/blog/tanstack-releve-frameworks-react</link>
            <guid>https://www.premieroctet.com/blog/tanstack-releve-frameworks-react</guid>
            <pubDate>Wed, 18 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Il y a quelques semaines de ça, une faille de sécurité a été découverte dans NextJS au niveau de la gestion de l'authentification au sein du fichier middleware...]]></description>
            <content:encoded><![CDATA[<p>Il y a quelques semaines de ça, une faille de sécurité a été découverte dans NextJS au niveau de la gestion de <a href="https://vercel.com/blog/postmortem-on-next-js-middleware-bypass">l&#x27;authentification au sein du fichier middleware</a>. Cette faille a été corrigée assez rapidement, mais Vercel n&#x27;a pas réellement communiqué dessus à vrai dire. Par la suite, les réseaux se sont enflammés et pour la plupart des développeurs, c&#x27;était la goutte de trop pour un framework qui souffrait déjà de bien des inconvénients notamment en terme de DX. J&#x27;ai pu voir passer de nombreux retours très positifs de la part de développeurs ayant complètement migré de NextJS vers <a href="https://tanstack.com/start">TanStack Start</a> (que nous nommerons <code>Start</code> pour la suite de cet article). Alors au final, est-ce que ce nouveau framework a des chances de concurrencer Remix ou NextJS, déjà bien en place dans l&#x27;écosystème React ?</p>
<h2>Présentation technique</h2>
<p>Start, créé par <a href="https://x.com/tannerlinsley">Tanner Linsley</a> que l&#x27;on ne présente plus, possède un routing basé sur une librairie déjà existante, <a href="https://tanstack.com/router">TanStack Router</a>. Utilisable de base pour les SPA, Start l&#x27;intègre notamment pour effectuer du SSR, à la manière de ce que fait <a href="https://remix.run/">Remix</a> avec <a href="https://reactrouter.com/">React Router</a>.</p>
<p>Côté bundling, Start se base sur <a href="https://vitejs.dev/">Vite</a>, nous permettant ainsi de bénéficier de tous les plugins Vite disponibles. Pour rappel, Next se base sur <a href="https://swc.rs/">SWC</a> ou <a href="https://turbo.build/pack">TurboPack</a> selon les versions.</p>
<p>Explorons plus en détails Start via une petite application toute simple avec une authentification basique, une liste de posts et les détails d&#x27;un post.</p>
<h2>Exploration</h2>
<h3>Mise en place</h3>
<p>Start propose un exemple de base à cloner faisant office de starter kit. Nous allons ici mettre en place un projet en partant de zéro, utilisant Tailwind CSS.</p>
<pre><code class="language-sh">mkdir starter-example
cd starter-example
yarn init -y
</code></pre>
<p>Installons maintenant les dépendances :</p>
<pre><code class="language-sh">yarn add @tanstack/react-start @tanstack/react-router react react-dom zod
yarn add -D vite @vitejs/plugin-react typescript @types/react @types/react-dom tailwindcss @tailwindcss/vite
</code></pre>
<p>Mettons en place nos différents fichiers de configuration :</p>
<pre><code class="language-json:tsconfig.json">{
  &quot;compilerOptions&quot;: {
    &quot;jsx&quot;: &quot;react-jsx&quot;,
    &quot;moduleResolution&quot;: &quot;Bundler&quot;,
    &quot;module&quot;: &quot;ESNext&quot;,
    &quot;target&quot;: &quot;ES2022&quot;,
    &quot;skipLibCheck&quot;: true,
    &quot;strictNullChecks&quot;: true,
  },
}
</code></pre>
<pre><code class="language-ts:vite.config.ts">



export default defineConfig({
    plugins: [
      tanstackStart(),
      tailwindcss()
    ],
  },
})
</code></pre>
<p>Mettons à jour les scripts de notre <code>package.json</code> :</p>
<pre><code class="language-json:package.json">{
  &quot;scripts&quot;: {
    &quot;dev&quot;: &quot;vite dev&quot;,
    &quot;build&quot;: &quot;vite build&quot;,
    &quot;start&quot;: &quot;node .output/server/index.mjs&quot;
  }
}
</code></pre>
<p>Créons maintenant notre points d&#x27;entrée requis par Start. Celui-ci se situera dans le dossier <code>src</code> à la racine du projet.</p>
<pre><code class="language-tsx:src/router.tsx">


export function createRouter() {
    const router = createTanStackRouter({
    routeTree,
    scrollRestoration: true,
    /*
        * il est ici possible de configurer les options par défaut du routeur,
        * comme par exemple le composant par défaut pour les routes en not found,
        */
    })

    return router

}

declare module &#x27;@tanstack/react-router&#x27; {
  interface Register {
    router: ReturnType&lt;typeof createRouter&gt;
  }
}
</code></pre>
<div type="info"><p>Le fichier <code>routeTree.gen</code> n&#x27;existe pas et c&#x27;est normal, il est généré automatiquement par le
bundler. C&#x27;est notamment lui qui est en charge de gérer le typage de nos routes.</p></div>
<p>Créons notre feuille de style CSS pour Tailwind :</p>
<pre><code class="language-css:app/styles/styles.css">@import &quot;tailwindcss&quot;;
</code></pre>
<p>Créons maintenant notre document HTML principal. C&#x27;est l&#x27;équivalent du fichier <code>layout.tsx</code> à la racine du dossier <code>app</code> d&#x27;une application Next. Ce fichier ce situe dans un sous-dossier <code>routes</code>. C&#x27;est dans ce sous-dossier que ce situeront toutes les routes de notre application.</p>
<pre><code class="language-ts:src/routes/__root.tsx">

  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from &quot;@tanstack/react-router&quot;;
// import de la feuille de style au format URL


export const Route = createRootRoute({
  head: () =&gt; ({
    meta: [
      {
        charSet: &quot;utf-8&quot;,
      },
      {
        name: &quot;viewport&quot;,
        content: &quot;width=device-width, initial-scale=1&quot;,
      },
      {
        title: &quot;Start Example&quot;,
      },
    ],
    links: [
      {
        rel: &quot;stylesheet&quot;,
        href: appCss,
      },
    ],
  }),
  component: RootComponent,
});

function RootComponent() {
  return (
    &lt;RootDocument&gt;
      &lt;Outlet /&gt;
    &lt;/RootDocument&gt;
  );
}

function RootDocument({ children }: Readonly&lt;{ children: ReactNode }&gt;) {
  return (
    &lt;html&gt;
      &lt;head&gt;
        &lt;HeadContent /&gt;
      &lt;/head&gt;
      &lt;body className=&quot;bg-white dark:bg-slate-900&quot;&gt;
        &lt;div className=&quot;min-h-screen flex flex-col&quot;&gt;{children}&lt;/div&gt;
        &lt;Scripts /&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}
</code></pre>
<p>Le composant <a href="https://tanstack.com/router/latest/docs/framework/react/guide/outlets"><code>Outlet</code></a> est un composant très utile pour construire des layouts imbriqués. Celui-ci rend le contenu de la route active et peut être utilisé à d&#x27;autres endroits de l&#x27;application. Nous verrons un cas d&#x27;utilisation plus tard.</p>
<p>Nous pouvons maintenant exécuter <code>yarn dev</code> pour démarrer notre bundler.</p>
<h3>Notre première route</h3>
<p>Créons un fichier <code>src/routes/index.tsx</code> pour notre première route. Si vous avez votre bundler d&#x27;ouvert, vous constaterez une fonctionnalité très intéressante venant de TanStack Router : un template de base pour le fichier avec tout le contenu nécessaire est généré. Celui-ci change en fonction du chemin du fichier. Par exemple, renommer le fichier changera le chemin de la route.</p>
<pre><code class="language-tsx:src/routes/index.tsx">

export const Route = createFileRoute(&quot;/&quot;)({
  component: Home,
});

function Home() {
  return (
    &lt;form
      className=&quot;flex flex-1 flex-col justify-center items-center gap-4&quot;
    &gt;
      &lt;div className=&quot;flex flex-col gap-2&quot;&gt;
        &lt;label htmlFor=&quot;username&quot; className=&quot;dark:text-white&quot;&gt;
          Username
        &lt;/label&gt;
        &lt;input
          name=&quot;username&quot;
          id=&quot;username&quot;
          placeholder=&quot;Username&quot;
          className=&quot;dark:text-white border dark:border-white&quot;
        /&gt;
        &lt;label htmlFor=&quot;password&quot; className=&quot;dark:text-white&quot;&gt;
          Password
        &lt;/label&gt;
        &lt;input
          name=&quot;password&quot;
          id=&quot;password&quot;
          placeholder=&quot;Password&quot;
          type=&quot;password&quot;
          className=&quot;dark:text-white border dark:border-white&quot;
        /&gt;
      &lt;/div&gt;
      &lt;button
        type=&quot;submit&quot;
        className=&quot;bg-blue-500 px-4 py-3 rounded-md text-white cursor-pointer&quot;
      &gt;
        Login
      &lt;/button&gt;
    &lt;/form&gt;
  );
}
</code></pre>
<p>En se rendant sur <code>http://localhost:3000</code>, notre page formulaire s&#x27;affiche correctement !</p>
<p>Mettons maintenant en place notre seconde route, qui sera notre liste de posts. Cette route devra être plus tard protégée par une authentification.</p>
<div><div><div value="src/routes/posts.tsx">src/routes/posts.tsx</div><div value="src/components/PostCard.tsx">src/components/PostCard.tsx</div></div><div value="src/routes/posts.tsx"><pre><code class="language-tsx">



export const Route = createFileRoute(&#x27;/posts&#x27;)({
  component: RouteComponent,
  loader: () =&gt; {
    return loadPosts()
  },
})

function RouteComponent() {
const loadedPosts = Route.useLoaderData();

return (

&lt;div className=&quot;flex flex-1 justify-center items-center&quot;&gt;
  &lt;div className=&quot;grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6&quot;&gt;
    {loadedPosts.map((post) =&gt; (
      &lt;PostCard key={post.id} title={post.title} content={post.content} id={post.id} /&gt;
    ))}
  &lt;/div&gt;
&lt;/div&gt;
); }

</code></pre></div><div value="src/components/PostCard.tsx"><pre><code class="language-tsx">

type Props = {
  title: string;
  content: string;
  id: number;
};

const PostCard = ({ title, content, id }: Props) =&gt; {
  return (
    &lt;Link
      to=&quot;/posts/$id&quot;
      // Prefetch de la page au hover
      preload=&quot;intent&quot;
      params={{ id: id.toString() }}
      className=&quot;bg-white shadow-md rounded-lg p-6 mb-6 dark:shadow-white hover:scale-[101%] transition-transform duration-300 cursor-pointer&quot;
    &gt;
      &lt;h2 className=&quot;text-2xl font-bold mb-4&quot;&gt;{title}&lt;/h2&gt;
      &lt;p className=&quot;text-gray-700 line-clamp-3 text-ellipsis&quot;&gt;{content}&lt;/p&gt;
    &lt;/Link&gt;
  );
};

export default PostCard;
</code></pre></div></div>
<p>Ici on retrouve une API assez similaire à celle de Remix pour charger de la donnée côté serveur :</p>
<ul>
<li><code>loader</code> est exécuté au chargement de la page et renvoie notre donnée</li>
<li>on récupère cette donnée via le hook <code>useLoaderData</code> accessible depuis notre objet <code>Route</code>.</li>
</ul>
<p>Ici, notre liste de posts est chargée depuis une fonction serveur, l&#x27;opportunité de faire la connaissance avec les server functions de Start.</p>
<h3>Server functions</h3>
<p>Les server functions sont, comme celles de React et Next, des fonctions appelées depuis n&#x27;importe où (sauf les routes API) et exécutées côté serveur. Les server functions de Start ont toutefois des fonctionnalités en plus par rapport à ce que l&#x27;on retrouve par exemple sur Next :</p>
<ul>
<li>possibilité de définir la méthode HTTP à utiliser (POST ou GET)</li>
<li>ajout de validation (via <a href="https://zod.dev/">zod</a> par exemple)</li>
<li>accès à l&#x27;ensemble de la requête HTTP</li>
<li>possibilité de définir le type de réponse (JSON, stream)</li>
</ul>
<p>Créons notre fonction <code>loadPosts</code> :</p>
<div><div><div value="src/functions/posts.ts">src/functions/posts.ts</div><div value="src/mocks/posts.ts">src/mocks/posts.ts</div></div><div value="src/functions/posts.ts"><pre><code class="language-tsx">


export const loadPosts = createServerFn()
  .handler((async) =&gt; {
    return posts;
  });
</code></pre></div><div value="src/mocks/posts.ts"><pre><code class="language-tsx">export const posts = Array.from({ length: 10 }, (_, i) =&gt; ({
  id: i + 1,
  title: &quot;Lorem ipsum dolor sit amet.&quot;,
  content: &#x27;....lorem ipsum&#x27;,
}));
</code></pre></div></div>
<p>Profitons-en pour créer notre fonction de login pour notre formulaire ! Chose très intéressante, Start propose une solution clé en main pour gérer les sessions, que ce soit via le <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage">session storage</a> ou les cookies.</p>
<div><div><div value="src/functions/auth.ts">src/functions/auth.ts</div><div value="src/utils/session.ts">src/utils/session.ts</div><div value="src/routes/login.tsx">src/routes/login.tsx</div></div><div value="src/functions/auth.ts"><pre><code class="language-tsx">






export const loginAction = createServerFn({
  method: &#x27;POST&#x27;,
})
  .validator((data: unknown) =&gt; {
    const schema = z.object({
      username: z.string().min(1),
      password: z.string().min(1),
    })

    const result = schema.safeParse(data)

    if (!result.success) {
      throw json({ error: result.error.issues, message: &#x27;Validation failed&#x27; }, { status: 422 })
    }

    return result.data
  })
  .handler(async ({ data }) =&gt; {
    // Fake delay
    await setTimeout(1000)
    if (data.username !== &#x27;admin&#x27; &amp;&amp; data.password !== &#x27;admin&#x27;) {
      setResponseStatus(401)
      return {
        success: false,
        error: &#x27;Unauthorized&#x27;,
      }
    }

    const session = await useAppSession()

    await session.update({
      name: data.username,
    })

    // On redirige vers la page de posts après la connexion réussie
    throw redirect({
      to: &#x27;/posts&#x27;,
    })
  })

export const logoutAction = createServerFn({ method: &#x27;POST&#x27; }).handler(async () =&gt; {
  const session = await useAppSession()

  await session.clear()

  throw redirect({
    to: &#x27;/&#x27;,
  })
})
</code></pre></div><div value="src/utils/session.ts"><pre><code class="language-ts">


type UserSession = {
  name: string
}

export const useAppSession = () =&gt; {
  return useSession&lt;UserSession&gt;({
    password: &#x27;MyPasswordAsALongStringOfCharactersOfAtLeast32Characters&#x27;,
    // On utilise les cookies, on peut configurer les options de cookie ici.
    // Par défaut, le session storage est utilisé.
    cookie: {},
  })
}
</code></pre></div><div value="src/routes/login.tsx"><pre><code class="language-tsx">



export const Route = createFileRoute(&#x27;/&#x27;)({
  component: Home,
})

function Home() {
  const login = useServerFn(loginAction)

  return (
    &lt;form
      className=&quot;flex flex-1 flex-col justify-center items-center gap-4&quot;
      onSubmit={async (e) =&gt; {
        e.preventDefault()
        const formData = new FormData(e.target as HTMLFormElement)
        const username = formData.get(&#x27;username&#x27;) as string
        const password = formData.get(&#x27;password&#x27;) as string
        login({ data: { username, password } })
      }}
    &gt;
      &lt;div className=&quot;flex flex-col gap-2&quot;&gt;
        &lt;label htmlFor=&quot;username&quot; className=&quot;dark:text-white&quot;&gt;
          Username
        &lt;/label&gt;
        &lt;input
          name=&quot;username&quot;
          id=&quot;username&quot;
          placeholder=&quot;Username&quot;
          className=&quot;dark:text-white border dark:border-white&quot;
        /&gt;
        &lt;label htmlFor=&quot;password&quot; className=&quot;dark:text-white&quot;&gt;
          Password
        &lt;/label&gt;
        &lt;input
          name=&quot;password&quot;
          id=&quot;password&quot;
          placeholder=&quot;Password&quot;
          type=&quot;password&quot;
          className=&quot;dark:text-white border dark:border-white&quot;
        /&gt;
      &lt;/div&gt;
      &lt;button type=&quot;submit&quot; className=&quot;bg-blue-500 px-4 py-3 rounded-md text-white cursor-pointer&quot;&gt;
        Login
      &lt;/button&gt;
    &lt;/form&gt;
  )
}
</code></pre></div></div>
<p>Et voilà, après la connexion, vous êtes maintenant redirigé vers la liste des posts !</p>
<h3>Le concept de layout</h3>
<p>Un layout a pour but de créer une structure statique dans laquelle va s&#x27;intégrer le contenu de notre route. Avec Next, nous créons un layout via une route <code>layout.tsx</code> au même niveau que la route. Start (via l&#x27;intermédiaire du Router) a une approche très similaire à React Router pour gérer les layouts. Tant qu&#x27;une partie de notre URL est matchée, alors sa route est rendue.</p>
<p>Par exemple :</p>
<ul>
<li><code>/posts</code> -&gt; <code>&lt;RootDocument&gt;&lt;Posts&gt;</code></li>
<li><code>/posts/:id</code> -&gt; <code>&lt;RootDocument&gt;&lt;Posts&gt;&lt;Post&gt;</code></li>
</ul>
<p>Ce comportement est configurable en fonction du <a href="https://tanstack.com/router/latest/docs/framework/react/routing/file-naming-conventions">nom du fichier</a>. Par conséquent, par défaut, une route comme <code>/posts</code> que l&#x27;on a créé peut agir comme un layout pour <code>/posts/:id</code>. Le contenu de <code>/posts/:id</code> sera affiché via le composant <code>Outlet</code>.</p>
<p>Il est aussi possible de faire en sorte de créer un layout qui n&#x27;est pas lié à une route. C&#x27;est ce que l&#x27;on appelle un <code>pathless layout</code>. Celui-ci aura un nom de fichier préfixé d&#x27;un <code>_</code>.</p>
<p>Créons un layout utilisé au sein de nos routes authentifiées :</p>
<pre><code class="language-tsx:src/routes/_app.tsx">



export const Route = createFileRoute(&quot;/_app&quot;)({
  component: RouteComponent,
});

function RouteComponent() {
  const logout = useServerFn(logoutAction);

  return (
    &lt;div className=&quot;flex flex-col flex-1&quot;&gt;
      &lt;nav className=&quot;flex items-center justify-end py-3 px-4&quot;&gt;
        &lt;ul&gt;
          &lt;li
            className=&quot;dark:text-white hover:underline cursor-pointer&quot;
            onClick={() =&gt; {
              logout();
            }}
          &gt;
            Logout
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/nav&gt;
      &lt;div className=&quot;px-4&quot;&gt;
        &lt;Outlet /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>Nous devons maintenant déplacer notre route <code>posts.tsx</code> dans un sous dossier <code>_app</code>, correspondant au nom de notre layout. Évidemment, nous pouvons donner le nom que l&#x27;on veut à notre layout. Il faut simplement garder une correspondance entre le nom du fichier du layout et le dossier créé.</p>
<p>Notre liste de posts est maintenant bien intégré au sein de notre layout. Il est évidemment possible de nester les layouts à l&#x27;infini, c&#x27;est le composant <code>Outlet</code> qui se charge de rendre les routes enfant à l&#x27;emplacement désiré.</p>
<p>Nous pouvons maintenant, grâce au layout, prévenir l&#x27;accès à nos pages authentifiées. En effet, un pathless layout dispose des mêmes possibilités d&#x27;usage d&#x27;une route. On peut donc utiliser la propriété <code>beforeLoad</code> afin de vérifier la connexion de l&#x27;utilisateur.</p>
<div><div><div value="src/routes/__root.tsx">src/routes/__root.tsx</div><div value="src/routes/_app.tsx">src/routes/_app.tsx</div></div><div value="src/routes/__root.tsx"><pre><code class="language-tsx">

  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from &quot;@tanstack/react-router&quot;;




const fetchUser = createServerFn({ method: &quot;GET&quot; }).handler(async () =&gt; {
const session = await useAppSession();

if (!session.data.name) {
return null;
}
return { name: session.data.name };
});

export const Route = createRootRoute({
  head: () =&gt; ({
    meta: [
      {
        charSet: &quot;utf-8&quot;,
      },
      {
        name: &quot;viewport&quot;,
        content: &quot;width=device-width, initial-scale=1&quot;,
      },
      {
        title: &quot;Start Example&quot;,
      },
    ],
    links: [
      {
        rel: &quot;stylesheet&quot;,
        href: appCss,
      },
    ],
  }),
  component: RootComponent,
  beforeLoad: async () =&gt; {
    const user = await fetchUser();

    return { user };

},
});

function RootComponent() {
return (

&lt;RootDocument&gt;
  &lt;Outlet /&gt;
&lt;/RootDocument&gt;
); }

function RootDocument({ children }: Readonly&lt;{ children: ReactNode }&gt;) {
return (

&lt;html&gt;
  &lt;head&gt;
    &lt;HeadContent /&gt;
  &lt;/head&gt;
  &lt;body className=&quot;bg-white dark:bg-slate-900&quot;&gt;
    &lt;div className=&quot;min-h-screen flex flex-col&quot;&gt;{children}&lt;/div&gt;
    &lt;Scripts /&gt;
  &lt;/body&gt;
&lt;/html&gt;
); }

</code></pre></div><div value="src/routes/_app.tsx"><pre><code class="language-tsx">



export const Route = createFileRoute(&quot;/_app&quot;)({
  component: RouteComponent,
  beforeLoad: async ({ context }) =&gt; {
    const isLoggedIn = !!context.user;

    if (!isLoggedIn) {
      throw redirect({
        to: &quot;/&quot;,
      });
    }
  },
});

function RouteComponent() {
  const logout = useServerFn(logoutAction);

  return (
    &lt;div className=&quot;flex flex-col flex-1&quot;&gt;
      &lt;nav className=&quot;flex items-center justify-end py-3 px-4&quot;&gt;
        &lt;ul&gt;
          &lt;li
            className=&quot;dark:text-white hover:underline cursor-pointer&quot;
            onClick={() =&gt; {
              logout();
            }}
          &gt;
            Logout
          &lt;/li&gt;
        &lt;/ul&gt;
      &lt;/nav&gt;
      &lt;div className=&quot;px-4&quot;&gt;
        &lt;Outlet /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre></div></div>
<p>Et voilà, maintenant toutes les routes enfant de notre layout <code>_app</code> sont protégées !</p>
<h3>Routes API</h3>
<p>Comme tout framework full-stack qui se respecte, Start dispose d&#x27;un concept de routes API. La notation de fichier pour le routing est la même que pour nos pages classiques.</p>
<p>Créons une route API nous permettant de récupérer notre liste de posts :</p>
<pre><code class="language-ts:src/routes/api/posts/index.ts">



export const APIRoute = createServerFileRoute(&quot;/api/posts&quot;).methods({
  GET: async ({ request }) =&gt; {
    return json(posts);
  },
});
</code></pre>
<p>Que ce soit pour les server functions ou les routes API, il est possible d&#x27;utiliser tout un ensemble de fonctions utilitaires fournies par Stack afin de gérer à le contenu d&#x27;une requête et d&#x27;une réponse. C&#x27;est le cas de la fonction <code>json</code>. Par exemple si l&#x27;on souhaite renvoyer un statut HTTP particulier pour notre requête :</p>
<pre><code class="language-ts:src/routes/api/posts/index.ts">



export const APIRoute = createServerFileRoute(&quot;/api/posts&quot;).methods({
  GET: async ({ request }) =&gt; {
    setResponseStatus(200);
    return json(posts);
  },
});
</code></pre>
<h3>Middlewares</h3>
<p>Les middlewares sont des fonctions que l&#x27;on branche aux server functions afin d&#x27;intercepter l&#x27;exécution de celles-ci. Elles permettent d&#x27;ajouter du contexte à nos requêtes, que ce soit lors de l&#x27;exécution côté client ou côté serveur.</p>
<p>Créons un middleware afin de rediriger l&#x27;utilisateur vers la page d&#x27;accueil s&#x27;il tente d&#x27;accéder à notre liste de posts en tant qu&#x27;utilisateur anonyme.</p>
<div><div><div value="src/middlewares/auth.ts">src/middlewares/auth.ts</div><div value="src/functions/posts.ts">src/functions/posts.ts</div></div><div value="src/middlewares/auth.ts"><pre><code class="language-ts">



export const authMiddleware = createMiddleware().server(async ({ next }) =&gt; {
  const user = await useAppSession();

if (!user.data.name) {
throw redirect({
to: &quot;/&quot;,
});
}

return next();
});

</code></pre></div><div value="src/functions/posts.ts"><pre><code class="language-ts">



export const loadPosts = createServerFn()
  .middleware([authMiddleware])
  .handler((async) =&gt; {
    return posts;
  });
</code></pre></div></div>
<p>Et voilà, maintenant le <code>handler</code> de notre server function ne s&#x27;exécutera uniquement si l&#x27;utilisateur est authentifié.</p>
<p>Un middleware est aussi utilisable sur une route API :</p>
<pre><code class="language-ts:src/routes/api/posts/index.ts">




export const APIRoute = createServerFileRoute(&quot;/api/posts&quot;).methods((api) =&gt; ({
  GET: api.middleware([authMiddleware]).handler(async ({ request }) =&gt; {
    setResponseStatus(200);
    return json(posts);
  }),
}));
</code></pre>
<h3>DevTools</h3>
<p>Un gros quick win par rapport à Next : la possibilité de débugger les routes (les données du loader par exemple). Cela est disponible via les <a href="https://tanstack.com/router/latest/docs/framework/react/devtools">DevTools de TanStack Router</a>, à intégrer directement au sein du composant <code>RootDocument</code>.</p>
<p><img src="https://www.premieroctet.com/blog/tanstack-releve-frameworks-react/devtools.png" alt="DevTools TanStack Router" title="DevTools TanStack Router"/></p>
<h3>Déploiement</h3>
<p>Le déploiement d&#x27;une application Start est très simple. Différents presets sont fournis, à passer soit au sein de la configuration, soit via un flag au moment du build. Pour un simple serveur node par exemple :</p>
<pre><code class="language-sh">yarn build
node .output/server/index.mjs
</code></pre>
<p>Start supporte les services d&#x27;hébergements les plus importants du marché comme <a href="https://tanstack.com/start/latest/docs/framework/react/hosting#vercel">Vercel</a> et <a href="https://tanstack.com/start/latest/docs/framework/react/hosting#netlify">Netlify</a>.</p>
<h2>Conclusion</h2>
<p>Globalement j&#x27;ai eu une bonne expérience avec Start. Une DX très agréable, une documentation très complète (comme toutes les librairies TanStack) et toutes les fonctionnalités que l&#x27;on attends d&#x27;un framework full-stack. Le fonctionnement des server functions est, selon moi, bien mieux que sur NextJS (méthode <code>GET/POST</code>, réponse customisable). L&#x27;ajout de devtools liés au routing est aussi un énorme plus. C&#x27;est en tout cas une solution sur laquelle nous allons garder un oeil attentif, car c&#x27;est une alternative très intéressante par rapport à NextJS. Une idée de projet qui nécessiterait TanStack Start ? Utilisez <a href="https://www.premieroctet.com/estimateur-projet-web-mobile">notre estimateur de projet</a> !</p>]]></content:encoded>
            <enclosure url="https://www.premieroctet.com/blog/tanstack-releve-frameworks-react/illu.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>