<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Elys Maldov</title>
    <description>The latest articles on Forem by Elys Maldov (@elys_m).</description>
    <link>https://forem.com/elys_m</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2628217%2F58c4e583-572a-47d9-a605-9116e68959ba.png</url>
      <title>Forem: Elys Maldov</title>
      <link>https://forem.com/elys_m</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/elys_m"/>
    <language>en</language>
    <item>
      <title>A low-cost idea to automate your e-commerce platform with Payload CMS and Hermes Agent</title>
      <dc:creator>Elys Maldov</dc:creator>
      <pubDate>Thu, 19 Mar 2026 16:50:35 +0000</pubDate>
      <link>https://forem.com/elys_m/a-low-cost-idea-to-automate-your-e-commerce-platform-with-payload-cms-and-hermes-agent-4j55</link>
      <guid>https://forem.com/elys_m/a-low-cost-idea-to-automate-your-e-commerce-platform-with-payload-cms-and-hermes-agent-4j55</guid>
      <description>&lt;p&gt;A few days ago, I saw this tweet on Indonesian X about how sellers are moving to their own marketplace platforms due to increasing e-commerce platform fees:&lt;/p&gt;

&lt;p&gt;

&lt;iframe class="tweet-embed" id="tweet-2033512460813312279-794" src="https://platform.twitter.com/embed/Tweet.html?id=2033512460813312279"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-2033512460813312279-794');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2033512460813312279&amp;amp;theme=dark"
  }





&lt;/p&gt;

&lt;p&gt;I remembered a few months ago Payload CMS finally released an update to their &lt;a href="https://payloadcms.com/docs/ecommerce/overview" rel="noopener noreferrer"&gt;e-commerce template&lt;/a&gt;. After trying it out, it gives you a really good setup for a basic e-commerce with auth, inventory management, cart, and even payment flow using Stripe (with an option to create integrate your own payment method). It even has marketing features such as SEO and page builder, and of course you can customize Payload CMS in any way you want even white-labelling it to use with multiple clients easily.&lt;/p&gt;

&lt;p&gt;In other news, Hermes Agent has been trending recently on the AI space due to it being a "better" version of OpenClaw. After giving Hermes Agent a spin, I got to say it's very capable, easier to setup, and runs better than OpenClaw! I really appreciate how easy it is to setup the agent with your WhatsApp/Telegram and use it like you would message your personal assistant. It also has a lot of built-in tools to do business stuff like analyzing data, drafting emails, and any task you can automate. It even has memory so it remembers who you are better the more you use it.&lt;/p&gt;

&lt;p&gt;Considering how much AI agents and LLMs have grown to help automating our tasks and making building products quicker, it wouldn't be crazy to try to build your own e-commerce platform and integrate an actual ai assistant that helps you automate operations so you can focus on important business stuff. In this article, I'll walk you through how to use Payload CMS to build a low-cost e-commerce platform and powering it with Hermes Agent to automatically assist in managing your platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'll do
&lt;/h2&gt;

&lt;p&gt;We'll setup a basic e-commerce platform complete with auth, inventory management, cart, payment, SEO, and page builder. We won't have to code much since we can just use Payload CMS with its e-commerce template, which basically provides all of those out-of-the-box. Next, we'll setup Hermes Agent locally and connect it to our Payload CMS e-commerce platform using MCP. In the end, we'll have a basic e-commerce platform and an agent we can talk to help manage our platform such as updating the inventory, analyzing user's carts, and even creating blog posts for marketing.&lt;/p&gt;

&lt;p&gt;You can clone this project's repo here: &lt;a href="https://github.com/ElysMaldov/payload-hermes-ecommerce" rel="noopener noreferrer"&gt;ElysMaldov/payload-hermes-ecommerce&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup Payload CMS with e-commerce template
&lt;/h2&gt;

&lt;p&gt;Make sure you've setup your environment to install &lt;a href="https://payloadcms.com/docs/getting-started/installation" rel="noopener noreferrer"&gt;Payload CMS&lt;/a&gt;, then follow the &lt;a href="https://github.com/payloadcms/payload/tree/main/templates/ecommerce#quick-start" rel="noopener noreferrer"&gt;e-commerce template quickstart&lt;/a&gt;. For this tutorial, we'll be using SQLite for the database, but you can use any DB you want.&lt;/p&gt;

&lt;p&gt;Once finished, you can run the program with &lt;code&gt;pnpm dev&lt;/code&gt;. I won't go into details about how Payload CMS nor this template works, but just know now we have 2 apps in this 1 project that you can customize however you want:&lt;/p&gt;

&lt;p&gt;The client facing e-commerce platform in &lt;code&gt;localhost:3000&lt;/code&gt;:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5mhe7g3c3a8unj0chnht.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5mhe7g3c3a8unj0chnht.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the admin dashboard in &lt;code&gt;localhost:3000/admin&lt;/code&gt;:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshfa9ysagswg8orqralp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshfa9ysagswg8orqralp.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Exploring Payload CMS E-commerce
&lt;/h2&gt;

&lt;p&gt;On the first run, follow the instructions to open the admin dashboard, create your first admin account, seed the database, and go try out the features Payload provides. Optionally, you can create a Stripe account and give it your sandbox mode API key to try out the payment flow. For my Indonesian fellows, you can create custom Payment methods for Xendit, Midtrans, and any other gateways by following this tutorial: &lt;a href="https://payloadcms.com/docs/ecommerce/payments#making-your-own-payment-adapter" rel="noopener noreferrer"&gt;Payment Adapters | Documentation | Payload&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When your database is seeded, you should now able to see the client e-commerce app is updated with new pages, mainly the Shop page:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyspxog9api2cthvnmrse.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyspxog9api2cthvnmrse.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can manage your inventory in the admin dashboard by opening the Products collection:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3wledm3b3kazftiozv17.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3wledm3b3kazftiozv17.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a customer, you can add items to cart, pick product variants, and pay it on the checkout page. As an admin, you can manage all of these data in the admin dashboard. It's really comprehensive for a e-commerce starter, but the template lacks shipping and activity logs, which you can add your own later.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting up Hermes Agent
&lt;/h2&gt;

&lt;p&gt;Follow the docs to &lt;a href="https://hermes-agent.nousresearch.com/docs/getting-started/quickstart" rel="noopener noreferrer"&gt;install your Hermes Agent&lt;/a&gt;. For this tutorial, I'll be using Nous Portal provider, WSL2, and Telegram Bot to communicate with the agent. I created a new WSL2 instance just for this tutorial with this command: &lt;code&gt;wsl --install Ubuntu --name PayloadHermesEcommerce&lt;/code&gt;, this prevents the agent from messing with my other existing WSLs. We can assume the agent is working if you can chat with it through the messaging gateway you setup (for me it's Telegram).&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwcl8tbtte8dal7g0dwsk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwcl8tbtte8dal7g0dwsk.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Integrating Hermes with Payload using MCP plugin
&lt;/h2&gt;

&lt;p&gt;Currently, our Hermes Agent can't really communicate with our Payload app. Sure, we could prompt it to learn our Payload app's API endpoints and make skills/tools for it, but a much more organized way is using MCP. We first need to setup &lt;a href="https://payloadcms.com/docs/plugins/mcp" rel="noopener noreferrer"&gt;Payload MCP Plugin&lt;/a&gt;, this gives us an organized method to control what our agent can access on our Payload app. Once the MCP server is setup, you can enable/disable which collections and other data your agent can utilize through the MCP. For this tutorial, I enable all the e-commerce collections.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt; &lt;span class="nf"&gt;mcpPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;addresses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;carts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;categories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;forms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;media&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;transactions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;variantOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;variantTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;form-submissions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, you need to configure the MCP client in Hermes Agent config. Open your &lt;code&gt;.hermes/config.yaml&lt;/code&gt; and add this section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;mcp_servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;PayloadHermesEcommerce&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http"&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://windows.local:3000/api/mcp"&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;API-KEY-HERE"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace your &lt;code&gt;API-KEY-HERE&lt;/code&gt; with the API key you get from Payload CMS API Keys collection in the admin dashboard that was created from the MCP plugin. &lt;/p&gt;

&lt;p&gt;Sidenote: your MCP setup might be different from mine, since I run my Hermes Agent on WSL2, I created a custom host &lt;code&gt;windows.local&lt;/code&gt; to target my Window host IP address and reach my Payload through &lt;code&gt;localhost:3000&lt;/code&gt;, if you run the Payload in another server or alongside where Hermes Agent is running, you might be able to just use localhost or target the Payload host directly.&lt;/p&gt;

&lt;p&gt;Now run your Payload app and call &lt;code&gt;/reload-mcp&lt;/code&gt; on your Hermes Agent. If it works, you should see a success message informing PayloadHermesEcommerce MCP has been added to the agent. I used PayloadHermesEcommerce for a more descriptive name. To test out that the MCP works, try asking your Hermes about what products your e-commerce have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What products does my e-commerce have?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhyr4tikwo9c3z06hhe5y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhyr4tikwo9c3z06hhe5y.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hermes will use the tools provided in the MCP and return products from your platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things you can do with the integration
&lt;/h2&gt;

&lt;p&gt;Now it's the fun part! With the MCP running, you are able to manage your Payload e-commerce platform in multiple ways. Let's try researching new products and adding it to the listing by sending this message to the agent through Telegram.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Research 3 Y2K styled clothing items and add them to our platform. Make sure each item has at least 1 image you download from the internet to the gallery and descriptive details. Make sure you fill out the SEO details completely.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmnqtn4t01cglhvcfqmsm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmnqtn4t01cglhvcfqmsm.png" alt=" "&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F07v6qukmeeneldra0rls.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F07v6qukmeeneldra0rls.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, let's make a few fake users and transactions, then create a PDF report about our store's performance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Please make 3 fake customers and 20 fake transactions with variative orders, status, and total. Then, create a PDF report about how our platform is performing and recommendations to improve our sales.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl8uaaxnqon09mehkhx5t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl8uaaxnqon09mehkhx5t.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are multitude of ways you can utilize this integration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setup cron job to research the market and update the product listing daily&lt;/li&gt;
&lt;li&gt;Research and write blog posts or promotion pages with good SEO for your products&lt;/li&gt;
&lt;li&gt;Analyze stale carts and automatically send personalized emails to remind the user&lt;/li&gt;
&lt;li&gt;Ask about transaction and orders data to create PDF/Excel reports&lt;/li&gt;
&lt;li&gt;Feed Hermes with your business information to build a FAQ database&lt;/li&gt;
&lt;li&gt;Integrate chatbot feature on the e-commerce client app with distinct personality&lt;/li&gt;
&lt;li&gt;Many more!
You can ask Hermes to do all of that just through your Telegram, WhatsApp, and any other messaging gateway you setup. This gives admins an easy way to operate your e-commerce platform.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;p&gt;I won't go into details on how to deploy this but since Payload CMS is a Next.js app, you can deploy it in Vercel and any other methods such as a VPS. For Hermes Agent, you can deploy it to a VPS and set up the Payload MCP to direct to your deployed Payload app. If you use another database such as Postgres, you need to also deploy and manage that. Since you own the source code and agent setup, you are able to optimize your infrastructure cost based on your needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AI agents are developing super quickly and its making building products and automating tasks accessible to a wider range of audience. By using Payload CMS e-commerce template with its MCP plugin capabilities, we can create an AI assistant with Hermes Agent that helps automate your e-commerce operations. Since you now own the e-commerce platform and AI agent, you can optimize your expenses.&lt;/p&gt;

</description>
      <category>payloadcms</category>
      <category>hermes</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>Reactively logout users on token refresh failure from Dio interceptors using RxDart in Flutter</title>
      <dc:creator>Elys Maldov</dc:creator>
      <pubDate>Sat, 31 Jan 2026 15:33:24 +0000</pubDate>
      <link>https://forem.com/elys_m/reactively-logout-users-on-token-refresh-failure-from-dio-interceptors-using-rxdart-in-flutter-399n</link>
      <guid>https://forem.com/elys_m/reactively-logout-users-on-token-refresh-failure-from-dio-interceptors-using-rxdart-in-flutter-399n</guid>
      <description>&lt;p&gt;Firebase auth should cover about 90% of a Flutter app's authentication needs, but what if you need to roll your own auth? Then, you'd need to manually handle trading and saving your JWT tokens from the backend to your app's secure storage. &lt;/p&gt;

&lt;p&gt;Now, your JWT should have an expiration date, that means you need to refresh your tokens periodically using the refresh token sent by your backend. All seems good until your user logs in to your app after 60 days, by then your access and refresh tokens should expire, and now you are getting 401 errors in your app. &lt;/p&gt;

&lt;p&gt;Your Dio refresh token interceptor should try to get new tokens using the saved refresh token, but if your refresh token is expired, that won't work. If the user is conscious enough, they would try to logout and login again to get new tokens, but wouldn't it be nicer if in cases of refreshing token failures, our app automatically logs the user out?&lt;/p&gt;

&lt;p&gt;This might be a rare occurrence, but it was a requirement I had to deal with for a recent project I was working on, &lt;a href="https://app.kawal.io/" rel="noopener noreferrer"&gt;Kawal&lt;/a&gt;. In this article, I'll explain how I handled refreshing JWT token failures by reactively logging out the user using RxDart. I'll also provide a Flutter app project to explain how the core system works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kawal Case Study
&lt;/h2&gt;

&lt;p&gt;The app handles custom JWT authentication using a straightforward flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User logs in&lt;/li&gt;
&lt;li&gt;Backend responds with an access and refresh token&lt;/li&gt;
&lt;li&gt;Store both of these using flutter secure storage&lt;/li&gt;
&lt;li&gt;On app startup, read from the secure storage&lt;/li&gt;
&lt;li&gt;If both tokens exist, then the user is authenticated&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since I’m binding the existence of those tokens to the users authenticated status, logging out could simply be clearing that token and redirecting the user to the login page. Before we move on, this is how the app's auth system is architected:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F77kxxyot8x1zvppicd9f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F77kxxyot8x1zvppicd9f.png" alt="diagram" width="800" height="129"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;RefreshTokenInterceptor&lt;/code&gt; handles refreshing access token by checking if any request responds with a 401 status from the backend. If it does, the interceptor will hit a refresh token endpoint to get and store the new tokens. But, there are situations where refreshing token could fail, for example when the server is down or the refresh token expires. My team agreed that if this happens, we would log the user out.&lt;/p&gt;

&lt;p&gt;Now I could have this setup to handle that logout:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcl2tujyo1pau4586ts4n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcl2tujyo1pau4586ts4n.png" alt="diagram" width="800" height="88"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since the existence of auth token is what sets the authentication status, clearing it should suffice. But, our app also have other logic to do during logout:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clearing shared preferences&lt;/li&gt;
&lt;li&gt;Calling Google Analytics
These methods are handled by the &lt;code&gt;AuthRepository.logout&lt;/code&gt; method, so the architecture really looks like this:
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw8giohr076u28awlf819.png" alt="diagram" width="800" height="234"&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since the &lt;code&gt;AuthRepository.logout&lt;/code&gt; method is the source-of-truth for logging out, now logging out just by clearing the secure storage in the refresh token interceptor is not sufficient. So why not just use &lt;code&gt;AuthRepository&lt;/code&gt; inside of the &lt;code&gt;RefreshTokenInterceptor&lt;/code&gt;? Well…&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5bnq079iyzwh6w1bz5jz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5bnq079iyzwh6w1bz5jz.png" alt="diagram" width="800" height="185"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now there’s a circular dependency: Dio is used by AuthService, which is used by AuthRepository, which is used by RefreshTokenInterceptor, which is used by Dio, which is used by AuthService… not good.&lt;/p&gt;

&lt;p&gt;One option is to just copy-paste the logout method in &lt;code&gt;AuthRepository&lt;/code&gt; to &lt;code&gt;RefreshTokenInterceptor&lt;/code&gt;, but I prefer to keep things DRY. So in this article, I’ll show you how you can handle this problem by making use of good ol’ RxDart as a signal for app to logout at any point we want.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tutorial: Getting user’s profile data using JWT
&lt;/h2&gt;

&lt;p&gt;I've built an example project using &lt;a href="https://fakeapi.platzi.com/en" rel="noopener noreferrer"&gt;Platzi Fake Store API | Platzi Fake Store API&lt;/a&gt; since it supports JWT auth. You can &lt;a href="https://github.com/ElysMaldov/flutter_reactive_logout_example" rel="noopener noreferrer"&gt;clone the repository here&lt;/a&gt; to read along the code and I'll explain the core mechanics for the rest of this article.&lt;/p&gt;

&lt;p&gt;The app has simple features:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Unauthenticated users must login/register&lt;/li&gt;
&lt;li&gt;Authenticated users are redirected to the home screen where they can see their auth-protected user profile&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Platzi API requires us to provide the JWT to get the user’s profile, so this part will be a good example of how we can handle reactive authentication.&lt;/p&gt;
&lt;h3&gt;
  
  
  RxDart Basic Concepts
&lt;/h3&gt;

&lt;p&gt;Before we dive in to the tutorial, we need to know a few basic concept of RxDart, mainly &lt;code&gt;BehaviorSubject&lt;/code&gt; and &lt;code&gt;PublishSubject&lt;/code&gt;. These classes are what we call Observables in reactive programming, basically they are “things” our app can “listen” to and based on what “thing” that happened, our app can run certain logic (listeners). &lt;/p&gt;

&lt;p&gt;These “things” we listen to are Streams. If you have ever used something like Kafka or Pub/Sub, its basically like how you can publish topics and your subscribers/consumers can react to it by triggering certain callbacks, for example calling a function, API endpoint, etc. With reactive programming, we can send (emit) values over time to the stream and our listeners can receive those values to do whatever they want.&lt;/p&gt;

&lt;p&gt;The main difference between BehaviorSubject and PublishSubject is BehaviorSubjects stores the last emitted value to the stream (stateful) while PublishSubjects don’t (ephemeral). So if a new listener comes in, they will instantly receive the last value; unlike PublishSubject that doesn’t store the last emitted value, so new listeners won’t know what happened last time.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zk51yufpyr24nnlm7gn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3zk51yufpyr24nnlm7gn.jpg" alt="Behavior subject" width="641" height="821"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftu45f3orybpc4btretxt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftu45f3orybpc4btretxt.jpg" alt="Publish subject" width="364" height="811"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can use these concepts to solve our reactive logout problem. Imagine if we only cleared the auth token from secure storage, then what? Flutter doesn’t know the user should be redirected to the login screen, we have to do it ourselves. But, redirection is a view layer concern, while our AuthRepository.logout method lives in the AuthRepository which should only concern business logic problems. That means, signaling our app to logout consists of 2 steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clearing the auth token from secure storage (data layer)&lt;/li&gt;
&lt;li&gt;Redirecting the user to login screen (view layer)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We’ll handle these steps by leveraging reactive programming, we’ll need to setup 2 streams: a stream built with BehaviorSubject to store the user's authentication status and a stream built with PublishSubject to signal our app to logout.&lt;/p&gt;
&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;

&lt;p&gt;This tutorial uses MVVM:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Felriokozb1tsd4cei1te.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Felriokozb1tsd4cei1te.png" alt="diagram" width="736" height="70"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each layer handles different concerns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Services: directly calls the Platzi API and returns DTOs (Data Transfer Objects)&lt;/li&gt;
&lt;li&gt;Repositories: utilizes multiple services and streams to build the business logic&lt;/li&gt;
&lt;li&gt;Cubits: our ViewModel to abstract away our views’ logic by orchestrating multiple repositories and store state&lt;/li&gt;
&lt;li&gt;Views: screens that will simply present the data from our cubits states&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We’ll also spice things up by introducing dependency injection using get_it. This might seem overkill for this simple app, but I find this setup as a good basis when managing my app that has a lot of business logic and screens going around.&lt;/p&gt;
&lt;h3&gt;
  
  
  Create AppSignal to emit logout signal
&lt;/h3&gt;

&lt;p&gt;We need a signal that is accessible to any layer of our app without causing a circular dependency. We can fix this easily by creating a dependency called &lt;code&gt;AppSignal&lt;/code&gt; that stores any global signals our app can emit/listen to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/data/core/app_signals.dart&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:logging/logging.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:rxdart/rxdart.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppSignals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;_log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'AppSignals'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;_logoutSignalSubject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PublishSubject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Null&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
  &lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Null&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;logoutSignalStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_logoutSignalSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;emitLogoutSignal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Emitting logout signal'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_logoutSignalSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Logout signal emitted'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We expose a &lt;code&gt;logoutSignalStream&lt;/code&gt; that will be listened to by our app to trigger &lt;code&gt;AuthRepository.logout&lt;/code&gt; method. With the &lt;code&gt;emitLogoutSignal&lt;/code&gt;, we can signal our app to logout anytime and anywhere, since this dependency is a core dependency that can be used on any layer of our app that has access to get_it/any other DI framework you implement.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6igesoncn7oc0mobmglg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6igesoncn7oc0mobmglg.png" alt="diagram" width="800" height="171"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Listen to logoutSignalStream in AuthRepository
&lt;/h3&gt;

&lt;p&gt;I placed my logout method in AuthRepository, alongside a BehaviorSubject to store the user's current auth status. The logout method will be responsible to clear the auth token, update the user's auth status stream, and do other things such as sending analytics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/data/repositories\auth_repository.dart&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/data/core/app_error.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/data/core/app_signals.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/data/services/auth_service.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/data/storage/secure_storage.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/domain/enums/auth_status.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/domain/models/auth_token.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/domain/models/login_request.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/domain/models/register_request.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/domain/models/user.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:logging/logging.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:rxdart/rxdart.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthStatus&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;authStatusStream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;AuthStatus&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;currentAuthStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RegisterRequest&lt;/span&gt; &lt;span class="n"&gt;registerRequest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LoginRequest&lt;/span&gt; &lt;span class="n"&gt;loginRequest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;getUserProfile&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthRepositoryRemote&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;AuthRepository&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Logger&lt;/span&gt; &lt;span class="n"&gt;_log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'AuthRepositoryRemote'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;AuthService&lt;/span&gt; &lt;span class="n"&gt;_authService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;SecureStorage&lt;/span&gt; &lt;span class="n"&gt;_secureStorage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;AppSignals&lt;/span&gt; &lt;span class="n"&gt;_appSignals&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthStatus&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_authStatusSubject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthStatus&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;seeded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Stream&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthStatus&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;authStatusStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_authStatusSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;AuthStatus&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;currentAuthStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_authStatusSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;AuthRepositoryRemote&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;AuthService&lt;/span&gt; &lt;span class="n"&gt;authService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;SecureStorage&lt;/span&gt; &lt;span class="n"&gt;secureStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;AppSignals&lt;/span&gt; &lt;span class="n"&gt;appSignals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_authService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;_secureStorage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secureStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;_appSignals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;appSignals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="n"&gt;_listenToLogoutSignal&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_listenToLogoutSignal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_appSignals&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;logoutSignalStream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Logout signal received, performing logout'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Logging out user'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_secureStorage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clearAuthToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="c1"&gt;// TODO do other things (send analytics, clear shared preferences, etc.)&lt;/span&gt;
      &lt;span class="n"&gt;_authStatusSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;unauthenticated&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;_log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Logout successful'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;appError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_mapToAppError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;_log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;severe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Logout failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;appError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;_authStatusSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;unauthenticated&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;appError&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During the construction of the class, we depend on the AppSignals and listen to its logoutSignalStream to trigger the logout method. Later, the rest of our app can emit a logout signal and our AuthRepository will handle logging out. This is much cleaner, since if we ever need to modify the logout method, we just have to modify it in this one repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Emit logout signal in refresh token interceptor
&lt;/h3&gt;

&lt;p&gt;We'll handle refreshing the user's token using a Dio interceptor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/config/dio/interceptors/refresh_token_interceptor.dart&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:dio/dio.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/config/api_routes.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/data/core/app_error.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/data/core/app_signals.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/data/storage/secure_storage.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/domain/models/auth_token.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:logging/logging.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;/// Uses [QueuedInterceptorsWrapper] so if multiple request fails at once, we refresh&lt;/span&gt;
&lt;span class="c1"&gt;/// the first one before retrying the rest using the new token&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RefreshTokenInterceptor&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;QueuedInterceptorsWrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;Dio&lt;/span&gt; &lt;span class="n"&gt;_dio&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;SecureStorage&lt;/span&gt; &lt;span class="n"&gt;_storage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;AppSignals&lt;/span&gt; &lt;span class="n"&gt;_appSignals&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;_log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'RefreshTokenInterceptor'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="n"&gt;RefreshTokenInterceptor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;Dio&lt;/span&gt; &lt;span class="n"&gt;dio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;SecureStorage&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;AppSignals&lt;/span&gt; &lt;span class="n"&gt;appSignals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_dio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;_storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;_appSignals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;appSignals&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;DioException&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ErrorInterceptorHandler&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;isUnauthorized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// Avoid infinite loop if the refresh-token endpoint itself fails&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;isRefreshRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestOptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;ApiRoutes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isUnauthorized&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isRefreshRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;_log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Token expired! Refreshing token...'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// ... handle refreshing logic&lt;/span&gt;

      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;severe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Refresh token failed. Logging user out'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;appError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AppError&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Important piece to signal logout for our app&lt;/span&gt;
        &lt;span class="n"&gt;_appSignals&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;emitLogoutSignal&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;DioException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;requestOptions:&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;error:&lt;/span&gt; &lt;span class="n"&gt;appError&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how it wraps the refreshing logic in a try/catch, and the catch block calls &lt;code&gt;_appSignals.emitLogoutSignal()&lt;/code&gt;. Whenever  the refresh token logic fails, we can assume the user should logout to get new tokens. By calling that method, we'll trigger the listener in &lt;code&gt;AppRepository&lt;/code&gt; earlier that calls the logout method. Side note, this interceptor only runs if your request encounters an error, specifically a 401 error. My team agreed that encountering a 401 status means the user should try to refresh their tokens, but your requirement may vary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reactively navigate using GoRouter's refreshListenable
&lt;/h3&gt;

&lt;p&gt;So far we've only managed the auth status in the data layer, but haven't really tied it with the app's actual navigation to the login screen. We can use GoRouter's refreshListenable to listen to a Listenable and trigger navigation whenever that Listenable changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/routing/router.dart&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/material.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_bloc/flutter_bloc.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/domain/enums/auth_status.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:go_router/go_router.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/config/dependencies.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/data/repositories/auth_repository.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/ui/home/cubit/home_cubit.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/ui/home/widgets/home_screen.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/ui/login/cubit/login_cubit.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/ui/login/widgets/login_screen.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/ui/register/cubit/register_cubit.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/ui/register/widgets/register_screen.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/ui/splash/splash_screen.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'routes.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;/// AppRouter class that configures go_router with authentication guard and reactive navigation&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppRouter&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ChangeNotifier&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;AppRouter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_setupAuthListener&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;AuthRepository&lt;/span&gt; &lt;span class="n"&gt;_authRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="n"&gt;DependencyManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;/// Listen to any stream from any repositories to reactively update the user's navigation&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;_setupAuthListener&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_authRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authStatusStream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;notifyListeners&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;/// Creates the GoRouter instance with auth guard and route configuration&lt;/span&gt;
  &lt;span class="n"&gt;GoRouter&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GoRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;initialLocation:&lt;/span&gt; &lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;splashScreen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;redirect:&lt;/span&gt; &lt;span class="n"&gt;_handleRedirect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;routes:&lt;/span&gt; &lt;span class="n"&gt;_routes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;refreshListenable:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;/// Auth guard redirect logic&lt;/span&gt;
  &lt;span class="c1"&gt;/// Listens to authStatusStream and redirects based on authentication state&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_handleRedirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;GoRouterState&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;authRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DependencyManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;authStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentAuthStatus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Define public routes that don't require authentication&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;isPublicRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;login&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Handle redirect based on auth status&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authStatus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;AuthStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authenticated&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;splashScreen&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;isPublicRoute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;AuthStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;unauthenticated&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;isPublicRoute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;login&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;AuthStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;/// Route definitions&lt;/span&gt;
  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GoRoute&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;_routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;GoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;splashScreen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s"&gt;'splash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SplashScreen&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;GoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;login&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s"&gt;'login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BlocProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;create:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;LoginCubit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;authRepository:&lt;/span&gt; &lt;span class="n"&gt;DependencyManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()),&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;LoginScreen&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;GoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s"&gt;'register'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BlocProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;create:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RegisterCubit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;authRepository:&lt;/span&gt; &lt;span class="n"&gt;DependencyManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;RegisterScreen&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;GoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s"&gt;'home'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BlocProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;create:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;HomeCubit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;authRepository:&lt;/span&gt; &lt;span class="n"&gt;DependencyManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AuthRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()),&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HomeScreen&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The way I did it is by wrapping the router in a class that extends ChangeNotifier. This effectively turns &lt;code&gt;this&lt;/code&gt; into a Listenable that the router listens to. This allows us to listen to multiple streams and notify the Listenable (such as the &lt;code&gt;_setupAuthListener&lt;/code&gt; method), that will then, trigger the redirect method of the router. The &lt;code&gt;_handleRedirect&lt;/code&gt; method will be triggered whenever we notify something through a stream listener or any other method inside of the class.&lt;br&gt;
We can then wrap the router around the main app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/main.dart&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/material.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/config/dependencies.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_reactive_logout_example/routing/router.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:logging/logging.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;WidgetsFlutterBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ensureInitialized&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Level&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ALL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;Logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onRecord&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${record.level.name}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;${record.time}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;${record.message}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;DependencyManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MainApp&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainApp&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MainApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AppRouter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;router&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;routerConfig:&lt;/span&gt; &lt;span class="n"&gt;appRouter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="s"&gt;'Flutter Reactive Logout Example'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;debugShowCheckedModeBanner:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we have finished tying together our logout signal to our app's navigation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Refresh token fails, emit logout signal in &lt;code&gt;RefreshTokenInterceptor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AuthRepository&lt;/code&gt; listen to logout signal to call &lt;code&gt;logout&lt;/code&gt;, which will update our &lt;code&gt;authStatusStream&lt;/code&gt; into unauthenticated&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AppRouter.router&lt;/code&gt; listens to the auth status change, and redirect user to the login page since they are now unauthenticated&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  That's it!
&lt;/h2&gt;

&lt;p&gt;By leveraging RxDart, we can store the user's auth status and signal our app to logout by exposing streams. Our app signals can be listened to by any layer of our app since its a core dependency. We utilize this by emitting logout signal when token refresh fails, which will reactively update our auth status and redirec the user to the login screen.&lt;br&gt;
You should run &lt;a href="https://github.com/ElysMaldov/flutter_reactive_logout_example" rel="noopener noreferrer"&gt;the demo app&lt;/a&gt; to see how the interceptor and stream works. There is a demo logout button in &lt;code&gt;home_screen.dart&lt;/code&gt;  that will always trigger the &lt;code&gt;RefreshTokenInterceptor&lt;/code&gt; catch block.&lt;/p&gt;

&lt;p&gt;Have you ever encountered a similar situation where you need to logout the user when the token refresh fails? I'd love to hear your experiences in the comments!&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>mobile</category>
      <category>rxdart</category>
      <category>authentication</category>
    </item>
    <item>
      <title>[DEVLOG] Payload CMS Booster: Day 1 - 2</title>
      <dc:creator>Elys Maldov</dc:creator>
      <pubDate>Tue, 13 Jan 2026 16:19:45 +0000</pubDate>
      <link>https://forem.com/elys_m/devlog-payload-cms-booster-day-1-2-2oed</link>
      <guid>https://forem.com/elys_m/devlog-payload-cms-booster-day-1-2-2oed</guid>
      <description>&lt;p&gt;I've been using Payload CMS a lot, and I believe it's becoming a pretty hot emerging tech in the Next.js space. As someone who's used to building APIs in Express and developing admin dashboards in React from scratch, it gets pretty tedious when basic APIs and logic (like CRUD-ing a table or handling login screens) get repeated over and over.&lt;/p&gt;

&lt;p&gt;Sure, I've used Prisma, Drizzle, tRPC, admin dashboard templates, and the good ol' CTRL+C CTRL+V, but still having to separately handle the backend and frontend for a feature felt like it could be easier. So you should see my face when I first tried out Payload CMS with its config-based approach of building the frontend, backend, and even the data layer of an app. It's so much quicker to declare a Payload collection, which is then generated as a frontend in the admin dashboard, backend APIs, and Drizzle schemas with TypeScript interfaces for me to use instantly. This process would've taken me hours or even days if I were to use my good ol' MERN stack, but with Payload its just instant!&lt;/p&gt;

&lt;p&gt;I have no qualms with Payload's DX (though the documentation for advanced use-cases does leave much to be desired), but I'm starting to feel friction when working on a large Payload project with about 20+ collections. My biggest Payload project, &lt;a href="https://akasiaedu.com/en" rel="noopener noreferrer"&gt;Akasia Education&lt;/a&gt;, has about 30+ collections with some of them having complex relationships/join fields to multiple other collections. Having an ERD does help, but it's hard for me to actually visualize how it's implemented in Payload CMS. I would have to read each payload collection individually, see their relationship/join field, and imagine it in my head if it fits with my ERD or not. I even encountered errors that were hard to debug due to my collections having multiple relationships with other collections, where those collections have relationships with other collections, and so forth.&lt;/p&gt;

&lt;p&gt;Usually, finding where a piece of variable/function/class is used on a project is easy in an IDE with features like go-to definitions or find all references. But finding where a Payload collection is referenced by other collections is more difficult since relationship/join fields use another collection's slug, which are just strings that cannot be used for go-to definitions/find all references in IDEs. So, being inspired by &lt;a href="https://www.codecanvas.app/" rel="noopener noreferrer"&gt;Code Canvas&lt;/a&gt; extension and always wanting to make a VSCode extension on my own, is it possible to build a Payload CMS collection visualizer where I can simply see how each collection relates to one another?&lt;/p&gt;

&lt;h1&gt;
  
  
  The idea
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fik9sn98k1o95jmmfixwx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fik9sn98k1o95jmmfixwx.png" alt=" " width="800" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's super simple: get all Payload collection in a project, analyze their relationship, and visualize them in a canvas like above. Having something like this would really help me visualize my big Payload project without having to take out a pen-and-pencil or always compare it againts my ERD.&lt;/p&gt;

&lt;h1&gt;
  
  
  Making sure it's possible
&lt;/h1&gt;

&lt;p&gt;With nothing else to do in my free time, I just started building this extension idea. The main idea is that my extension can be activated on a Payload CMS project, and it will traverse the collections to build a visualizer to show how each collection relates to the others. Basically, I want to make something like Code Canvas, but for Payload collections.&lt;/p&gt;

&lt;p&gt;Obivously the first thing I need to ensure is if it's even possible. Well, if Code Canvas is possible, then of course this idea should also be. Specifically, I need to make sure how I would implement this and what stack I need.&lt;/p&gt;

&lt;p&gt;Reading VSCode extension development docs, it is possible by making a &lt;a href="https://code.visualstudio.com/api/extension-guides/webview" rel="noopener noreferrer"&gt;webview-based extension&lt;/a&gt;. Frankly, there's not much &lt;em&gt;structured&lt;/em&gt; resource on how to build a webview VSCode extension, but after some digging, I found 2 good repositories to serve as a boilerplate: &lt;a href="https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/frameworks/hello-world-react-vite" rel="noopener noreferrer"&gt;Microsoft's official example&lt;/a&gt; and &lt;a href="https://github.com/QiyuanChen02/react-webview" rel="noopener noreferrer"&gt;QiyuanChen02 repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ultimately, I chose QiyuanChen02's repo since its newer, but these 2 repo provides a boilerplate for VSCode webview extension using React + Vite and hot-reload. The gist of the development flow is I just build a normal React app whose built code will be rendered as a webview in VSCode, and I can pass messages between VSCode and my webview using &lt;a href="https://code.visualstudio.com/api/extension-guides/webview#scripts-and-message-passing" rel="noopener noreferrer"&gt;message passing&lt;/a&gt;. Next is how to render the visualizer. I chose &lt;a href="https://reactflow.dev/" rel="noopener noreferrer"&gt;React Flow&lt;/a&gt; since it's super popular for this.&lt;/p&gt;

&lt;p&gt;Now that the basic prerequisite is possible for me to build the extension, it's time to actually build the extension and not abandon it as another side project.&lt;/p&gt;

&lt;h1&gt;
  
  
  Actually building the extension
&lt;/h1&gt;

&lt;p&gt;I forked QiyuanChen02's repo to quickly bootstrap my extension project here: &lt;a href="https://github.com/ElysMaldov/payload-cms-booster" rel="noopener noreferrer"&gt;https://github.com/ElysMaldov/payload-cms-booster&lt;/a&gt;. I called it Payload CMS Booster because I have a few other ideas for this extension to boost working with Payload in VSCode. It just works by running the debug mode (F5), it will open up another VSCode window with my custom command to show the React app in a webview. I added a new launch JSON that starts the debug VSCode window in a clean slate by disabling all other extensions except the one I'm developing and also opening an existing Payload project to test it later; I placed this new debug config in the &lt;code&gt;launch.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the folder structure, it's quite straightforward. I have a "root" project with the main entrypoint being the &lt;code&gt;src/extension.ts&lt;/code&gt;. This is where I register my webview to the extension. The webview takes in the &lt;code&gt;dist&lt;/code&gt; folder of the React webview subproject in the &lt;code&gt;webview&lt;/code&gt; folder. I have 2 projects, each having its own dependencies. For now, this works, but I could potentially turn it into a monorepo like how &lt;a href="https://github.com/Kilo-Org/kilocode" rel="noopener noreferrer"&gt;Kilo Code&lt;/a&gt; does it.&lt;/p&gt;

&lt;p&gt;To run the project, I need to run the dev server in the &lt;code&gt;webview&lt;/code&gt; project and start a debugger to open up another VSCode window. In that new window, I can access the command to bring up the webview (currently the command is  "Payload CMS Booster: Visualize" specified in the root package.json or VSCode calls it extension manifest). Then I can just work on the webview project like a regular React + Vite project with HMR working.&lt;/p&gt;

&lt;p&gt;The latest progress I made with this repo is setting up an example React Flow component. I haven't really thought much about how to architect this project but I'll try applying parts of MVVM since I've been working on a lot of Flutter these days; mainly I need to separate between the data and UI layer, and use domain models a lot so I don't have to worry much about how to pass my actual payload collection data from VSCode since I can just assume that's a DTO I need to map to my domain model. &lt;/p&gt;

&lt;p&gt;So my next step is how can I traverse all of the Payload collection in a project, analyze their relationships with each other, and structure it for my webview and finally render it in React Flow? The idea I have so far is to read the payload.config.ts, iterate through each collection to collect their slug, and build their relationship based on the existence of either a relationship/join field in that collection. This seems okay so far since a relationship/join field refers to another collection using their unique slug, so I could use this as an id for each collection. I thought about using the generated payload-types.ts file, but it seems wayyyy more complicated than a regular Payload collection file.&lt;/p&gt;

&lt;p&gt;I'll update again soon 😸.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6a722o4gjacxzyxpa9sy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6a722o4gjacxzyxpa9sy.png" alt=" " width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>vscode</category>
      <category>payloadcms</category>
      <category>extensions</category>
    </item>
  </channel>
</rss>
