<?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>DEV Community: Ehan Siddique</title>
    <description>The latest articles on DEV Community by Ehan Siddique (@ehan_siddique).</description>
    <link>https://dev.to/ehan_siddique</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4087026%2Fa3ca6c2d-02f7-4bec-8b75-31e0db7587b8.PNG</url>
      <title>DEV Community: Ehan Siddique</title>
      <link>https://dev.to/ehan_siddique</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ehan_siddique"/>
    <language>en</language>
    <item>
      <title>Farmora: my Next.js shop stalled up to 6s a page, so I made it local-first</title>
      <dc:creator>Ehan Siddique</dc:creator>
      <pubDate>Wed, 14 Oct 2026 12:56:25 +0000</pubDate>
      <link>https://dev.to/ehan_siddique/farmora-my-nextjs-shop-stalled-up-to-6s-a-page-so-i-made-it-local-first-3h9o</link>
      <guid>https://dev.to/ehan_siddique/farmora-my-nextjs-shop-stalled-up-to-6s-a-page-so-i-made-it-local-first-3h9o</guid>
      <description>&lt;h2&gt;
  
  
  What Farmora is
&lt;/h2&gt;

&lt;p&gt;Farmora ("From Local Farms to Your Door") is a farm-to-door marketplace. Farmers list what they've harvested and customers buy straight from them. Every order is delivered within three days, with a live tracker. Behind the shop there's an admin side where the platform keeps a 10% commission on each sale.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://farmora-smoky.vercel.app" rel="noopener noreferrer"&gt;https://farmora-smoky.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/Fehan999/Farmora" rel="noopener noreferrer"&gt;https://github.com/Fehan999/Farmora&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Fehan999" rel="noopener noreferrer"&gt;
        Fehan999
      &lt;/a&gt; / &lt;a href="https://github.com/Fehan999/Farmora" rel="noopener noreferrer"&gt;
        Farmora
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Farmora 🌱&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;From Local Farms to Your Door&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Farmora is a farm-to-table marketplace I built with Next.js. Farmers list what they've harvested, customers buy it straight from them, and every order arrives within three days with a live tracker that follows it from the farm to the front door. Behind that there's an admin side where the platform keeps a 10% commission on each sale. It started as a portfolio project, but I wanted it to behave like a real shop: real sign-ups, a real database, photo uploads for farmers, and pages that still load when the backend is having a bad day.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://farmora-smoky.vercel.app" rel="nofollow noopener noreferrer"&gt;farmora-smoky.vercel.app&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Try it&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;No sign-up and no passwords needed. Open the &lt;a href="https://farmora-smoky.vercel.app/login" rel="nofollow noopener noreferrer"&gt;login page&lt;/a&gt; and:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pick &lt;strong&gt;Customer&lt;/strong&gt; or &lt;strong&gt;Farmer&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;View demo account&lt;/strong&gt; to jump straight in, or &lt;strong&gt;Create your own account&lt;/strong&gt; if you'd rather start fresh.&lt;/li&gt;
&lt;li&gt;Curious about the business side? Click &lt;strong&gt;View admin&lt;/strong&gt;…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Fehan999/Farmora" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;I designed and built it on my own. It has 27 page routes plus 6 blog article pages, 40 products in 8 categories, 8 partner farms with full profiles, 12 sample customers and 18 sample orders. All of it is sample data. There are no real farms, customers or payments behind it.&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%2Fraw.githubusercontent.com%2FFehan999%2FFarmora%2FHEAD%2Fscreenshots%2Fhome.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%2Fraw.githubusercontent.com%2FFehan999%2FFarmora%2FHEAD%2Fscreenshots%2Fhome.jpg" alt="Farmora home page with the hero, product categories and featured products" width="800" height="2865"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are three roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Customers&lt;/strong&gt; search and filter the shop and fill a cart. Delivery is free over $50 and $4.99 otherwise. They can apply coupons and check out with a demo card or cash on delivery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Farmers&lt;/strong&gt; get a dashboard with gross sales, the 10% platform fee, net earnings, a 7-day earnings chart and a product editor with photo upload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admins&lt;/strong&gt; see platform revenue, a sales table with CSV export, product moderation, user management and a contact inbox. They don't get a cart or "My Orders", because they aren't shoppers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I wanted a portfolio project that behaves like a real shop, and a marketplace forces questions a single-vendor store never has to answer. Who can see which order? What counts as the platform's revenue? What does a farmer see that an admin doesn't? What happens when the backend is down?&lt;/p&gt;

&lt;p&gt;The farm angle gave every product a source, too. Each item shows the farm it came from, and each farm has its own profile page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;I kept the stack small on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 14.2 with the App Router and React 18, in plain JSX (no TypeScript)&lt;/li&gt;
&lt;li&gt;Tailwind CSS 3&lt;/li&gt;
&lt;li&gt;Firebase 10 for Authentication, Firestore and Analytics&lt;/li&gt;
&lt;li&gt;Supabase Storage for farmer photo uploads&lt;/li&gt;
&lt;li&gt;Pexels CDN for product photos&lt;/li&gt;
&lt;li&gt;Vercel for hosting&lt;/li&gt;
&lt;li&gt;Playwright and GitHub Actions for screenshots and image checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seed farms, products, orders and photo ids live in &lt;code&gt;data/&lt;/code&gt;. Most of the code in this post is in &lt;code&gt;lib/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A data layer that never makes the page wait
&lt;/h2&gt;

&lt;p&gt;In the first version, &lt;code&gt;getProducts()&lt;/code&gt; started with &lt;code&gt;await readCollection("products")&lt;/code&gt;, and the page sat on a loading state until that came back. Now every getter in &lt;code&gt;lib/db.js&lt;/code&gt; answers immediately. It merges three sources by id: the seed data, the last Firestore snapshot cached in &lt;code&gt;localStorage&lt;/code&gt;, and any changes made in this browser. Firestore only refreshes that data in the background.&lt;/p&gt;

&lt;p&gt;Every Firestore call in &lt;code&gt;db.js&lt;/code&gt; goes through a timeout, now 4 seconds instead of 6:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;withTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;race&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rej&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;rej&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&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;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;))]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;remote()&lt;/code&gt; returns whatever is cached right now. If that cache is more than a minute old, it also starts a refresh. &lt;code&gt;refresh()&lt;/code&gt; does the fetch, but skips it when Firestore is already known to be down, when the collection is locked, or when a request for that collection is already in flight:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isBrowser&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;remoteDown&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;deniedCollections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;inflight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getDb&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;snap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;withTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDocs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
      &lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CACHE_PREFIX&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;notify&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;markFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;inflight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&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="nx"&gt;inflight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isBrowser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CACHE_PREFIX&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entry&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="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;REMOTE_TTL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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;&lt;code&gt;getProducts()&lt;/code&gt; merges the three sources in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mergeById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PRODUCTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;farmora_products&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Failures go one of two ways. A &lt;code&gt;permission-denied&lt;/code&gt; on one collection keeps only that collection local. Anything else, like a timeout or a disabled API, marks Firestore as down for the rest of the session, so no other page waits out the same four seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&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="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;permission-denied&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="nx"&gt;deniedCollections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;remoteDown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STATUS_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&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="k"&gt;catch&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;When fresh data arrives, &lt;code&gt;notify()&lt;/code&gt; fires a window event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;notify&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="nx"&gt;isBrowser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;farmora:data&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the React side, &lt;code&gt;components/useData.js&lt;/code&gt; listens for that event and re-runs the page's loader in silent mode, so the page updates without a spinner. A request counter drops the result of an older run that finishes late:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;silent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;requestRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;silent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setLoading&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;loaderRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&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="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;requestRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&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="nx"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;run&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;farmora:data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;farmora:data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&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="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The home page, product pages and farm profiles pass seed data to &lt;code&gt;useData&lt;/code&gt; as the initial value, so their first render already has content. Writes follow the same pattern. They're saved to &lt;code&gt;localStorage&lt;/code&gt; first, then sent to Firestore in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Loading Firebase when the browser is idle
&lt;/h2&gt;

&lt;p&gt;The other cost was bundle size. The first &lt;code&gt;lib/firebase.js&lt;/code&gt; imported &lt;code&gt;firebase/auth&lt;/code&gt; and &lt;code&gt;firebase/firestore&lt;/code&gt; at the top of the file, so every page shipped the SDK whether it used it or not. In the build output, that was about half of the shared JavaScript on every page.&lt;/p&gt;

&lt;p&gt;Now each service is a cached promise around a dynamic &lt;code&gt;import()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getDb&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;dbPromise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;dbPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;getFirebaseApp&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;firebase/firestore&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFirestore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;sdk&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="nx"&gt;dbPromise&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 auth listener in &lt;code&gt;context/AuthContext.jsx&lt;/code&gt; doesn't start until the browser is idle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestIdleCallback&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nf"&gt;idle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;start&lt;/code&gt; prefetches the products, users and orders collections, starts Analytics and subscribes to &lt;code&gt;onAuthStateChanged&lt;/code&gt;. The saved session in &lt;code&gt;localStorage&lt;/code&gt; is read before any of that, so a returning user sees their name in the navbar straight away.&lt;/p&gt;

&lt;p&gt;Shared first-load JS per page went from about 215 KB to about 106 KB.&lt;/p&gt;

&lt;p&gt;Supabase got the same treatment. &lt;code&gt;getSupabase()&lt;/code&gt; in &lt;code&gt;lib/supabase.js&lt;/code&gt; is a lazy &lt;code&gt;import("@supabase/supabase-js")&lt;/code&gt;, so the client only downloads when a farmer uploads a photo.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Commission math in one place
&lt;/h2&gt;

&lt;p&gt;The business rule is short. Farmora keeps 10% of the product subtotal (price × quantity) of every order that wasn't cancelled. Delivery fees and tax don't count. An order with $42.00 of produce earns the platform $4.20 and the farmer $37.80.&lt;/p&gt;

&lt;p&gt;My first admin dashboard got this wrong. Its "Total revenue" card added up full order totals, delivery and tax included. The platform never keeps that money, so the biggest number on the page was misleading.&lt;/p&gt;

&lt;p&gt;Now the rate lives in one constant in &lt;code&gt;lib/format.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;COMMISSION_RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;itemsSubtotal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commissionOf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;COMMISSION_RATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;lib/stats.js&lt;/code&gt; builds every order-level number from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FEE_PCT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;COMMISSION_RATE&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PAYOUT_PCT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;FEE_PCT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isActiveOrder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cancelled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orderGross&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;itemsSubtotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orderCommission&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;commissionOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;orderGross&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orderPayout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;orderGross&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;orderCommission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The admin overview, the 7-day commission chart, commission by category, the Sales table, its CSV export and the farmer dashboard all read from these two files, so the numbers always agree. Even the labels use &lt;code&gt;FEE_PCT&lt;/code&gt;. Change the rate and the "Commission (10%)" column header changes with it.&lt;/p&gt;

&lt;p&gt;The admin side still shows sales volume and farmer payouts, labelled as context so nobody mistakes them for revenue.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpe2djg5zyrjn9cuvvtup.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpe2djg5zyrjn9cuvvtup.jpg" alt="Admin dashboard with the 10% platform commission as the headline number and a 7-day commission chart" width="800" height="1038"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A delivery tracker with no background job
&lt;/h2&gt;

&lt;p&gt;Every order moves through five stages over 72 hours. I didn't want a cron job or a cloud function moving statuses along, so &lt;code&gt;lib/delivery.js&lt;/code&gt; works out the stage from the order's timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DELIVERY_HOURS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STAGES&lt;/span&gt; &lt;span class="o"&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;placed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Order Placed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;📝&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;packed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Packed at Farm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;📦&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shipped&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Shipped&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;🚚&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;out&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Out for Delivery&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;🛵&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delivered&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Delivered&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;🏡&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deliveryStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;created&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="nx"&gt;e5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;percent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hours&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;DELIVERY_HOURS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;stageIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;STAGES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="nx"&gt;hours&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;stageIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&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 &lt;code&gt;DeliveryTracker&lt;/code&gt; component updates &lt;code&gt;now&lt;/code&gt; every 30 seconds, which moves the progress bar and the countdown.&lt;/p&gt;

&lt;p&gt;Because it's plain math on a timestamp, no server has to keep it current. Sample orders work too, since the seed creates them relative to the current time. And the public &lt;code&gt;/track&lt;/code&gt; page can show any order's progress from its order number, with no login. Customers can cancel until the order ships, and each order has a printable invoice.&lt;/p&gt;

&lt;p&gt;The trade-off is that a stage isn't a real event. A real delivery system would need scans from the farm and the courier, and the timestamp math would only be the estimate shown until those scans arrive.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Form3jhhcygsm0awzpt8a.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Form3jhhcygsm0awzpt8a.jpg" alt="Order page with the live 3-day delivery tracker, countdown and five delivery stages" width="800" height="829"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Images: srcset, fallbacks, and a workflow that checks every photo
&lt;/h2&gt;

&lt;p&gt;The first version used placeholder images from a random-image service. They were slow and often showed the wrong food, so I replaced them with 125 curated Pexels photos.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;data/photos.js&lt;/code&gt; stores the Pexels ids. &lt;code&gt;lib/images.js&lt;/code&gt; builds a &lt;code&gt;srcset&lt;/code&gt; from each URL, so a phone never downloads a desktop-sized photo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WIDTHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;320&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;480&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;640&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;960&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;srcSetFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&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="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;isPexels&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;?&amp;amp;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;w=&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&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;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;WIDTHS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;w`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SafeImage&lt;/code&gt; wraps every product photo. It lazy-loads unless the image is marked &lt;code&gt;priority&lt;/code&gt;, which the hero images are. When a photo fails, it falls back in two steps: first to an optional &lt;code&gt;fallbackSrc&lt;/code&gt;, then to an inline SVG placeholder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setStage&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&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="nx"&gt;src&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;fallbackSrc&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&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;Fallbacks hide broken images from me as well as from visitors. So I added a GitHub Action that builds the site, starts it and runs &lt;code&gt;scripts/screenshots.mjs&lt;/code&gt; with Playwright.&lt;/p&gt;

&lt;p&gt;The script screenshots every page, with the right demo session in place where a page needs one. It also requests every photo URL in &lt;code&gt;data/photos.js&lt;/code&gt;. A photo passes only on a 200 response whose content type, if there is one, is an image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/&lt;/span&gt;&lt;span class="dl"&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 results go to &lt;code&gt;image-report.json&lt;/code&gt;. The script also renders contact sheets: grids of every photo, each captioned with the product or page it belongs to, with failures outlined in red. The workflow uploads all of it as an artifact and pushes the screenshots to a separate &lt;code&gt;screenshots&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;The URL check caught 10 broken photo URLs.&lt;/p&gt;

&lt;p&gt;The contact sheets caught something a status code never will: several photos that loaded fine but didn't match their product. When I couldn't find a matching photo, I renamed the product rather than show a misleading picture. "Rainbow Carrots" became "Farm Fresh Carrots".&lt;/p&gt;

&lt;h2&gt;
  
  
  6. SEO with JSON-LD and generated OG images
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;pageMeta()&lt;/code&gt; helper in &lt;code&gt;lib/seo.js&lt;/code&gt; gives each public page its own title, description, canonical URL and Open Graph/Twitter tags.&lt;/p&gt;

&lt;p&gt;The root layout adds a JSON-LD graph with &lt;code&gt;WebSite&lt;/code&gt; (including a &lt;code&gt;SearchAction&lt;/code&gt; that points at &lt;code&gt;/shop?q=&lt;/code&gt;), &lt;code&gt;Organization&lt;/code&gt;, &lt;code&gt;Person&lt;/code&gt; for me as the author, and &lt;code&gt;SoftwareSourceCode&lt;/code&gt; linking to the repo.&lt;/p&gt;

&lt;p&gt;Product pages get a &lt;code&gt;Product&lt;/code&gt; graph with offers, price, stock, delivery time and rating, plus a breadcrumb list. The product page itself is a client component, so the JSON-LD goes in a server layout that looks the product up in the seed data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;farmer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt; &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;toJsonLd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;productJsonLd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;farmer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;&lt;code&gt;toJsonLd&lt;/code&gt; escapes &lt;code&gt;&amp;lt;&lt;/code&gt;, so a product description can never close the script tag early:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toJsonLd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;u003c&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Product pages start from seed data, so the h1, price and description are in the server-rendered HTML too. Blog articles get &lt;code&gt;BlogPosting&lt;/code&gt; data. &lt;code&gt;app/sitemap.js&lt;/code&gt; lists every product, farm and blog post, and &lt;code&gt;app/robots.js&lt;/code&gt; tells crawlers to skip the cart, checkout, orders, account, wishlist and dashboards. There's also a web manifest and a &lt;code&gt;humans.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The share image is generated with &lt;code&gt;next/og&lt;/code&gt;. &lt;code&gt;app/opengraph-image.jsx&lt;/code&gt; returns a 1200×630 &lt;code&gt;ImageResponse&lt;/code&gt; with the logo, the tagline, my name and the stack. There's no separate design file to keep in sync with the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Demo accounts for visitors
&lt;/h2&gt;

&lt;p&gt;Nobody reviewing a portfolio wants to sign up just to see a dashboard. The login page asks one question: Customer or Farmer? Then you can click "View demo customer account" (or the farmer version), or create your own account with Firebase email/password. A separate "View admin demo" option opens the business side.&lt;/p&gt;

&lt;p&gt;The demo login doesn't wait on Firebase. It writes a session and tells the auth listener to ignore Firebase for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;demoLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DEMO_ACCOUNTS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`demo-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`demo-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;farmerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;farmerId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;`demo-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;demo&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="nx"&gt;ignoreFirebase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&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="nx"&gt;session&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 demo button also prefetches its destination route as soon as it renders.&lt;/p&gt;

&lt;p&gt;The demo farmer is Robert Hayes from Green Valley Farm. His dashboard opens with products, orders and earnings already in it.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F573lgfslhipkudg0k8go.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F573lgfslhipkudg0k8go.jpg" alt="Farmer dashboard for the demo farm showing gross sales, the 10% platform fee, net earnings and a 7-day earnings chart" width="800" height="1039"&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1twkqv2u5w2vl2ktbp3p.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1twkqv2u5w2vl2ktbp3p.jpg" alt="Login page asking visitors to continue as a Customer or a Farmer, with a separate admin demo option" width="800" height="775"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested it
&lt;/h2&gt;

&lt;p&gt;Besides the screenshot workflow, I crawled every route with Playwright at desktop and mobile widths. No route had page errors or horizontal overflow. I checked 79 internal links and ran the main flows end to end: each demo login, checkout through to the tracker, and the admin access rules. The layout holds up on phones down to 390 px wide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A timeout doesn't help if the page still waits for it.&lt;/strong&gt; The first version had a timeout and a fallback to sample data. Every page still sat through six seconds before it got to use that fallback. Rendering from local data first, and treating the network as an update, is what fixed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the build output before optimizing.&lt;/strong&gt; I only found out Firebase was half of every page's JavaScript because I looked at the numbers. Moving it behind dynamic imports was a small change with a big result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write down what "revenue" means before building the dashboard.&lt;/strong&gt; My first admin overview led with the wrong number because I hadn't defined what the platform keeps. One constant and a few small helpers fixed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A 200 status doesn't mean the image is right.&lt;/strong&gt; The HTTP check found the broken URLs. Only the contact sheets showed me the photos of the wrong food.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the demo the front door.&lt;/strong&gt; For a portfolio project, a one-click demo account matters more than any feature hidden behind a sign-up form.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and what's next
&lt;/h2&gt;

&lt;p&gt;Checkout is a demo. No real payments happen, and the test card &lt;code&gt;4242 4242 4242 4242&lt;/code&gt; charges nothing.&lt;/p&gt;

&lt;p&gt;Next on my list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real payments with Stripe&lt;/li&gt;
&lt;li&gt;Moving the background sync to Firestore real-time listeners, and tightening the security rules&lt;/li&gt;
&lt;li&gt;Delivery slots per farm, instead of one 72-hour timeline for everyone&lt;/li&gt;
&lt;li&gt;A proper end-to-end test suite (the screenshot script already does half the clicking)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Open the &lt;a href="https://farmora-smoky.vercel.app/login" rel="noopener noreferrer"&gt;login page&lt;/a&gt;, pick a role and click the demo button.&lt;/p&gt;

&lt;p&gt;A few other things to try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track a sample order without signing in at &lt;a href="https://farmora-smoky.vercel.app/track" rel="noopener noreferrer"&gt;/track&lt;/a&gt;. Try &lt;code&gt;ORD-10241&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use the coupons &lt;code&gt;FRESH10&lt;/code&gt;, &lt;code&gt;FARMORA20&lt;/code&gt; and &lt;code&gt;WELCOME5&lt;/code&gt; at checkout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something breaks, or you'd have built the data layer differently, leave a comment here or open an issue on &lt;a href="https://github.com/Fehan999/Farmora" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. I'd like to hear it.&lt;/p&gt;

</description>
      <category>api</category>
      <category>react</category>
      <category>nextjs</category>
      <category>ui</category>
    </item>
    <item>
      <title>7 Real Problems I Faced While Building a Real-Time Chat Application</title>
      <dc:creator>Ehan Siddique</dc:creator>
      <pubDate>Sat, 10 Oct 2026 05:31:56 +0000</pubDate>
      <link>https://dev.to/ehan_siddique/7-real-problems-i-faced-while-building-a-real-time-chat-application-1m3k</link>
      <guid>https://dev.to/ehan_siddique/7-real-problems-i-faced-while-building-a-real-time-chat-application-1m3k</guid>
      <description>&lt;p&gt;Building a real-time chat application sounds pretty straightforward at first.&lt;/p&gt;

&lt;p&gt;You create a login system, add a message input, connect a database, and display messages on the screen. That's basically it, right?&lt;/p&gt;

&lt;p&gt;Well, that's what I thought when I started working on &lt;strong&gt;ChatLoop&lt;/strong&gt;, my real-time messaging application built with React, Firebase, and WebRTC.&lt;/p&gt;

&lt;p&gt;I wanted to create something more than a basic messaging demo. My goal was to build an application where users could connect with friends, exchange messages instantly, share files, make voice and video calls, and enjoy features you normally find in modern messaging platforms.&lt;/p&gt;

&lt;p&gt;As development progressed, I added message reactions, typing indicators, read receipts, online status, voice messages, disappearing messages, and even an AI-powered chat assistant.&lt;/p&gt;

&lt;p&gt;But behind those features were problems I hadn't fully anticipated.&lt;/p&gt;

&lt;p&gt;Some bugs appeared only when two users were online simultaneously. Others involved network timing, browser restrictions, database synchronization, or React's component lifecycle.&lt;/p&gt;

&lt;p&gt;Here are seven real challenges I faced while developing ChatLoop, how I approached them, and what they taught me about building real-world applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Managing Real-Time Messages Without Duplicates or Unexpected Updates
&lt;/h2&gt;

&lt;p&gt;The first challenge was making real-time messaging reliable.&lt;/p&gt;

&lt;p&gt;I used &lt;strong&gt;Firebase Firestore&lt;/strong&gt; because its real-time listeners allow an application to receive database updates without constantly requesting new data.&lt;/p&gt;

&lt;p&gt;At first, it seemed simple. Whenever someone sent a message, Firestore would update, and the conversation would immediately display the new message.&lt;/p&gt;

&lt;p&gt;However, problems started appearing as the application became more complex.&lt;/p&gt;

&lt;p&gt;Sometimes, changing between conversations created unexpected updates. I also had to deal with listener cleanup and situations where asynchronous updates could arrive after a component had changed.&lt;/p&gt;

&lt;p&gt;The problem wasn't Firestore itself. It was how I managed real-time subscriptions inside React.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I approached the problem
&lt;/h3&gt;

&lt;p&gt;I focused on managing the lifecycle of every Firestore listener.&lt;/p&gt;

&lt;p&gt;When a user opened a conversation, the application subscribed to the relevant messages. When they switched conversations or left the page, the previous subscription needed to be removed.&lt;/p&gt;

&lt;p&gt;React's &lt;code&gt;useEffect&lt;/code&gt; cleanup mechanism became particularly important.&lt;/p&gt;

&lt;p&gt;I also had to think carefully about how messages were ordered and how incoming data updated the interface.&lt;/p&gt;

&lt;p&gt;A real-time listener shouldn't automatically mean adding every received message to an existing array. Firestore snapshots need to be handled according to whether they represent the current dataset or individual changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; Real-time applications require more than fast database updates. They need proper subscription management, predictable state updates, and careful handling of asynchronous events.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Preventing Duplicate Conversations Between Two Users
&lt;/h2&gt;

&lt;p&gt;Another interesting challenge involved managing private conversations.&lt;/p&gt;

&lt;p&gt;ChatLoop includes a friend-request system where users can send, accept, reject, or cancel friend requests. Once two users become friends, they can start a private conversation.&lt;/p&gt;

&lt;p&gt;But that introduces a question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if both users try to start the same conversation?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine User A opens a chat with User B. Meanwhile, User B opens a chat with User A.&lt;/p&gt;

&lt;p&gt;Without a consistent conversation identifier, the application could accidentally create two different chat threads for the same pair of users.&lt;/p&gt;

&lt;p&gt;That would make message history, unread counts, and conversation management unnecessarily complicated.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I solved it
&lt;/h3&gt;

&lt;p&gt;I used a deterministic conversation ID based on the two users' identifiers.&lt;/p&gt;

&lt;p&gt;Instead of generating a random ID every time someone opened a conversation, the application created a consistent ID from the same pair of users.&lt;/p&gt;

&lt;p&gt;For example, the identifiers could be sorted before being combined:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User A + User B → Same conversation ID&lt;/li&gt;
&lt;li&gt;User B + User A → Same conversation ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The order of the users no longer mattered.&lt;/p&gt;

&lt;p&gt;This made it easier to locate an existing conversation and associate its messages with the correct chat.&lt;/p&gt;

&lt;p&gt;Of course, a predictable ID alone doesn't replace proper database access rules. Users should still only be able to access conversations they're authorized to view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; Good data modeling can prevent entire categories of bugs before they happen.&lt;/p&gt;

&lt;p&gt;Sometimes, solving a complicated UI problem starts with designing a better database structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Making Unread Messages and Read Receipts Work Correctly
&lt;/h2&gt;

&lt;p&gt;Unread message counts seem like a tiny feature.&lt;/p&gt;

&lt;p&gt;You receive a message, the unread counter increases, and when you open the conversation, the counter disappears.&lt;/p&gt;

&lt;p&gt;Simple.&lt;/p&gt;

&lt;p&gt;But implementing this correctly in a real-time environment turned out to be more challenging than I expected.&lt;/p&gt;

&lt;p&gt;Consider these situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A user receives multiple messages while viewing another conversation.&lt;/li&gt;
&lt;li&gt;The user opens a conversation while new messages are still arriving.&lt;/li&gt;
&lt;li&gt;Two browser sessions are active for the same account.&lt;/li&gt;
&lt;li&gt;A sender expects to see whether the recipient has read a message.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suddenly, the application needs to keep track of more than just message content.&lt;/p&gt;

&lt;p&gt;It needs to understand the relationship between messages, recipients, active conversations, and read status.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I approached the problem
&lt;/h3&gt;

&lt;p&gt;I worked with conversation-level unread information and message status updates.&lt;/p&gt;

&lt;p&gt;When a message arrived, ChatLoop needed to determine whether it should count as unread for the recipient.&lt;/p&gt;

&lt;p&gt;Opening a conversation also needed to update the relevant read state without incorrectly changing the sender's information.&lt;/p&gt;

&lt;p&gt;I implemented visual indicators for message status, including read indicators, so users could better understand what was happening.&lt;/p&gt;

&lt;p&gt;One important lesson was that delivering a message and reading a message are two different events.&lt;/p&gt;

&lt;p&gt;A message existing in Firestore doesn't necessarily mean the recipient has seen it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; Small features can require surprisingly careful state management, especially when multiple users interact with the same data.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Building Accurate Online Status and Typing Indicators
&lt;/h2&gt;

&lt;p&gt;I wanted ChatLoop to feel like a real messaging platform rather than a static web application.&lt;/p&gt;

&lt;p&gt;That meant showing when someone was online, when they were last active, and when they were typing.&lt;/p&gt;

&lt;p&gt;These features make conversations feel more natural, but they also introduce a different kind of real-time problem.&lt;/p&gt;

&lt;p&gt;For example, when should the application show someone as offline?&lt;/p&gt;

&lt;p&gt;What happens if they close their browser without logging out?&lt;/p&gt;

&lt;p&gt;What happens if their internet connection suddenly disappears?&lt;/p&gt;

&lt;p&gt;A basic online/offline boolean cannot reliably answer all these questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I approached the problem
&lt;/h3&gt;

&lt;p&gt;For online presence, I worked with user activity and last-seen timestamps, using Firebase server timestamps where appropriate.&lt;/p&gt;

&lt;p&gt;I also implemented typing information that could be displayed to the other participant.&lt;/p&gt;

&lt;p&gt;Typing indicators need to be temporary. Otherwise, someone could stop typing, leave the conversation, and still appear to be typing indefinitely.&lt;/p&gt;

&lt;p&gt;This required thinking about when typing state should begin, when it should stop, and how the interface should respond to changes.&lt;/p&gt;

&lt;p&gt;Presence tracking has its own limitations, too. A last-seen timestamp is useful, but truly reliable online presence requires accounting for disconnects and inactive sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; Real-time user experience isn't just about displaying fresh information. It's also about knowing when information is no longer valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Getting WebRTC Voice and Video Calls to Connect
&lt;/h2&gt;

&lt;p&gt;This was one of the most challenging parts of building ChatLoop.&lt;/p&gt;

&lt;p&gt;Text messaging was one thing. Real-time audio and video communication introduced an entirely different set of problems.&lt;/p&gt;

&lt;p&gt;I used &lt;strong&gt;WebRTC&lt;/strong&gt; for peer-to-peer media communication and Firebase Firestore for signaling.&lt;/p&gt;

&lt;p&gt;WebRTC allows browsers to establish connections for real-time audio and video, but creating that connection involves several steps.&lt;/p&gt;

&lt;p&gt;The calling user generates an offer, the receiving user responds with an answer, and both sides exchange ICE candidates to discover a working network connection.&lt;/p&gt;

&lt;p&gt;It sounds manageable when explained in a few sentences.&lt;/p&gt;

&lt;p&gt;In practice, timing matters enormously.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ICE candidate problem
&lt;/h3&gt;

&lt;p&gt;One issue I encountered involved ICE candidates arriving before the receiving peer connection was ready to process them.&lt;/p&gt;

&lt;p&gt;This created connection failures that were difficult to reproduce consistently.&lt;/p&gt;

&lt;p&gt;Sometimes a call would connect successfully. Other times, the connection would fail even though the same general process had worked before.&lt;/p&gt;

&lt;p&gt;After investigating the signaling flow, I realized I needed to manage the order in which candidates were handled.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I solved it
&lt;/h3&gt;

&lt;p&gt;I introduced an ICE candidate queue.&lt;/p&gt;

&lt;p&gt;Instead of trying to process every candidate immediately, the application could temporarily store candidates that arrived too early.&lt;/p&gt;

&lt;p&gt;Once the remote session description was ready, the queued candidates could be processed.&lt;/p&gt;

&lt;p&gt;I also worked with STUN and TURN server configuration to help establish connections across different network conditions.&lt;/p&gt;

&lt;p&gt;TURN support is especially important because direct peer-to-peer connectivity isn't always possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; In asynchronous systems, having the correct data isn't enough. The data also needs to arrive—or be processed—at the correct stage.&lt;/p&gt;

&lt;p&gt;WebRTC taught me a lot about signaling, network communication, race conditions, and error handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Fixing Audio Playback and Handling Browser Restrictions
&lt;/h2&gt;

&lt;p&gt;After working through WebRTC connection problems, I discovered another issue.&lt;/p&gt;

&lt;p&gt;A call could appear connected, but the remote audio wouldn't always play as expected.&lt;/p&gt;

&lt;p&gt;From a user's perspective, that feels like a broken call.&lt;/p&gt;

&lt;p&gt;From a developer's perspective, the connection itself might actually be working.&lt;/p&gt;

&lt;p&gt;This was particularly confusing because I had to distinguish between a networking problem, a media-stream problem, and a browser playback problem.&lt;/p&gt;

&lt;p&gt;Modern browsers place restrictions on automatic media playback, especially when audio is involved.&lt;/p&gt;

&lt;p&gt;That means receiving a remote stream doesn't guarantee that the browser will immediately play it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I approached the problem
&lt;/h3&gt;

&lt;p&gt;I reviewed how remote media streams were attached to media elements and how playback was initiated.&lt;/p&gt;

&lt;p&gt;I also handled audio and video call states more carefully, including what should happen when a user accepts, minimizes, or ends a call.&lt;/p&gt;

&lt;p&gt;The goal was to make the interface accurately reflect the actual call state.&lt;/p&gt;

&lt;p&gt;A call shouldn't appear fully functional if the application hasn't properly initialized its media handling.&lt;/p&gt;

&lt;p&gt;I also learned that media elements need careful lifecycle management. Streams and tracks should be cleaned up when a call ends so that resources aren't left running unnecessarily.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; A successful network connection doesn't automatically mean a successful user experience.&lt;/p&gt;

&lt;p&gt;You have to consider how browsers handle permissions, playback, device access, and media cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Handling File Uploads and Firebase Storage CORS Errors
&lt;/h2&gt;

&lt;p&gt;ChatLoop supports more than text messages.&lt;/p&gt;

&lt;p&gt;I wanted users to exchange attachments and voice messages as part of normal conversations.&lt;/p&gt;

&lt;p&gt;For file handling, I used Firebase Storage.&lt;/p&gt;

&lt;p&gt;However, while developing the attachment features, I encountered CORS-related problems.&lt;/p&gt;

&lt;p&gt;CORS, or Cross-Origin Resource Sharing, is a browser security mechanism that controls whether web applications can access resources from different origins.&lt;/p&gt;

&lt;p&gt;An upload or resource request can fail when the required cross-origin permissions aren't configured correctly.&lt;/p&gt;

&lt;p&gt;At first, these errors can be frustrating because the application code might look perfectly reasonable.&lt;/p&gt;

&lt;p&gt;The problem may actually involve the interaction between the browser, storage service, and hosting configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I approached the problem
&lt;/h3&gt;

&lt;p&gt;I investigated the storage requests and reviewed the configuration needed for the application's origin.&lt;/p&gt;

&lt;p&gt;After addressing the relevant configuration issues, I continued working on the attachment experience.&lt;/p&gt;

&lt;p&gt;But the experience also taught me that successful uploads are only part of the problem.&lt;/p&gt;

&lt;p&gt;A production-ready file-sharing system needs to consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;File size and type validation&lt;/li&gt;
&lt;li&gt;Upload progress and error messages&lt;/li&gt;
&lt;li&gt;Storage access permissions&lt;/li&gt;
&lt;li&gt;Secure file retrieval&lt;/li&gt;
&lt;li&gt;Failed or interrupted uploads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Uploading a file is easy to demonstrate in a tutorial. Building an experience that users can depend on requires more planning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; Frontend development doesn't stop at writing React components. Understanding browser security, cloud configuration, and access control is equally important.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Building ChatLoop Taught Me About Real-World Development
&lt;/h2&gt;

&lt;p&gt;Before building ChatLoop, I already had experience working with React, JavaScript, and modern frontend tools.&lt;/p&gt;

&lt;p&gt;But this project pushed me to think differently.&lt;/p&gt;

&lt;p&gt;I wasn't simply developing individual features anymore. I was connecting several systems that needed to work together.&lt;/p&gt;

&lt;p&gt;React managed the user interface. Firebase handled authentication, data, and storage. WebRTC enabled real-time calls. And the application needed to keep all those pieces synchronized.&lt;/p&gt;

&lt;p&gt;Every time I added a feature, I had to consider how it would affect the existing application.&lt;/p&gt;

&lt;p&gt;Adding message reactions affected message rendering.&lt;/p&gt;

&lt;p&gt;Adding voice messages introduced media permissions and file handling.&lt;/p&gt;

&lt;p&gt;Adding calling features introduced connection states, signaling, and cleanup.&lt;/p&gt;

&lt;p&gt;Even a simple feature like showing whether a friend was online raised questions about connection reliability.&lt;/p&gt;

&lt;p&gt;I also integrated an AI chat feature using Google's Gemini API, which introduced another area to think about: connecting AI services to an existing application safely and predictably.&lt;/p&gt;

&lt;p&gt;One of the biggest lessons was that &lt;strong&gt;building software isn't just about making features work once. It's about making them work consistently under different conditions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tutorials are useful for learning technologies, but real projects force you to understand how those technologies behave together.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Advice to Developers Building Their First Real-Time Application
&lt;/h2&gt;

&lt;p&gt;If you're planning to build a real-time chat application using React, Firebase, or WebRTC, here's what I'd recommend based on my experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with the data model.&lt;/strong&gt; Think about users, friendships, conversations, messages, and permissions before creating a complicated interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep real-time listeners under control.&lt;/strong&gt; Make sure subscriptions are cleaned up when they're no longer needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test with multiple accounts.&lt;/strong&gt; A chat application might work perfectly when you're testing alone, but behave differently when two users are sending messages at the same time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expect asynchronous problems.&lt;/strong&gt; Database updates, network responses, and WebRTC signaling don't always happen in the order you expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat loading and error states as real features.&lt;/strong&gt; Users should understand what's happening when something is connecting, uploading, or failing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't ignore security.&lt;/strong&gt; Authentication alone isn't enough. Database rules, storage permissions, and server-side API handling deserve attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Focus on reliability before adding more features.&lt;/strong&gt; A basic messaging application that works consistently is more valuable than a feature-rich application with unpredictable behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building ChatLoop has been one of my most valuable learning experiences as a developer.&lt;/p&gt;

&lt;p&gt;It gave me the opportunity to work with real-time databases, authentication, media communication, cloud storage, application state, and AI integration in a single project.&lt;/p&gt;

&lt;p&gt;Some problems took longer to understand than I expected. Others taught me concepts that I probably wouldn't have explored through tutorials alone.&lt;/p&gt;

&lt;p&gt;And that's what I enjoy most about building projects.&lt;/p&gt;

&lt;p&gt;Every challenging bug is an opportunity to understand something more deeply.&lt;/p&gt;

&lt;p&gt;If you're working on your own real-time application, don't be discouraged when features become more complicated than they initially seemed. Those difficult parts are often where the most useful learning happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want to explore ChatLoop?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Live Application: &lt;a href="https://chatloop-mu.vercel.app/" rel="noopener noreferrer"&gt;https://chatloop-mu.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm &lt;strong&gt;Ehan Siddique&lt;/strong&gt;, a full-stack web developer working with React, Next.js, Node.js, Firebase, and modern web technologies.&lt;/p&gt;

&lt;p&gt;I share development experiences, technical lessons, and projects on my website:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ehansiddique.com" rel="noopener noreferrer"&gt;https://ehansiddique.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Built a Real-Time Chat App with React, Firebase, WebRTC &amp; Gemini AI — Here’s What I Learned</title>
      <dc:creator>Ehan Siddique</dc:creator>
      <pubDate>Mon, 28 Sep 2026 13:11:59 +0000</pubDate>
      <link>https://dev.to/ehan_siddique/i-built-a-real-time-chat-app-with-react-firebase-webrtc-gemini-ai-heres-what-i-learned-596h</link>
      <guid>https://dev.to/ehan_siddique/i-built-a-real-time-chat-app-with-react-firebase-webrtc-gemini-ai-heres-what-i-learned-596h</guid>
      <description>&lt;p&gt;Building a chat UI is easy.&lt;/p&gt;

&lt;p&gt;Building a chat application that actually behaves like the messaging apps we use every day is a completely different challenge.&lt;/p&gt;

&lt;p&gt;I’m &lt;strong&gt;Ehan Siddique&lt;/strong&gt;, a full-stack web developer, and recently I built &lt;strong&gt;ChatLoop&lt;/strong&gt;, one of my biggest personal projects so far.&lt;/p&gt;

&lt;p&gt;ChatLoop started as an idea for a simple real-time messaging application, but I kept asking myself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What would I need to build if this were a real communication product instead of just another portfolio project?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question changed the entire project.&lt;/p&gt;

&lt;p&gt;I ended up building real-time messaging, reactions, read receipts, voice notes, file sharing, presence tracking, audio/video calls, screen sharing, friend requests, an AI assistant, reporting and moderation, account suspensions, and a complete admin dashboard.&lt;/p&gt;

&lt;p&gt;In this article, I want to share how I approached the project, some of the engineering decisions behind it, and what I learned while building it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is ChatLoop?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ChatLoop&lt;/strong&gt; is a real-time communication web application built primarily with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React 18&lt;/li&gt;
&lt;li&gt;Vite&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;li&gt;Firebase Authentication&lt;/li&gt;
&lt;li&gt;Cloud Firestore&lt;/li&gt;
&lt;li&gt;Supabase Storage&lt;/li&gt;
&lt;li&gt;WebRTC&lt;/li&gt;
&lt;li&gt;Google Gemini API&lt;/li&gt;
&lt;li&gt;Framer Motion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal wasn't simply to let two users send messages.&lt;/p&gt;

&lt;p&gt;I wanted the application to handle many of the smaller details that make a messaging product actually feel complete.&lt;/p&gt;

&lt;p&gt;That includes things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;typing indicators&lt;/li&gt;
&lt;li&gt;read receipts&lt;/li&gt;
&lt;li&gt;message reactions&lt;/li&gt;
&lt;li&gt;unread counters&lt;/li&gt;
&lt;li&gt;voice messages&lt;/li&gt;
&lt;li&gt;online/away/last-seen status&lt;/li&gt;
&lt;li&gt;audio and video calls&lt;/li&gt;
&lt;li&gt;missed and rejected call states&lt;/li&gt;
&lt;li&gt;screen sharing&lt;/li&gt;
&lt;li&gt;user reporting&lt;/li&gt;
&lt;li&gt;account suspension&lt;/li&gt;
&lt;li&gt;admin announcements&lt;/li&gt;
&lt;li&gt;AI conversations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And importantly, these features needed to work together.&lt;/p&gt;




&lt;h1&gt;
  
  
  1. Real-Time Messaging Was More Than &lt;code&gt;onSnapshot()&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;Firebase makes listening for real-time data surprisingly simple.&lt;/p&gt;

&lt;p&gt;But displaying messages in real time is only one part of building a messaging system.&lt;/p&gt;

&lt;p&gt;Every conversation in ChatLoop needs information such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;participants
lastMessage
lastMessageTime
lastMessageSender
unreadCounts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while the actual messages are stored separately.&lt;/p&gt;

&lt;p&gt;A simplified version of the structure looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chats/{chatId}

messages/{chatId}/messages/{messageId}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When someone sends a message, ChatLoop doesn't only create the message.&lt;/p&gt;

&lt;p&gt;It also updates the conversation metadata.&lt;/p&gt;

&lt;p&gt;I handle those operations together in a batch.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;writeBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Create message&lt;/span&gt;
&lt;span class="c1"&gt;// Update last message&lt;/span&gt;
&lt;span class="c1"&gt;// Update timestamp&lt;/span&gt;
&lt;span class="c1"&gt;// Update sender&lt;/span&gt;
&lt;span class="c1"&gt;// Increment receiver unread count&lt;/span&gt;

&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Imagine the message succeeds but updating &lt;code&gt;lastMessage&lt;/code&gt; fails.&lt;/p&gt;

&lt;p&gt;The receiver could have a new message inside the conversation while their sidebar still displays the previous one.&lt;/p&gt;

&lt;p&gt;Keeping related operations together prevents that inconsistent state.&lt;/p&gt;

&lt;p&gt;Firestore also applies local writes before receiving confirmation from the server.&lt;/p&gt;

&lt;p&gt;That allowed me to make a newly sent message appear immediately while displaying a small pending state until the server confirms it.&lt;/p&gt;

&lt;p&gt;That tiny detail makes messaging feel much faster.&lt;/p&gt;




&lt;h1&gt;
  
  
  2. I Didn't Want Hundreds of Unnecessary Listeners
&lt;/h1&gt;

&lt;p&gt;Real-time applications can become expensive and complicated when everything becomes a listener.&lt;/p&gt;

&lt;p&gt;Initially, it is tempting to listen to everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Conversation A messages
Conversation B messages
Conversation C messages
Conversation D messages
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the dashboard doesn't actually need every message from every conversation.&lt;/p&gt;

&lt;p&gt;It mainly needs conversation metadata.&lt;/p&gt;

&lt;p&gt;So ChatLoop's dashboard listens to chat documents.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;lastMessageTime&lt;/code&gt; changes and another user was the sender, the application knows that a new message arrived.&lt;/p&gt;

&lt;p&gt;It can then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;update the conversation preview&lt;/li&gt;
&lt;li&gt;update unread counts&lt;/li&gt;
&lt;li&gt;play a notification sound&lt;/li&gt;
&lt;li&gt;show a desktop notification when appropriate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the currently opened conversation needs its messages actively displayed.&lt;/p&gt;

&lt;p&gt;This taught me an important lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Real-time doesn't mean everything needs to be real-time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You still need to think about what data the UI actually needs.&lt;/p&gt;




&lt;h1&gt;
  
  
  3. Building User Presence Was Surprisingly Interesting
&lt;/h1&gt;

&lt;p&gt;I wanted ChatLoop to display:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Online
Away
Last seen...
Offline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple solution would be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;online&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;when the application opens and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;online&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;when it closes.&lt;/p&gt;

&lt;p&gt;Unfortunately, browsers aren't that reliable.&lt;/p&gt;

&lt;p&gt;What happens if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the browser crashes?&lt;/li&gt;
&lt;li&gt;the laptop loses internet?&lt;/li&gt;
&lt;li&gt;the phone kills the browser?&lt;/li&gt;
&lt;li&gt;the user closes the process unexpectedly?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application might never get the chance to write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;online&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So ChatLoop uses a heartbeat system.&lt;/p&gt;

&lt;p&gt;The application periodically updates the user's presence.&lt;/p&gt;

&lt;p&gt;If an &lt;code&gt;online&lt;/code&gt; status becomes older than the allowed threshold, ChatLoop treats that user as offline.&lt;/p&gt;

&lt;p&gt;I also listen for tab visibility.&lt;/p&gt;

&lt;p&gt;When the tab becomes hidden:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;online → away
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and when the user returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;away → online
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a privacy setting allowing users to hide their active status.&lt;/p&gt;

&lt;p&gt;It seems like a small feature, but building it taught me a lot about how unreliable client state can be.&lt;/p&gt;




&lt;h1&gt;
  
  
  4. Then I Added WebRTC Calls
&lt;/h1&gt;

&lt;p&gt;This was probably one of the most interesting parts of the project.&lt;/p&gt;

&lt;p&gt;ChatLoop supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;audio calls&lt;/li&gt;
&lt;li&gt;video calls&lt;/li&gt;
&lt;li&gt;microphone mute/unmute&lt;/li&gt;
&lt;li&gt;camera controls&lt;/li&gt;
&lt;li&gt;screen sharing&lt;/li&gt;
&lt;li&gt;missed calls&lt;/li&gt;
&lt;li&gt;rejected calls&lt;/li&gt;
&lt;li&gt;reconnect attempts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WebRTC handles the actual peer-to-peer communication.&lt;/p&gt;

&lt;p&gt;But two browsers still need a way to find each other and exchange connection information.&lt;/p&gt;

&lt;p&gt;For that, I use Firestore as the signalling layer.&lt;/p&gt;

&lt;p&gt;The basic process looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Caller
   ↓
Create call
   ↓
Create WebRTC offer
   ↓
Store signalling data in Firestore
   ↓
Receiver
   ↓
Create WebRTC answer
   ↓
Exchange ICE candidates
   ↓
Peer-to-peer connection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The call itself has lifecycle states such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;calling
   ↓
active
   ↓
ended
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and additional final states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;missed
rejected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  5. Race Conditions Matter
&lt;/h1&gt;

&lt;p&gt;One interesting problem happens when both users hang up almost simultaneously.&lt;/p&gt;

&lt;p&gt;Both clients could potentially try to write the final call state and create the same "call ended" event.&lt;/p&gt;

&lt;p&gt;I solved this using a transaction for final call states.&lt;/p&gt;

&lt;p&gt;That way, only one final call event wins.&lt;/p&gt;

&lt;p&gt;This was one of those situations where building the feature teaches you more than reading about the concept.&lt;/p&gt;

&lt;p&gt;You start noticing race conditions because suddenly there are two real clients modifying the same state.&lt;/p&gt;




&lt;h1&gt;
  
  
  6. Opening the Call Screen Immediately
&lt;/h1&gt;

&lt;p&gt;I didn't want this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Click call
↓
Wait
↓
Wait for Firestore
↓
Open calling UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It feels slow.&lt;/p&gt;

&lt;p&gt;Instead, the call ID can be generated on the client.&lt;/p&gt;

&lt;p&gt;The calling screen can therefore open immediately while the Firestore document is being created in the background.&lt;/p&gt;

&lt;p&gt;This makes the experience feel significantly more responsive.&lt;/p&gt;

&lt;p&gt;However, it creates another problem.&lt;/p&gt;

&lt;p&gt;What if the caller hangs up before the Firestore write finishes?&lt;/p&gt;

&lt;p&gt;The other person's device could potentially receive the call afterward and keep ringing.&lt;/p&gt;

&lt;p&gt;So the hang-up flow needs to account for the initial call write before completing cleanup.&lt;/p&gt;

&lt;p&gt;These are the kinds of problems I didn't think much about before building a calling system.&lt;/p&gt;




&lt;h1&gt;
  
  
  7. Network Changes Are Another Problem
&lt;/h1&gt;

&lt;p&gt;WebRTC connections aren't always stable.&lt;/p&gt;

&lt;p&gt;Users can move between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Wi-Fi → mobile data
mobile data → Wi-Fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or temporarily lose connectivity.&lt;/p&gt;

&lt;p&gt;My WebRTC service handles an ICE restart attempt when the network changes before completely giving up on the connection.&lt;/p&gt;

&lt;p&gt;I also learned why TURN servers matter.&lt;/p&gt;

&lt;p&gt;Peer-to-peer WebRTC connections don't always work directly between restrictive networks.&lt;/p&gt;

&lt;p&gt;For production-level reliability, a TURN server becomes important.&lt;/p&gt;




&lt;h1&gt;
  
  
  8. Voice Notes and File Uploads
&lt;/h1&gt;

&lt;p&gt;ChatLoop also supports voice messages.&lt;/p&gt;

&lt;p&gt;Users can record a message directly from the chat input and send it like a normal message.&lt;/p&gt;

&lt;p&gt;I wanted voice notes to feel like an actual messaging feature instead of just displaying an HTML audio element, so the player includes a waveform-style interface.&lt;/p&gt;

&lt;p&gt;Users can also send:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;images&lt;/li&gt;
&lt;li&gt;documents&lt;/li&gt;
&lt;li&gt;screenshots&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and can drag files into the chat or paste screenshots directly.&lt;/p&gt;

&lt;p&gt;I use &lt;strong&gt;Supabase Storage&lt;/strong&gt; for these uploads while Firestore stores the related message information.&lt;/p&gt;

&lt;p&gt;This gave me a useful separation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Firestore
→ application/message data

Supabase
→ actual uploaded files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  9. Friend Requests Became Their Own System
&lt;/h1&gt;

&lt;p&gt;Users can discover other people using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name&lt;/li&gt;
&lt;li&gt;username&lt;/li&gt;
&lt;li&gt;four-digit ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then they can:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Send request
Accept
Decline
Cancel
Remove friend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user document's friend list is ultimately the source of truth.&lt;/p&gt;

&lt;p&gt;When a friend request is accepted, I update the request and both users' friend lists together.&lt;/p&gt;

&lt;p&gt;Again, atomic operations become important.&lt;/p&gt;

&lt;p&gt;You don't want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User A says B is their friend

but

User B doesn't have A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because one database write failed.&lt;/p&gt;




&lt;h1&gt;
  
  
  10. I Added Gemini AI as Another Conversation
&lt;/h1&gt;

&lt;p&gt;I also wanted ChatLoop to include an AI assistant.&lt;/p&gt;

&lt;p&gt;Instead of making it feel like a completely separate feature, &lt;strong&gt;ChatLoop AI&lt;/strong&gt; appears directly in the chat list.&lt;/p&gt;

&lt;p&gt;Each user has their own AI conversation history.&lt;/p&gt;

&lt;p&gt;The structure is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aiChats/{uid}/messages/{id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When requesting a response, recent messages are provided as context to Gemini.&lt;/p&gt;

&lt;p&gt;I don't send the user's entire lifetime conversation every time.&lt;/p&gt;

&lt;p&gt;The service uses the most recent messages as context.&lt;/p&gt;

&lt;p&gt;I also implemented a fallback model.&lt;/p&gt;

&lt;p&gt;If the main Gemini model becomes overloaded, the application can attempt to use a lighter model instead.&lt;/p&gt;

&lt;p&gt;One decision I particularly like is how errors are handled.&lt;/p&gt;

&lt;p&gt;Instead of silently failing, an error can become part of the conversation so the user understands what happened.&lt;/p&gt;




&lt;h1&gt;
  
  
  11. Markdown Without &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;AI responses often contain Markdown.&lt;/p&gt;

&lt;p&gt;I wanted ChatLoop AI to render those responses nicely, but I didn't want to simply inject arbitrary HTML into the application.&lt;/p&gt;

&lt;p&gt;So the Markdown rendering system doesn't rely on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;dangerouslySetInnerHTML&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a small implementation detail, but security decisions like this become increasingly important once your application starts displaying generated or user-controlled content.&lt;/p&gt;




&lt;h1&gt;
  
  
  12. Building the Admin Panel Changed the Project
&lt;/h1&gt;

&lt;p&gt;This was one of the biggest differences between ChatLoop and some of my earlier projects.&lt;/p&gt;

&lt;p&gt;I started thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If real users were using this application, how would I actually manage it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That resulted in a complete &lt;code&gt;/admin&lt;/code&gt; interface.&lt;/p&gt;

&lt;p&gt;The dashboard displays information including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;total users&lt;/li&gt;
&lt;li&gt;users currently online&lt;/li&gt;
&lt;li&gt;active users&lt;/li&gt;
&lt;li&gt;registrations&lt;/li&gt;
&lt;li&gt;conversations&lt;/li&gt;
&lt;li&gt;messages&lt;/li&gt;
&lt;li&gt;friendships&lt;/li&gt;
&lt;li&gt;reports&lt;/li&gt;
&lt;li&gt;suspended users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Administrators can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;search users&lt;/li&gt;
&lt;li&gt;edit selected profile information&lt;/li&gt;
&lt;li&gt;suspend users&lt;/li&gt;
&lt;li&gt;remove suspensions&lt;/li&gt;
&lt;li&gt;review reports&lt;/li&gt;
&lt;li&gt;dismiss reports&lt;/li&gt;
&lt;li&gt;delete reported messages&lt;/li&gt;
&lt;li&gt;publish announcements&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  13. I Also Built a Read-Only Guest Admin
&lt;/h1&gt;

&lt;p&gt;I wanted people visiting my portfolio or GitHub repository to actually see the administration system.&lt;/p&gt;

&lt;p&gt;But obviously I couldn't give random visitors admin privileges.&lt;/p&gt;

&lt;p&gt;So I created a guest administrator.&lt;/p&gt;

&lt;p&gt;The guest can view the real dashboard but cannot perform administrative actions.&lt;/p&gt;

&lt;p&gt;And importantly, this isn't just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;guest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;disableButton&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 Firestore security rules also prevent guest writes.&lt;/p&gt;

&lt;p&gt;That distinction is important.&lt;/p&gt;




&lt;h1&gt;
  
  
  14. The UI Is Not Security
&lt;/h1&gt;

&lt;p&gt;This was probably one of the most valuable security lessons from this project.&lt;/p&gt;

&lt;p&gt;ChatLoop doesn't have a traditional custom backend server.&lt;/p&gt;

&lt;p&gt;That means &lt;strong&gt;Firestore Security Rules effectively become part of the backend security layer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The rules enforce things such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Users can only modify permitted profile fields.

Only conversation participants can access private chats.

Only conversation participants can access messages.

Suspended users cannot send messages.

Suspended users cannot initiate calls.

Suspended users cannot send friend requests.

Guest administrators cannot modify admin data.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend can hide a button.&lt;/p&gt;

&lt;p&gt;But someone can modify frontend JavaScript.&lt;/p&gt;

&lt;p&gt;They cannot simply override properly configured Firestore rules from the browser.&lt;/p&gt;

&lt;p&gt;My rule became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;UI permissions determine what the user sees. Security rules determine what the user is actually allowed to do.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  15. Reporting and Suspensions
&lt;/h1&gt;

&lt;p&gt;I also wanted to explore moderation.&lt;/p&gt;

&lt;p&gt;Users can report individual messages or entire accounts.&lt;/p&gt;

&lt;p&gt;A message report can preserve information about the reported message so an administrator has context when reviewing it.&lt;/p&gt;

&lt;p&gt;Administrators can then take actions such as removing the reported message or suspending the user.&lt;/p&gt;

&lt;p&gt;Suspension also needed to affect the rest of the application.&lt;/p&gt;

&lt;p&gt;When someone is suspended:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;They see a suspension screen.

Friends see "Account suspended."

Messaging becomes unavailable.

Calling becomes unavailable.

Friend requests become unavailable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And again, Firestore rules enforce those restrictions.&lt;/p&gt;




&lt;h1&gt;
  
  
  16. Admin Announcements
&lt;/h1&gt;

&lt;p&gt;Admins can publish announcements that appear to users like notifications.&lt;/p&gt;

&lt;p&gt;But I didn't want the same announcement to appear every time someone refreshed the application.&lt;/p&gt;

&lt;p&gt;So ChatLoop creates a small version identifier based on the announcement content.&lt;/p&gt;

&lt;p&gt;Once a user sees it, that version is stored.&lt;/p&gt;

&lt;p&gt;If the administrator changes the announcement, the version changes and users receive the updated announcement.&lt;/p&gt;

&lt;p&gt;It's a relatively small feature, but I enjoyed solving it.&lt;/p&gt;




&lt;h1&gt;
  
  
  17. Organizing the Codebase
&lt;/h1&gt;

&lt;p&gt;As the application grew, architecture became increasingly important.&lt;/p&gt;

&lt;p&gt;One rule I adopted was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Components should not directly contain Firestore implementation logic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead, Firebase-related functionality lives inside dedicated files.&lt;/p&gt;

&lt;p&gt;A simplified structure looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/

components/
  auth/
  chat/
  admin/
  common/

firebase/
  messaging
  users
  friends
  reports
  admin

service/
  presence
  WebRTC

hooks/

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

&lt;/div&gt;



&lt;p&gt;So if a component needs to send a message, it calls a function from the relevant Firebase service rather than implementing database operations directly inside the component.&lt;/p&gt;

&lt;p&gt;That made the project significantly easier to reason about as it grew.&lt;/p&gt;




&lt;h1&gt;
  
  
  18. Some Things I Would Change at Larger Scale
&lt;/h1&gt;

&lt;p&gt;ChatLoop isn't perfect.&lt;/p&gt;

&lt;p&gt;There are several things I would approach differently if I were scaling it to a very large user base.&lt;/p&gt;

&lt;h3&gt;
  
  
  User search
&lt;/h3&gt;

&lt;p&gt;The current friend search can load users and filter them client-side.&lt;/p&gt;

&lt;p&gt;That's acceptable for a relatively small number of users.&lt;/p&gt;

&lt;p&gt;It would not be my solution for hundreds of thousands or millions of accounts.&lt;/p&gt;

&lt;p&gt;At that point, I'd move search to a proper indexing/search solution.&lt;/p&gt;

&lt;h3&gt;
  
  
  TURN infrastructure
&lt;/h3&gt;

&lt;p&gt;WebRTC connections can fail when users are behind restrictive NATs or networks.&lt;/p&gt;

&lt;p&gt;A production deployment should have reliable TURN infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Group conversations
&lt;/h3&gt;

&lt;p&gt;ChatLoop currently focuses on one-to-one communication.&lt;/p&gt;

&lt;p&gt;Group messaging and group calling would require additional architecture.&lt;/p&gt;




&lt;h1&gt;
  
  
  19. What I Learned
&lt;/h1&gt;

&lt;p&gt;ChatLoop taught me much more than simply how to build a chat interface.&lt;/p&gt;

&lt;p&gt;I had to think about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What actually needs a listener and what doesn't?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What happens when one write succeeds and another fails?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Race conditions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What happens when two clients update the same state simultaneously?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Presence&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How do you know someone is really online?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Networking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What happens when a WebRTC connection fails or the network changes?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What prevents someone from bypassing the frontend?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Moderation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How would administrators handle abusive users?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How much data should remain live at once?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UX&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How do you make operations feel immediate even when network requests are happening?&lt;/p&gt;

&lt;p&gt;Those questions made this project much more valuable to me than simply creating another CRUD application.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;ChatLoop started as a personal project.&lt;/p&gt;

&lt;p&gt;But it became an opportunity for me to explore several areas of modern web development inside one application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React
+
Firebase
+
Real-Time Data
+
WebRTC
+
Supabase
+
Gemini AI
+
Security
+
Moderation
+
Admin Tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are still many things I want to improve, and that's part of why I enjoy building projects like this.&lt;/p&gt;

&lt;p&gt;Every new feature creates another engineering problem to solve.&lt;/p&gt;

&lt;p&gt;I'm &lt;strong&gt;Ehan Siddique&lt;/strong&gt;, and I build full-stack web applications while continuing to explore real-time systems, React, Next.js, Firebase and modern web technologies.&lt;/p&gt;

&lt;p&gt;If you'd like to explore ChatLoop, you can find the project, source code and a more detailed case study through my portfolio and GitHub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://ehansiddique.com/projects/chatloop" rel="noopener noreferrer"&gt;https://ehansiddique.com/projects/chatloop&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Fehan999/Chat-Loop" rel="noopener noreferrer"&gt;https://github.com/Fehan999/Chat-Loop&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Live Link:&lt;/strong&gt; &lt;a href="https://chatloop-mu.vercel.app" rel="noopener noreferrer"&gt;https://chatloop-mu.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're working on a real-time application yourself, I'd also be interested to hear what architecture and technologies you're using.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>nextjs</category>
      <category>chatapplication</category>
    </item>
    <item>
      <title>AI Won't Replace Web Developers—But Developers Who Use AI May Replace Those Who Don't</title>
      <dc:creator>Ehan Siddique</dc:creator>
      <pubDate>Thu, 24 Sep 2026 22:53:38 +0000</pubDate>
      <link>https://dev.to/ehan_siddique/ai-wont-replace-web-developers-but-developers-who-use-ai-may-replace-those-who-dont-1dbc</link>
      <guid>https://dev.to/ehan_siddique/ai-wont-replace-web-developers-but-developers-who-use-ai-may-replace-those-who-dont-1dbc</guid>
      <description>&lt;p&gt;The use of artificial intelligence in web development does not signal a threat to the job but a chance for renewal.What this means for web developers is that AI will not take their place but those who don't keep with it may be replaced!&lt;/p&gt;

&lt;p&gt;Several years ago, writing the code with AI was seen as a futuristic concept.&lt;/p&gt;

&lt;p&gt;To-day, AI can create a react component, explain error codes, write SQL queries, build APIs, or suggest UI designs to help developers resolve issues in just seconds.&lt;/p&gt;

&lt;p&gt;Of course, many devs are wondering:&lt;/p&gt;

&lt;p&gt;“Will AI take the place of web developers?”&lt;/p&gt;

&lt;p&gt;My answer is:&lt;/p&gt;

&lt;p&gt;Palpitations aside, AI likely won't overtake web developers as of yet. However, web developers who can harness the power of AI may have the upper hand over those resisting its uptake.&lt;/p&gt;

&lt;p&gt;But that's a whole another story.&lt;/p&gt;

&lt;p&gt;AI affects the actions of developers, as well.AI also impacts what developers do.&lt;/p&gt;

&lt;p&gt;AI is emerging as yet another tool in a developer's kit.&lt;/p&gt;

&lt;p&gt;Just as developers do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;VS Code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Git&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stack Overflow&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;npm&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Documentation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Browser DevTools&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These days, AI can assist with a lot of the development procedure.&lt;/p&gt;

&lt;p&gt;An AI developer can request the AI to:&lt;/p&gt;

&lt;p&gt;Describe a little known fault&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Generate a basic component&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create boilerplate code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Suggest improvements&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Write tests&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Explain documentation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Find potential bugs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Refactor repetitive code&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Generate SQL queries&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create API examples&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This doesn't mean the developer is no longer needed.&lt;/p&gt;

&lt;p&gt;It means that the developer can focus on actual problem and not repetitive tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  But There's a Problem
&lt;/h2&gt;

&lt;p&gt;AI can write code.&lt;/p&gt;

&lt;p&gt;But, this does not imply that AI is always capable of writing the proper code.&lt;/p&gt;

&lt;p&gt;It's here that developers still have a role to play.&lt;/p&gt;

&lt;p&gt;Suppose you call the AI and say: "Create a system to handle payments".&lt;/p&gt;

&lt;p&gt;It may output code which is then perfectly normal.&lt;/p&gt;

&lt;p&gt;But is it secure?&lt;/p&gt;

&lt;p&gt;Does it provide for failed payments?&lt;/p&gt;

&lt;p&gt;If one of the users refreshes the page?&lt;/p&gt;

&lt;p&gt;Does the request contain a message where it can be meddled with?&lt;/p&gt;

&lt;p&gt;Is it possible for sensitive credentials to be disclosed?&lt;/p&gt;

&lt;p&gt;Is it up-to-date with the payment provider's latest requirements?&lt;/p&gt;

&lt;p&gt;These are NOT just coding questions.&lt;/p&gt;

&lt;p&gt;These are engineering / business questions.&lt;/p&gt;

&lt;p&gt;It is the developer's responsibility to know what the system should do and how to ensure that it performs its duties properly.&lt;/p&gt;

&lt;p&gt;It is significant that the Skill is no longer “Writing Code”, but is “Solving Problems”.&lt;/p&gt;

&lt;p&gt;Perhaps the largest change AI is making in development is this.&lt;/p&gt;

&lt;p&gt;Knowledge of Coding used to provide a tremendous competitive edge.&lt;/p&gt;

&lt;p&gt;It's still important.&lt;/p&gt;

&lt;p&gt;However, more and more, the developers must also understand:&lt;/p&gt;

&lt;p&gt;What should I construct?&lt;/p&gt;

&lt;p&gt;Why needs to I construct it in this method?&lt;/p&gt;

&lt;p&gt;What are some potential issues?&lt;/p&gt;

&lt;p&gt;How to scale this system up?&lt;/p&gt;

&lt;p&gt;How should the users interact with it?&lt;/p&gt;

&lt;p&gt;How can I be sure the AI-generated code is correct?&lt;/p&gt;

&lt;p&gt;The increasingly valuable competencies of good questioning, understanding systems, testing solutions, and making technical decisions are all essential.&lt;/p&gt;

&lt;p&gt;You can make a good developer faster with AI.AI can help make a good developer faster.&lt;/p&gt;

&lt;p&gt;Now consider two developers.&lt;/p&gt;

&lt;p&gt;Developer A is against AI.&lt;/p&gt;

&lt;p&gt;Developer B is familiar with the use of AI and has also a solid understanding of programming basics.&lt;/p&gt;

&lt;p&gt;Provide the same project to both the developers.&lt;/p&gt;

&lt;p&gt;Developer B could rely on AI to generate boilerplate rapidly, analyze errors, experiment with various solutions and automate repetitive tasks.&lt;/p&gt;

&lt;p&gt;But, he/she still reviews the code, tests it and gives feedback, asks for changes, and makes final decisions, which is Developer B.&lt;/p&gt;

&lt;p&gt;That's the true power.&lt;/p&gt;

&lt;p&gt;AI isn't necessarily replacing the developer.&lt;/p&gt;

&lt;p&gt;It's putting more money in the hands of the developer.&lt;/p&gt;

&lt;p&gt;However, don't become reliant on AI.&lt;/p&gt;

&lt;p&gt;But, there's another way of thinking about this.&lt;/p&gt;

&lt;p&gt;If you don't comprehend the code AI produces, you're not really in control of your project.&lt;/p&gt;

&lt;p&gt;It just has to be hoped that the code produced is functional.&lt;/p&gt;

&lt;p&gt;That's dangerous.&lt;/p&gt;

&lt;p&gt;However, a developer needs to know basic concepts like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;JavaScript&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HTML/CSS&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;APIs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Databases&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authentication&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Git&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;HTTP&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Security&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Debugging&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Software architecture&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not necessary to memorise all codes.&lt;/p&gt;

&lt;p&gt;However, it is important to understand what the code is doing and why it works.&lt;/p&gt;

&lt;p&gt;The coming developer will not be “AI vs Developer”.&lt;/p&gt;

&lt;p&gt;I don't see the future on:&lt;/p&gt;

&lt;p&gt;AI → replaces developers&lt;/p&gt;

&lt;p&gt;I believe it's more like:&lt;/p&gt;

&lt;p&gt;Developer + AI = Improved developers process.&lt;/p&gt;

&lt;p&gt;AI can do more of the repetitive aspects.&lt;/p&gt;

&lt;p&gt;For developers, they can now better concentrate on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Architecture&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;User experience&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Business logic&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Security&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Performance&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Problem solving&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Communication&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Product decisions&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This doesn't debase the very value of developers.&lt;/p&gt;

&lt;p&gt;It alters their value's origin.&lt;/p&gt;

&lt;p&gt;So what does this mean for developers?&lt;/p&gt;

&lt;p&gt;Don't panic.&lt;/p&gt;

&lt;p&gt;AI shouldn't be overlooked, either.&lt;/p&gt;

&lt;p&gt;Know how to use it.&lt;/p&gt;

&lt;p&gt;Start small.&lt;/p&gt;

&lt;p&gt;When you encounter an error that you don't understand, explain it using AI.&lt;/p&gt;

&lt;p&gt;Request it to evaluate a function.&lt;/p&gt;

&lt;p&gt;Make use of it for creating test cases.&lt;/p&gt;

&lt;p&gt;Seek other solutions to a problem.&lt;/p&gt;

&lt;p&gt;Apply it to comprehend documentation.&lt;/p&gt;

&lt;p&gt;Then verify everything.&lt;/p&gt;

&lt;p&gt;It must not be the objective:&lt;/p&gt;

&lt;p&gt;“Let the Artificial Intelligence construct everything for me”.&lt;/p&gt;

&lt;p&gt;The goal should be:&lt;/p&gt;

&lt;p&gt;“How might AI assist me in enhancing my coding capabilities and improving productivity?”&lt;/p&gt;

&lt;p&gt;The difficulty in comprehending the impact of the novel.The challenge of understanding the novel.&lt;/p&gt;

&lt;p&gt;I think that the people who will get the biggest return in using AI will not be the ones the most frantically using all the AI tools.&lt;/p&gt;

&lt;p&gt;They will be the developers that mix:&lt;/p&gt;

&lt;p&gt;Strong fundamentals + AI skills + problem-solving + knowledge of the business.&lt;/p&gt;

&lt;p&gt;It is a far greater combination compared to any one AI tool.&lt;/p&gt;

&lt;p&gt;The transformation of web development due to AI is in full effect.&lt;/p&gt;

&lt;p&gt;However, technology always-always change development.&lt;/p&gt;

&lt;p&gt;Those who adapt will have a chance to be faster, more productive and more valuable developers.&lt;/p&gt;

&lt;p&gt;Thus, rather than Asking:&lt;/p&gt;

&lt;p&gt;“Will AI take my job?”&lt;/p&gt;

&lt;p&gt;Perhaps a more pertinent question is:&lt;/p&gt;

&lt;p&gt;“How to harness the power of AI and become an irreplaceable developer?”&lt;/p&gt;




&lt;p&gt;What are your views?&lt;/p&gt;

&lt;p&gt;Will AI ever take over certain aspects of web development or just alter the way developers function?&lt;/p&gt;

&lt;p&gt;I would be very happy to have your comments at the end of the post.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>developer</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>JWT Refresh Tokens: How They Make Login Systems Safer and More Practical</title>
      <dc:creator>Ehan Siddique</dc:creator>
      <pubDate>Wed, 23 Sep 2026 04:20:34 +0000</pubDate>
      <link>https://dev.to/ehan_siddique/jwt-refresh-tokens-how-they-make-login-systems-safer-and-more-practical-dn4</link>
      <guid>https://dev.to/ehan_siddique/jwt-refresh-tokens-how-they-make-login-systems-safer-and-more-practical-dn4</guid>
      <description>&lt;p&gt;When building a modern web application, authentication is one of the first things we need to think about.&lt;/p&gt;

&lt;p&gt;A simple login system sounds easy:&lt;/p&gt;

&lt;p&gt;User enters email and password → server checks them → user is logged in.&lt;/p&gt;

&lt;p&gt;But what happens after the login?&lt;/p&gt;

&lt;p&gt;How does the website remember that the user is authenticated? How long should the login remain valid? What happens if an authentication token gets stolen?&lt;/p&gt;

&lt;p&gt;This is where JWT access tokens and refresh tokens become useful.&lt;/p&gt;

&lt;p&gt;What is a JWT?&lt;/p&gt;

&lt;p&gt;JWT stands for JSON Web Token.&lt;/p&gt;

&lt;p&gt;It is a compact token that can be used to represent authenticated information between a client and a server.&lt;/p&gt;

&lt;p&gt;After a successful login, the server can generate an access token:&lt;/p&gt;

&lt;p&gt;User → Login&lt;br&gt;
      ↓&lt;br&gt;
Server verifies credentials&lt;br&gt;
      ↓&lt;br&gt;
Server generates JWT&lt;br&gt;
      ↓&lt;br&gt;
Client receives token&lt;/p&gt;

&lt;p&gt;The client can then send the access token with requests to protected APIs.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Authorization: Bearer &lt;/p&gt;

&lt;p&gt;The server verifies the token and decides whether the request is authenticated.&lt;/p&gt;

&lt;p&gt;So what's the problem with a normal JWT?&lt;/p&gt;

&lt;p&gt;The main problem is token lifetime.&lt;/p&gt;

&lt;p&gt;Imagine giving a user an access token that remains valid for 30 days.&lt;/p&gt;

&lt;p&gt;That's convenient, but it creates a security problem.&lt;/p&gt;

&lt;p&gt;If someone steals that token, they may be able to use it until it expires.&lt;/p&gt;

&lt;p&gt;On the other hand, if we make the access token expire after 5 or 10 minutes, security improves, but users would constantly have to log in again.&lt;/p&gt;

&lt;p&gt;That's not a great user experience.&lt;/p&gt;

&lt;p&gt;This is where the refresh token comes in.&lt;/p&gt;

&lt;p&gt;What is a Refresh Token?&lt;/p&gt;

&lt;p&gt;A refresh token is a longer-lived credential that can be used to obtain a new access token without asking the user to log in again.&lt;/p&gt;

&lt;p&gt;Instead of having one token do everything, we separate the responsibilities:&lt;/p&gt;

&lt;p&gt;Access Token&lt;/p&gt;

&lt;p&gt;Short-lived&lt;br&gt;
Used for API requests&lt;br&gt;
Expires quickly&lt;/p&gt;

&lt;p&gt;Refresh Token&lt;/p&gt;

&lt;p&gt;Longer-lived&lt;br&gt;
Used to obtain new access tokens&lt;br&gt;
Should be protected carefully&lt;br&gt;
Usually isn't sent with every API request&lt;/p&gt;

&lt;p&gt;The basic idea looks like this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;            LOGIN
              │
              ▼
      ┌─────────────────┐
      │ Server verifies │
      │   credentials   │
      └────────┬────────┘
               │
        ┌──────┴──────┐
        ▼             ▼
  Access Token   Refresh Token
   Short-lived    Long-lived
        │             │
        ▼             │
   API Requests       │
                      │
                Access token
                   expires
                      │
                      ▼
              Refresh request
                      │
                      ▼
              New access token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The user doesn't need to enter their password again.&lt;/p&gt;

&lt;p&gt;Why Not Just Use a Long-Lived JWT?&lt;/p&gt;

&lt;p&gt;This is one of the biggest reasons refresh tokens exist.&lt;/p&gt;

&lt;p&gt;Suppose your access token is valid for 30 days.&lt;/p&gt;

&lt;p&gt;If it gets stolen, the attacker may have a usable credential for a long period.&lt;/p&gt;

&lt;p&gt;Instead, you could make the access token short-lived:&lt;/p&gt;

&lt;p&gt;Access Token → 10–15 minutes&lt;br&gt;
Refresh Token → days/weeks&lt;/p&gt;

&lt;p&gt;Now, even if an access token is compromised, its useful lifetime is much shorter.&lt;/p&gt;

&lt;p&gt;The refresh token is more sensitive, so it needs stronger protection and should be handled carefully.&lt;/p&gt;

&lt;p&gt;How the Login Flow Works&lt;/p&gt;

&lt;p&gt;Let's imagine a user logs into an application.&lt;/p&gt;

&lt;p&gt;Step 1 — User submits credentials&lt;br&gt;
POST /api/login&lt;br&gt;
{&lt;br&gt;
  "email": "&lt;a href="mailto:user@example.com"&gt;user@example.com&lt;/a&gt;",&lt;br&gt;
  "password": "password"&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The server verifies the credentials.&lt;/p&gt;

&lt;p&gt;Step 2 — Server generates tokens&lt;/p&gt;

&lt;p&gt;If authentication succeeds:&lt;/p&gt;

&lt;p&gt;Access Token&lt;br&gt;
+&lt;br&gt;
Refresh Token&lt;/p&gt;

&lt;p&gt;The access token is used for normal API requests.&lt;/p&gt;

&lt;p&gt;The refresh token is kept in a safer storage mechanism, commonly an HttpOnly, Secure cookie for browser applications.&lt;/p&gt;

&lt;p&gt;Step 3 — User accesses protected resources&lt;/p&gt;

&lt;p&gt;The client sends the access token:&lt;/p&gt;

&lt;p&gt;GET /api/profile&lt;/p&gt;

&lt;p&gt;Authorization: Bearer &lt;/p&gt;

&lt;p&gt;The server verifies it.&lt;/p&gt;

&lt;p&gt;If valid:&lt;/p&gt;

&lt;p&gt;200 OK&lt;br&gt;
Step 4 — Access token expires&lt;/p&gt;

&lt;p&gt;Eventually:&lt;/p&gt;

&lt;p&gt;Access Token → EXPIRED&lt;/p&gt;

&lt;p&gt;Instead of showing the login page immediately, the application can use the refresh token.&lt;/p&gt;

&lt;p&gt;Step 5 — Refresh the session&lt;/p&gt;

&lt;p&gt;The client sends a request such as:&lt;/p&gt;

&lt;p&gt;POST /api/refresh&lt;/p&gt;

&lt;p&gt;The server validates the refresh token.&lt;/p&gt;

&lt;p&gt;If everything is valid, it issues a new access token.&lt;/p&gt;

&lt;p&gt;Old Access Token → Expired&lt;/p&gt;

&lt;p&gt;Refresh Token&lt;br&gt;
       ↓&lt;br&gt;
Server validates&lt;br&gt;
       ↓&lt;br&gt;
New Access Token&lt;/p&gt;

&lt;p&gt;The user can continue using the website without logging in again.&lt;/p&gt;

&lt;p&gt;Why This Matters for Website Owners&lt;/p&gt;

&lt;p&gt;For website owners and developers, refresh tokens aren't just about making authentication more complicated.&lt;/p&gt;

&lt;p&gt;They can provide a better balance between security and user experience.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Shorter exposure for access tokens&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Access tokens can have short lifetimes.&lt;/p&gt;

&lt;p&gt;This reduces the period during which a stolen access token remains usable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Better user experience&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Users don't have to repeatedly enter their passwords.&lt;/p&gt;

&lt;p&gt;They can remain signed in while the application silently obtains new access tokens.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Better session control&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A properly designed refresh-token system can allow the server to revoke sessions.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;User clicks "Log out from all devices"&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;         ↓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Server invalidates refresh tokens&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;         ↓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Existing sessions can no longer&lt;br&gt;
refresh themselves&lt;/p&gt;

&lt;p&gt;This is much more useful than simply waiting for a long-lived token to expire.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Device/session management&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A website can maintain separate sessions for different devices.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Chrome - Malaysia&lt;br&gt;
iPhone - Malaysia&lt;br&gt;
Laptop - Bangladesh&lt;/p&gt;

&lt;p&gt;Each session can have its own refresh token.&lt;/p&gt;

&lt;p&gt;If one device is lost, its session can be revoked without necessarily logging the user out everywhere.&lt;/p&gt;

&lt;p&gt;Access Token vs Refresh Token&lt;br&gt;
Feature Access Token    Refresh Token&lt;br&gt;
Main purpose    Access APIs Obtain new access tokens&lt;br&gt;
Lifetime    Short   Longer&lt;br&gt;
Sent with API requests  Usually yes Usually no&lt;br&gt;
Security sensitivity    High    Very high&lt;br&gt;
Can be revoked server-side  Depends on design   Commonly&lt;br&gt;
Should be exposed to JavaScript Minimize exposure   Prefer HttpOnly cookie&lt;/p&gt;

&lt;p&gt;The exact implementation depends on the application's architecture and threat model.&lt;/p&gt;

&lt;p&gt;Important Security Practices&lt;/p&gt;

&lt;p&gt;JWT itself doesn't automatically make an authentication system secure.&lt;/p&gt;

&lt;p&gt;The implementation matters.&lt;/p&gt;

&lt;p&gt;Use HTTPS&lt;/p&gt;

&lt;p&gt;Never send authentication credentials or tokens over an unencrypted HTTP connection.&lt;/p&gt;

&lt;p&gt;Use HTTPS/TLS in production.&lt;/p&gt;

&lt;p&gt;Keep access tokens short-lived&lt;/p&gt;

&lt;p&gt;Don't make an access token valid for an unnecessarily long period.&lt;/p&gt;

&lt;p&gt;A short lifetime limits the damage from token theft.&lt;/p&gt;

&lt;p&gt;Protect refresh tokens&lt;/p&gt;

&lt;p&gt;For browser-based applications, an HttpOnly + Secure cookie is commonly used.&lt;/p&gt;

&lt;p&gt;HttpOnly helps prevent JavaScript from directly reading the cookie.&lt;/p&gt;

&lt;p&gt;Secure ensures the cookie is sent only over HTTPS.&lt;/p&gt;

&lt;p&gt;Depending on the application, SameSite settings should also be configured appropriately.&lt;/p&gt;

&lt;p&gt;Consider refresh-token rotation&lt;/p&gt;

&lt;p&gt;Instead of reusing the exact same refresh token forever, the server can issue a new refresh token whenever one is successfully used.&lt;/p&gt;

&lt;p&gt;The previous token can then be invalidated.&lt;/p&gt;

&lt;p&gt;This is known as refresh-token rotation.&lt;/p&gt;

&lt;p&gt;It can help detect and limit certain replay attacks.&lt;/p&gt;

&lt;p&gt;Never store passwords inside JWTs&lt;/p&gt;

&lt;p&gt;A JWT should never contain the user's password.&lt;/p&gt;

&lt;p&gt;Passwords should be stored using an appropriate password hashing algorithm such as Argon2id or bcrypt.&lt;/p&gt;

&lt;p&gt;Validate tokens properly&lt;/p&gt;

&lt;p&gt;The server should verify important JWT properties such as:&lt;/p&gt;

&lt;p&gt;Signature&lt;br&gt;
Expiration&lt;br&gt;
Issuer&lt;br&gt;
Audience&lt;br&gt;
Algorithm&lt;/p&gt;

&lt;p&gt;The exact checks depend on the authentication architecture.&lt;/p&gt;

&lt;p&gt;What Happens When a User Logs Out?&lt;/p&gt;

&lt;p&gt;A common mistake is thinking:&lt;/p&gt;

&lt;p&gt;"Deleting the JWT from the browser means the session is completely gone."&lt;/p&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;If a refresh token remains valid on the server, it may still be possible to create another access token.&lt;/p&gt;

&lt;p&gt;A stronger logout design can invalidate the user's refresh session.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Logout&lt;br&gt;
  ↓&lt;br&gt;
Delete/clear client session&lt;br&gt;
  ↓&lt;br&gt;
Revoke refresh token&lt;br&gt;
  ↓&lt;br&gt;
Future refresh attempts fail&lt;/p&gt;

&lt;p&gt;This gives the server more control over active sessions.&lt;/p&gt;

&lt;p&gt;JWT Is Not a Complete Authentication System&lt;/p&gt;

&lt;p&gt;This is probably the most important point.&lt;/p&gt;

&lt;p&gt;JWT is simply a token format.&lt;/p&gt;

&lt;p&gt;Using JWT doesn't automatically mean an application has secure authentication.&lt;/p&gt;

&lt;p&gt;A secure authentication system also needs to consider:&lt;/p&gt;

&lt;p&gt;Password hashing&lt;br&gt;
HTTPS&lt;br&gt;
Token expiration&lt;br&gt;
Secure cookie configuration&lt;br&gt;
CSRF protection where applicable&lt;br&gt;
XSS protection&lt;br&gt;
Refresh-token rotation&lt;br&gt;
Session revocation&lt;br&gt;
Rate limiting&lt;br&gt;
Account recovery&lt;br&gt;
Multi-factor authentication&lt;br&gt;
Proper server-side authorization&lt;/p&gt;

&lt;p&gt;JWT is one part of the system, not the entire system.&lt;/p&gt;

&lt;p&gt;A Simple Mental Model&lt;/p&gt;

&lt;p&gt;I like to think about it this way:&lt;/p&gt;

&lt;p&gt;Access token = temporary access pass&lt;/p&gt;

&lt;p&gt;Refresh token = credential used to obtain another temporary pass&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;LOGIN&lt;br&gt;
  ↓&lt;br&gt;
Access Token ──────────→ API&lt;br&gt;
  │&lt;br&gt;
  │ expires&lt;br&gt;
  ↓&lt;br&gt;
Refresh Token&lt;br&gt;
  │&lt;br&gt;
  ↓&lt;br&gt;
New Access Token&lt;br&gt;
  │&lt;br&gt;
  ↓&lt;br&gt;
API&lt;/p&gt;

&lt;p&gt;The user experiences one continuous session, while the application can keep the actual API credential short-lived.&lt;/p&gt;

&lt;p&gt;Final Thoughts&lt;/p&gt;

&lt;p&gt;When building a modern application, authentication is not just about making the login button work.&lt;/p&gt;

&lt;p&gt;It's about deciding:&lt;/p&gt;

&lt;p&gt;How long should a session last?&lt;/p&gt;

&lt;p&gt;What happens if a token is stolen?&lt;/p&gt;

&lt;p&gt;How can a user stay logged in without keeping a powerful credential valid forever?&lt;/p&gt;

&lt;p&gt;Refresh tokens provide one way to solve this problem.&lt;/p&gt;

&lt;p&gt;Used correctly, the combination of short-lived access tokens + carefully protected refresh tokens + proper session management can give developers a practical balance between security and user experience.&lt;/p&gt;

&lt;p&gt;But the important part is not simply adding JWT.&lt;/p&gt;

&lt;p&gt;The real security comes from designing the entire authentication flow carefully.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8eur7b24bzx6o5oc2xlo.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8eur7b24bzx6o5oc2xlo.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>webdev</category>
      <category>programming</category>
      <category>jwt</category>
    </item>
    <item>
      <title>So what should it be – WordPress or custom code? 19 questions every business owner needs to answer - The truth.</title>
      <dc:creator>Ehan Siddique</dc:creator>
      <pubDate>Thu, 20 Aug 2026 17:09:36 +0000</pubDate>
      <link>https://dev.to/ehan_siddique/so-what-should-it-be-wordpress-or-custom-code-19-questions-every-business-owner-needs-to-answer-2edd</link>
      <guid>https://dev.to/ehan_siddique/so-what-should-it-be-wordpress-or-custom-code-19-questions-every-business-owner-needs-to-answer-2edd</guid>
      <description>&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2vstps48rhwzodgcev8z.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2vstps48rhwzodgcev8z.jpg" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;em&gt;“WordPress or custom coded?”&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
It is amongst the most recurrent queries businessmen usually ask while launching a new web page.&lt;/p&gt;

&lt;p&gt;Plus, everyone will most likely say something completely different.&lt;/p&gt;

&lt;p&gt;Some will tell you:&lt;/p&gt;

&lt;p&gt;“WordPress is BASIC and OLD.”&lt;/p&gt;

&lt;p&gt;Others will say:&lt;/p&gt;

&lt;p&gt;“No, it is not necessary to make custom development – you can use WordPress.”&lt;/p&gt;

&lt;p&gt;The reality is, however, that:&lt;/p&gt;

&lt;p&gt;There is no right or wrong answer.&lt;/p&gt;

&lt;p&gt;This will entirely rely on your business, objectives, budget and the true scope of functionality that you need your website to have.&lt;/p&gt;

&lt;p&gt;What once started out as a “cheap website” is now considered by many as the most powerful blogging platform available online.&lt;/p&gt;

&lt;p&gt;WordPress is not so easy to comprehend as people perceive it.&lt;/p&gt;

&lt;p&gt;Yes, it can (affordably) be used to create web sites rapidly. Doesn't imply it implies that WordPress sites are low quality though.&lt;/p&gt;

&lt;p&gt;The best-developed WordPress website can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Fast&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Secure&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SEO-friendly&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mobile-friendly&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easy to manage&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Highly customizable&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WordPress can be a great option for businesses because of its properties, such as a company website, portfolio, blog, landing page or online shop.&lt;/p&gt;

&lt;p&gt;Its great benefit is content management.&lt;/p&gt;

&lt;p&gt;Update pages, publish blogs, add products, change images and manage content without having to rely on a developer for each and every little bit of change.&lt;/p&gt;

&lt;p&gt;That is very spacious to lots of businesses.&lt;/p&gt;

&lt;p&gt;But what about web sites that are programmed in a custom code?&lt;/p&gt;

&lt;p&gt;With custom development, the web or app will have a great deal of control over the websites or apps operation.&lt;/p&gt;

&lt;p&gt;You don't have to conform to a pre-existing CMS and plugin community - instead, its own system can be tailored to suit your business.&lt;/p&gt;

&lt;p&gt;This comes in handy when creating something more elaborate like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;SaaS platforms&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Marketplaces&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Customer portals&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Advanced dashboards&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Booking platforms&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Complex business systems&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Real-time applications&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Standard WordPress tools may not work efficiently to suit unique functionality or workflow that your website has; in such instances, custom development may prove a better option.&lt;/p&gt;

&lt;p&gt;What About Which one is Better?&lt;/p&gt;

&lt;p&gt;There is the response that not many engineers don't want to have:&lt;/p&gt;

&lt;p&gt;It depends.&lt;/p&gt;

&lt;p&gt;With a website that needs a menu, location, contact info and restaurant bookings and has a blog, an expensive fully bespoke website may not be a feasible option for the restaurant owner.&lt;/p&gt;

&lt;p&gt;WordPress can provide you with all you require.&lt;/p&gt;

&lt;p&gt;However, in the case of an application that lots of users will be using each of whom will be creating accounts and developing business logic, managing dashboards and transactions, and interacting with other accounts, custom development might be the more intelligent option.&lt;/p&gt;

&lt;p&gt;The object is NOT to pick a new high tech solution.&lt;/p&gt;

&lt;p&gt;The objective is to select the technology that will be able to effectively solve your company's problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 3 Biggest Myths
&lt;/h2&gt;

&lt;p&gt;The fact that WordPress does not have good reputation for its performance.“WordPress is always slow.”&lt;/p&gt;

&lt;p&gt;Not true.&lt;/p&gt;

&lt;p&gt;Just as a poorly-constructed custom site can be slow, so, too, can a poorly-constructed WordPress site.&lt;/p&gt;

&lt;p&gt;Things like hosting, image optimization, code quality, database configuration, plugins and caching, and third-party scripts, are among the factors that impact performance.&lt;/p&gt;

&lt;p&gt;The most important thing to understand, and doing so will help your decision, is that “WordPress” or “custom” are not the things that matter most, GOOD DEVELOPMENT does.&lt;/p&gt;

&lt;p&gt;“The second myth is that ‘Custom code is always more secure'”.&lt;/p&gt;

&lt;p&gt;Not automatically.&lt;/p&gt;

&lt;p&gt;Even with custom applications, these can be vulnerable to security issues if not managed properly – including authentication, API, database, permissions and/or dependencies.&lt;/p&gt;

&lt;p&gt;Proper updating, secure plugins, robust authentication, backups and monitoring are also required for WordPress.&lt;/p&gt;

&lt;p&gt;Security is not simply a choice of technology, it is a part of development and maintenance.&lt;/p&gt;

&lt;p&gt;Myth #3: “Custom code is always more professional.”&lt;/p&gt;

&lt;p&gt;However, a custom-coded site does not necessarily mean that it is better and, therefore, worth the time or money.&lt;/p&gt;

&lt;p&gt;Use of WordPress isn't a compromise if it's getting the job done in solving your business needs.&lt;/p&gt;

&lt;p&gt;Indeed, it isn't necessarily the more complex option if it achieves what you're after which is more professional.&lt;/p&gt;

&lt;p&gt;People often focus just on the up-front development charges and not what the long-term consequences are.&lt;/p&gt;

&lt;p&gt;The first error done by business entities is their taking just the starting price.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;WordPress: Lower up-front development expenses.&lt;/p&gt;

&lt;p&gt;Complexity: Custom means that more money will be required for the initial development.&lt;/p&gt;

&lt;p&gt;However, there's more to that.&lt;/p&gt;

&lt;p&gt;If that's the case, you must factor in also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Hosting&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Maintenance&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Security&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Updates&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Future features&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Developer support&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scalability&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A website that is continually needing repairs and is cheap isn't necessarily cheap.&lt;/p&gt;

&lt;p&gt;You don't want to have a costly custom-built system with components that you never utilize either.&lt;/p&gt;

&lt;p&gt;Consider lifetime costs, rather than just development costs.&lt;/p&gt;

&lt;p&gt;What should your company opt for?&lt;/p&gt;

&lt;p&gt;If you are in need of, WordPress might be better suited for you:&lt;/p&gt;

&lt;p&gt;✅ A company website&lt;/p&gt;

&lt;p&gt;✅ A portfolio&lt;/p&gt;

&lt;p&gt;✅ A blog&lt;/p&gt;

&lt;p&gt;✅ A landing page&lt;/p&gt;

&lt;p&gt;A website that's based on providing a service.&lt;/p&gt;

&lt;p&gt;Online shop – with small to medium size clientele – = ✅&lt;/p&gt;

&lt;p&gt;✅ Easy content management&lt;/p&gt;

&lt;p&gt;🏃 A fast start, in comparison to the other projects.&lt;/p&gt;

&lt;p&gt;If you need things such as: then it may be better to opt for tailor-made development:&lt;/p&gt;

&lt;p&gt;✅ A SaaS product&lt;/p&gt;

&lt;p&gt;✅ A complex marketplace&lt;/p&gt;

&lt;p&gt;✅ Advanced dashboards&lt;/p&gt;

&lt;p&gt;✅ Unique business workflows&lt;/p&gt;

&lt;p&gt;✅ Complex integrations&lt;/p&gt;

&lt;p&gt;✅ Real-time functionality&lt;/p&gt;

&lt;p&gt;✅ Positive use cases to showcase the fit with the operation or industry of your interest&lt;/p&gt;

&lt;p&gt;✅ A system in need of a full system control with the architecture.&lt;/p&gt;

&lt;p&gt;Occasionally you may be able to select more than one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hybrid Approach
&lt;/h2&gt;

&lt;p&gt;It's one of the elements that many businesses might not have considered.&lt;/p&gt;

&lt;p&gt;You can also utilize WordPress for your marketing site and website and use a customized made application for complicated features.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;yourbusiness.com → WordPress&lt;/p&gt;

&lt;p&gt;app.yourbusiness.com → Custom application&lt;/p&gt;

&lt;p&gt;This helps marketers quickly make changes to the website content and the development team to create website-specific functions.&lt;/p&gt;

&lt;p&gt;At times, this is the most convenient alternative.&lt;/p&gt;

&lt;p&gt;In this video, you'll learn the questions you need to ask before hiring a developer.This video is about the questions you should ask prior to hiring a developer.&lt;/p&gt;

&lt;p&gt;Avoid beginning with questions:&lt;/p&gt;

&lt;p&gt;“Let's figure out is it better to use WordPress or to pay attention to custom code?”&lt;/p&gt;

&lt;p&gt;Decide upon the following question starters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What are the requirements of my business?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Avoid paying for features that you don't need to use.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Who will handle maintenance of the Web site?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When frequent updates to content are a regular occurrence for your team, ease-of-management is key.&lt;/p&gt;

&lt;p&gt;**3. What is the anticipated increase in the size of the business?&lt;/p&gt;

&lt;p&gt;The technology you select is for the future, so your future needs should be taken into consideration.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What are maintenance fees going to be? &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Inquire about updates, hosting, security and backups and ongoing support.&lt;/p&gt;

&lt;h1&gt;
  
  
  5: Does the site have potential to expand as my company expands?
&lt;/h1&gt;

&lt;p&gt;You shouldn't be stuck with a website that will not do the job when your enterprise grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;“WordPress isn't necessarily bad.”&lt;/p&gt;

&lt;p&gt;This doesn't mean that custom code has to be better.&lt;/p&gt;

&lt;p&gt;Especially, because the most costly web page does not always necessarily mean the best!&lt;/p&gt;

&lt;p&gt;The top Web page will be the one that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Helps to solve your business problem&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Provides a nice customer experience&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Performs reliably&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Does not need extensive cleaning up.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Can be adapted to suit a business's needs and scale of operations as it grows&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fits your budget&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Therefore, before selecting any technology know what your business needs are.&lt;/p&gt;

&lt;p&gt;Do not create an internet site due to the fact that a developer claims the site is “better” doing his or her intervention.&lt;/p&gt;

&lt;p&gt;Create a website for your business that it really requires.&lt;/p&gt;




&lt;p&gt;Would you opt for WordPress, or is it completely developed? Or is it a mix of the two?&lt;/p&gt;

&lt;p&gt;If you have any comments feel free to write me.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>fullstack</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
