<?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: DanitZ</title>
    <description>The latest articles on DEV Community by DanitZ (@stoichotoke).</description>
    <link>https://dev.to/stoichotoke</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%2F3947702%2F4b8cf726-4aaf-47c3-a73a-1875980066bd.png</url>
      <title>DEV Community: DanitZ</title>
      <link>https://dev.to/stoichotoke</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/stoichotoke"/>
    <language>en</language>
    <item>
      <title>Building Vidya: An Ultra-Fast Bilingual Voice AI Tutor with Murf Falcon &amp; LiveKit (10 Days of Voice Agents)</title>
      <dc:creator>DanitZ</dc:creator>
      <pubDate>Sat, 15 Aug 2026 10:04:02 +0000</pubDate>
      <link>https://dev.to/stoichotoke/building-vidya-an-ultra-fast-bilingual-voice-ai-tutor-with-murf-falcon-livekit-10-days-of-voice-1fcf</link>
      <guid>https://dev.to/stoichotoke/building-vidya-an-ultra-fast-bilingual-voice-ai-tutor-with-murf-falcon-livekit-10-days-of-voice-1fcf</guid>
      <description>&lt;h1&gt;
  
  
  Building Vidya: An Ultra-Fast Bilingual Voice AI Tutor.
&lt;/h1&gt;

&lt;p&gt;Over the past 10 days, as part of the &lt;strong&gt;#VoiceForBharat 10 Days of Voice Agents Challenge&lt;/strong&gt;, I built &lt;strong&gt;Vidya&lt;/strong&gt; — a real-time, bilingual (English/Hindi) AI Voice Tutor designed to make learning interactive, accessible, and human-like for students across India.&lt;/p&gt;

&lt;p&gt;In this post, I’ll share the story of how Vidya came to life, dive into the architecture behind ultra-low latency voice agents, highlight the key features built over the 10 days, discuss the toughest challenges faced, and walk you through building your own production-ready voice agent using &lt;strong&gt;Murf Falcon TTS&lt;/strong&gt;, &lt;strong&gt;LiveKit Agents&lt;/strong&gt;, &lt;strong&gt;Deepgram STT&lt;/strong&gt;, and &lt;strong&gt;Google Gemini&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Why Voice? Meet Vidya 🇮🇳
&lt;/h2&gt;

&lt;p&gt;In India, text-based educational platforms often face a steep digital literacy and language barrier. Millions of learners feel intimidated by typing long queries or struggling through English-only user interfaces. Voice unlocks immediate, natural, and hands-free learning — allowing students to speak naturally in &lt;strong&gt;English, Hindi, or code-mixed Hinglish&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vidya&lt;/strong&gt; serves as a personal AI learning companion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem Solved:&lt;/strong&gt; Overcomes literacy and language barriers by providing empathetic, spoken literacy and science lessons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track &amp;amp; Target Audience:&lt;/strong&gt; Education &amp;amp; Accessibility for students, young learners, and non-native English speakers across India.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why Voice is Essential:&lt;/strong&gt; Voice removes UI friction. Learners can simply talk to Vidya, attempt spoken exercises, ask questions, and receive instant, warm, spoken feedback in a natural Indian accent.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Key Features Built Across the 10-Day Journey
&lt;/h2&gt;

&lt;p&gt;Over 10 intensive days, Vidya grew from a basic echo bot into a multi-agent system equipped with tools, memory, telephony, analytics, and safety guardrails:&lt;/p&gt;

&lt;h3&gt;
  
  
  🎙️ 1. Ultra-Low Latency Indian Voice Powered by Murf Falcon
&lt;/h3&gt;

&lt;p&gt;Using &lt;strong&gt;Murf Falcon TTS&lt;/strong&gt; (&lt;code&gt;livekit-murf&lt;/code&gt;), Vidya speaks with a natural, conversational Indian voice (&lt;code&gt;Anisha&lt;/code&gt;). Streaming TTS with sentence tokenization (&lt;code&gt;min_sentence_len=2&lt;/code&gt;) and text pacing delivers speech chunks with sub-second latency, giving the agent a human-like flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  🇮🇳 2. Code-Mixed (Hinglish) &amp;amp; Bilingual Support
&lt;/h3&gt;

&lt;p&gt;Powered by &lt;strong&gt;Deepgram Nova-3 STT&lt;/strong&gt; (&lt;code&gt;language="multi"&lt;/code&gt;) and &lt;strong&gt;Google Gemini&lt;/strong&gt;, Vidya fluently handles English, Hindi in Devanagari script, and code-mixed Hinglish phrases (e.g., &lt;em&gt;"Namaste! Aaj hum beginner reading practice karenge."&lt;/em&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 3. Personalized Memory for Returning Users
&lt;/h3&gt;

&lt;p&gt;Vidya remembers returning students! Using a persistent profile store (&lt;code&gt;user_store.py&lt;/code&gt;), Vidya recalls the user's name, preferred language, current learning level, and last interaction date, greeting them warmly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Namaste Aarav, welcome back! You were working on beginner exercises. Last seen on August 14."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  🛠️ 4. Interactive Learning Tools &amp;amp; Scraper
&lt;/h3&gt;

&lt;p&gt;Vidya is equipped with specialized function tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fetch_next_exercise&lt;/code&gt;: Retrieves level-appropriate practice prompts tagged with data freshness timestamps (&lt;code&gt;last_updated&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;score_spoken_answer&lt;/code&gt;: Evaluates spoken pronunciations and answers on a 0–100 scale.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;award_learning_star&lt;/code&gt;: Awards virtual gold stars (🌟) to keep learners motivated.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scrape_website&lt;/code&gt;: Fetches live web pages in real-time (&lt;code&gt;web_scraper.py&lt;/code&gt;) for live context extraction.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📞 5. Outbound Telephony (SIP Calls)
&lt;/h3&gt;

&lt;p&gt;Integrated with LiveKit's SIP Trunking (&lt;code&gt;telephony/outbound/dial.py&lt;/code&gt;), Vidya can initiate active outbound phone calls to learners' mobile phones for daily study check-ins and practice sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  🛡️ 6. Human Escalation Engine with Explicit Consent
&lt;/h3&gt;

&lt;p&gt;If a learner is stuck, frustrated, or requests a human teacher, &lt;code&gt;create_escalation&lt;/code&gt; logs an escalation ticket (&lt;code&gt;ESC-12345&lt;/code&gt;) and alerts support staff. Vidya follows strict privacy guardrails — asking for &lt;strong&gt;explicit permission&lt;/strong&gt; before saving any personal details or submitting tickets.&lt;/p&gt;

&lt;h3&gt;
  
  
  📊 7. Call Analytics Dashboard
&lt;/h3&gt;

&lt;p&gt;Session outcomes are tracked in &lt;code&gt;call_store.py&lt;/code&gt; — logging call duration, agent type (browser vs. telephony), completion status (&lt;code&gt;successful&lt;/code&gt; / &lt;code&gt;failed&lt;/code&gt;), and success reasons (&lt;code&gt;exercise_scored&lt;/code&gt;, &lt;code&gt;star_awarded&lt;/code&gt;, &lt;code&gt;escalated_to_human&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  🔄 8. Dynamic Multi-Agent Handoffs
&lt;/h3&gt;

&lt;p&gt;When a student asks a physics question (e.g., &lt;em&gt;"Why does an apple fall from a tree?"&lt;/em&gt;), Vidya seamlessly hands off the conversation to &lt;strong&gt;Dr. Homi (Physics Specialist)&lt;/strong&gt; using LiveKit's &lt;code&gt;context.session.update_agent()&lt;/code&gt;. When physics practice ends, Dr. Homi hands the student back to Vidya for reading practice!&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The Tough Parts: Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building a real-time voice agent isn't just about linking APIs together. Here are three major hurdles faced and solved:&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge #1: Eliminating Awkward Conversational Latency
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Problem:&lt;/strong&gt; Combining STT, LLM, and TTS in sequence created a 2–3 second delay before the agent responded, ruining natural turn-taking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Solution:&lt;/strong&gt; We enabled streaming at every tier — Deepgram Nova-3 streaming STT, Gemini Flash Lite for fast first-token generation, LiveKit's preemptive generation, and &lt;strong&gt;Murf Falcon's low-latency streaming TTS&lt;/strong&gt; with sentence boundary tokenization (&lt;code&gt;min_sentence_len=2&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Challenge #2: Explicit Consent &amp;amp; Privacy in Spoken AI
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Problem:&lt;/strong&gt; Function tools automatically firing to record user profiles or create tickets without the user realizing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Solution:&lt;/strong&gt; We designed prompt-level guardrails requiring Vidya to explicitly ask out loud: &lt;em&gt;"I can remember that for next time — may I save this?"&lt;/em&gt; or &lt;em&gt;"May I share your details with a real teacher?"&lt;/em&gt; before invoking mutation tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Challenge #3: Preserving Session State During Agent Handoffs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Problem:&lt;/strong&gt; Switching from Vidya to Dr. Homi mid-call could cause audio glitches or lose participant state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Solution:&lt;/strong&gt; Leveraged LiveKit Agents SDK's native &lt;code&gt;session.update_agent()&lt;/code&gt; combined with WebRTC data channel events (&lt;code&gt;agent_handoff&lt;/code&gt;) to update the Next.js frontend UI live without dropping the WebRTC room session.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Architecture Overview &amp;amp; Code Highlights
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+------------------+      WebRTC Audio Stream     +---------------------+
|                  |  -------------------------&amp;gt;  |  Deepgram Nova-3    |
|   Learner / UI   |                              |  Streaming STT      |
|  (Next.js App)   |  &amp;lt;-------------------------  +----------+----------+
+--------+---------+      Real-time Audio Out                |
         ^                                                   v
         | RTC Data Channel                       +---------------------+
         | (State &amp;amp; Handoffs)                     |  Google Gemini LLM  |
         |                                        | (Flash Lite Model)  |
         +--------------------------------------  +----------+----------+
                                                             |
                                                             v
                                                  +---------------------+
                                                  |  Murf Falcon TTS    |
                                                  | (Streaming Indian)  |
                                                  +---------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Code Snippet: LiveKit Pipeline with Murf Falcon TTS (&lt;code&gt;agent.py&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;livekit.agents&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AgentSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AgentServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;room_io&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;livekit.plugins&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deepgram&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;murf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;silero&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;noise_cancellation&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;stt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;deepgram&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;STT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nova-3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;multi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LLM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-3.5-flash-lite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;murf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TTS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Anisha&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# Murf Falcon Indian accent voice
&lt;/span&gt;        &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Conversation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SentenceTokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_sentence_len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;text_pacing&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;turn_detection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;MultilingualModel&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;vad&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userdata&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vad&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;preemptive_generation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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;h3&gt;
  
  
  Code Snippet: Dynamic Multi-Agent Handoff (&lt;code&gt;agent.py&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@function_tool&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transfer_to_physics_specialist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Hand off the conversation to Dr. Homi when user asks physics questions.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Handing off conversation to PhysicsSpecialist. Reason: %s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;specialist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PhysicsSpecialist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;specialist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Notify Next.js frontend UI via WebRTC data channel
&lt;/span&gt;    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_handoff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;from_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Vidya (Literacy Tutor)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;to_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dr. Homi (Physics Specialist)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🔄 Switched conversation to Physics Specialist (Dr. Homi)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local_participant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I will connect you to our physics specialist.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5. How to Build Your Own Voice Agent (Quickstart)
&lt;/h2&gt;

&lt;p&gt;Want to build your own voice AI agent? You can clone and run our open-source repository in minutes!&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Clone the Repository
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/hotokeAtlast/murf-livekit-starter.git
&lt;span class="nb"&gt;cd &lt;/span&gt;murf-livekit-starter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Set Up Backend Environment Keys
&lt;/h3&gt;

&lt;p&gt;Copy &lt;code&gt;backend/.env.example&lt;/code&gt; to &lt;code&gt;backend/.env.local&lt;/code&gt; and fill in your keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LIVEKIT_URL=wss://your-livekit-project.livekit.cloud
LIVEKIT_API_KEY=your_key
LIVEKIT_API_SECRET=your_secret
MURF_API_KEY=your_murf_api_key
DEEPGRAM_API_KEY=your_deepgram_api_key
GOOGLE_API_KEY=your_google_gemini_api_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Run the Python Backend
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;backend
uv &lt;span class="nb"&gt;sync
&lt;/span&gt;uv run python src/agent.py dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Run the Next.js Frontend
&lt;/h3&gt;

&lt;p&gt;In a new terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;frontend
pnpm &lt;span class="nb"&gt;install
&lt;/span&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:3000&lt;/code&gt;, click &lt;strong&gt;Connect&lt;/strong&gt;, and start talking to your voice agent!&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Links &amp;amp; Repository
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🐙 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/hotokeAtlast/murf-livekit-starter" rel="noopener noreferrer"&gt;murf-livekit-starter on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🔊 &lt;strong&gt;TTS Engine:&lt;/strong&gt; Powered by &lt;a href="https://murf.ai/api/docs/text-to-speech/streaming" rel="noopener noreferrer"&gt;Murf Falcon Streaming API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Voice Framework:&lt;/strong&gt; Built with &lt;a href="https://docs.livekit.io/agents" rel="noopener noreferrer"&gt;LiveKit Agents SDK&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>ai</category>
      <category>murffalcon</category>
      <category>voiceforbharat</category>
      <category>voiceai</category>
    </item>
    <item>
      <title>🌀 Vortex: From a Firebase Toy to a Real Multi-Vendor Marketplace (Finished, Finally)</title>
      <dc:creator>DanitZ</dc:creator>
      <pubDate>Fri, 05 Jun 2026 11:48:29 +0000</pubDate>
      <link>https://dev.to/stoichotoke/vortex-from-a-firebase-toy-to-a-real-multi-vendor-marketplace-finished-finally-13bc</link>
      <guid>https://dev.to/stoichotoke/vortex-from-a-firebase-toy-to-a-real-multi-vendor-marketplace-finished-finally-13bc</guid>
      <description>&lt;h2&gt;
  
  
  Post body
&lt;/h2&gt;

&lt;p&gt;GitHub "Finish-Up-A-Thon" Challenge Submission&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Vortex&lt;/strong&gt; is a full-stack, multi-vendor e-commerce platform — a "mystical archive" of products from independent sellers, with an admin moderation layer, role-based access, and a real Razorpay checkout.&lt;/p&gt;

&lt;p&gt;It's a React 19 + Vite 8 + Tailwind 4 frontend talking to Firebase (Auth + Firestore) for data and a small Express 5 backend for payments. It started life as a sleepy single-admin Firebase CRUD app and grew into something I'd actually feel comfortable showing a recruiter.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://vortex-n60k.onrender.com/" rel="noopener noreferrer"&gt;https://vortex-n60k.onrender.com/&lt;/a&gt;&lt;br&gt;
🔗 &lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/hotokeAtlast/Vortex" rel="noopener noreferrer"&gt;https://github.com/hotokeAtlast/Vortex&lt;/a&gt;&lt;br&gt;
🔗 &lt;strong&gt;Backend health check:&lt;/strong&gt; &lt;code&gt;GET /api/health&lt;/code&gt; → &lt;code&gt;{ "status": "active", "message": "The Vortex is awake." }&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What's actually in the box
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three-tier role system&lt;/strong&gt; — Buyer, Seller, Admin — with a real &lt;code&gt;users/{uid}&lt;/code&gt; profile document, an &lt;code&gt;isAdmin()&lt;/code&gt; claim derived from email, and an &lt;code&gt;isActiveSeller()&lt;/code&gt; lookup helper used in Firestore rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seller Portal&lt;/strong&gt; — full product CRUD, stock tracking, image upload to Cloudinary, gated by an "active" &lt;code&gt;sellerStatus&lt;/code&gt; so pending sellers can't dump products into the marketplace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin Moderation Panel&lt;/strong&gt; — a single dashboard with stats (pending sellers, review queue, active listings), approve/reject flows for both sellers and products, and content deletion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Razorpay checkout with server-side verification&lt;/strong&gt; — &lt;code&gt;/api/create-order&lt;/code&gt; issues the order, &lt;code&gt;/api/verify-payment&lt;/code&gt; re-derives the HMAC-SHA256 signature on the Express side and only marks the Firestore order as &lt;code&gt;Paid&lt;/code&gt; when the signatures match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;114 lines of Firestore security rules&lt;/strong&gt; — seller-owned product enforcement, admin moderation authority, public read for approved products only, user privacy, and order history isolation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI/UX polish&lt;/strong&gt; — &lt;code&gt;en-IN&lt;/code&gt; currency formatting (₹1,23,456), proper empty states with CTAs, sticky order summary, dark/light theme, mobile-first responsive grid, scroll-to-top, toast notifications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single-service deploy&lt;/strong&gt; — the same Express process serves the Vite-built &lt;code&gt;dist/&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; the &lt;code&gt;/api/*&lt;/code&gt; routes, so one Render Web Service runs the whole thing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stack at a glance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;React 19, Vite 8, Tailwind 4, React Router 7, FontAwesome&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;Firebase Auth, Cloud Firestore, Cloudinary (image upload)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Razorpay (orders + HMAC signature verification)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Node 18+, Express 5, CORS, dotenv&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tooling&lt;/td&gt;
&lt;td&gt;ESLint 9, Vite Rolldown, concurrently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy&lt;/td&gt;
&lt;td&gt;Render (single Web Service hosts the whole monorepo)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://vortex-n60k.onrender.com/" rel="noopener noreferrer"&gt;https://vortex-n60k.onrender.com/&lt;/a&gt;&lt;br&gt;
🔗 &lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/hotokeAtlast/Vortex" rel="noopener noreferrer"&gt;https://github.com/hotokeAtlast/Vortex&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try the three roles in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Buyer&lt;/strong&gt; — sign up with a normal email, browse, add to cart, hit the Razorpay test checkout (use the test card &lt;code&gt;4111 1111 1111 1111&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seller&lt;/strong&gt; — sign up, pick "Seller" during registration, submit a couple of products. They show up in your &lt;code&gt;SellerPortal&lt;/code&gt; immediately as &lt;code&gt;pending&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin&lt;/strong&gt; — sign in with the admin email configured in &lt;code&gt;firestore.rules&lt;/code&gt; (&lt;code&gt;hotoke.atlast@gmail.com&lt;/code&gt;) and use the moderation panel to approve your own products.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Repo quick links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;server/server.cjs&lt;/code&gt; — Express API + static frontend host&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;firestore.rules&lt;/code&gt; — role-based access enforcement&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/pages/SellerPortal.jsx&lt;/code&gt; — seller product management&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/pages/Admin.jsx&lt;/code&gt; — moderation panel&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/pages/Checkout.jsx&lt;/code&gt; — Razorpay integration&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;render.yaml&lt;/code&gt; — single-service deploy config&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Comeback Story
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Where it started
&lt;/h3&gt;

&lt;p&gt;A few months ago, Vortex was a Vite + Firebase project with a single hardcoded admin who could add, edit, and delete products. That was the whole feature set. One user role ("user or the one specific admin email"), a half-finished cart, a checkout that didn't actually charge anyone, no seller concept, no moderation, and a basic Tailwind UI I'd gotten tired of looking at.&lt;/p&gt;

&lt;p&gt;I left it because the obvious next step — letting &lt;em&gt;other&lt;/em&gt; people post products — opened a can of worms. Sellers need their own role, their own dashboard, their own approval flow, and the database needs rules that keep one seller from editing another seller's product. Admin needs a moderation queue. Payments need a server. Deploy needs to actually work. None of it was a Saturday afternoon.&lt;/p&gt;

&lt;p&gt;So the project sat. Hosted, but rotting.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I changed
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Roles&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;user&lt;/code&gt; vs &lt;code&gt;admin&lt;/code&gt; (one hardcoded email)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;buyer&lt;/code&gt; / &lt;code&gt;seller&lt;/code&gt; / &lt;code&gt;admin&lt;/code&gt; with `sellerStatus: pending\&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketplace&lt;/td&gt;
&lt;td&gt;Admin posts products directly&lt;/td&gt;
&lt;td&gt;Sellers apply → admin approves → sellers post → admin approves → buyers see it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth flow&lt;/td&gt;
&lt;td&gt;Sign up → you're in&lt;/td&gt;
&lt;td&gt;Sign up → pick a role → sellers land in {% raw %}&lt;code&gt;pending&lt;/code&gt; state, gated by rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout&lt;/td&gt;
&lt;td&gt;"Place order" button that did nothing&lt;/td&gt;
&lt;td&gt;Razorpay test mode + &lt;code&gt;/api/create-order&lt;/code&gt; + HMAC-SHA256 verification on &lt;code&gt;/api/verify-payment&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data integrity&lt;/td&gt;
&lt;td&gt;No server, all rules imagined&lt;/td&gt;
&lt;td&gt;114 lines of &lt;code&gt;firestore.rules&lt;/code&gt; with helper functions, role checks, and ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;Light theme, no real empty states, broken currency&lt;/td&gt;
&lt;td&gt;Dark-first theme toggle, ₹ formatting with &lt;code&gt;en-IN&lt;/code&gt; locale, proper loading + empty states&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy&lt;/td&gt;
&lt;td&gt;Frontend on Vercel, backend half-deployed to Render&lt;/td&gt;
&lt;td&gt;Single Render Web Service: Vite build → Express serves both &lt;code&gt;dist/&lt;/code&gt; and &lt;code&gt;/api/*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The deploy line is the one I'm most embarrassed about, because the original &lt;code&gt;render.yaml&lt;/code&gt; was &lt;em&gt;technically there&lt;/em&gt; but completely broken — it had &lt;code&gt;startCommand: npm start &amp;amp;&amp;amp; npm run dev&lt;/code&gt; and there was no &lt;code&gt;start&lt;/code&gt; script in the root. The build never even reached the server startup phase. I just kept ignoring the failed deploy logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The finishing touches (this session)
&lt;/h3&gt;

&lt;p&gt;When I finally decided to "finish" Vortex for this challenge, I expected to spend an evening polishing the UI. Instead I spent half of it chasing a deployment that wouldn't boot:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;render.yaml&lt;/code&gt; had a typo that crashed every deploy&lt;/strong&gt; — &lt;code&gt;startCommand: npm start &amp;amp;&amp;amp; npm run dev&lt;/code&gt; because I never wrote a &lt;code&gt;start&lt;/code&gt; script. Fixed with &lt;code&gt;npm start&lt;/code&gt; + a real &lt;code&gt;start&lt;/code&gt; entry that resolves the right entry point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ESM-vs-CommonJS collision&lt;/strong&gt; — the root &lt;code&gt;package.json&lt;/code&gt; was &lt;code&gt;"type": "module"&lt;/code&gt;, but the server file used &lt;code&gt;require()&lt;/code&gt;. Renamed &lt;code&gt;server/server.js&lt;/code&gt; → &lt;code&gt;server/server.cjs&lt;/code&gt; so it stays CommonJS regardless of the parent's type, and updated the root start command to point at it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build was missing devDependencies&lt;/strong&gt; — Vite and &lt;code&gt;@vitejs/plugin-react&lt;/code&gt; lived in &lt;code&gt;devDependencies&lt;/code&gt;, but Render's &lt;code&gt;NODE_ENV=production&lt;/code&gt; made &lt;code&gt;npm install&lt;/code&gt; skip them. Added &lt;code&gt;--include=dev&lt;/code&gt; to the install command and &lt;code&gt;NPM_CONFIG_PRODUCTION=false&lt;/code&gt; to the env vars as a belt-and-braces fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Razorpay would crash the process if env vars were missing&lt;/strong&gt; — moved instantiation to lazy, added a startup warning and a &lt;code&gt;503&lt;/code&gt; response when keys aren't configured. Now a fresh Render deploy without Razorpay secrets still boots cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/api/health&lt;/code&gt; was the only thing that worked&lt;/strong&gt; — refactored the Express server to also serve the built Vite &lt;code&gt;dist/&lt;/code&gt; folder and act as an SPA catch-all (regex route that returns &lt;code&gt;index.html&lt;/code&gt; for any non-&lt;code&gt;/api/*&lt;/code&gt; path) so a single Render service hosts the whole thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend checkout was hardcoded to someone else's Render URL&lt;/strong&gt; — &lt;code&gt;https://vortex-api-6wk1.onrender.com/api/...&lt;/code&gt; from an earlier experiment. Replaced with a &lt;code&gt;VITE_API_BASE_URL&lt;/code&gt; env var, empty by default, so same-origin requests just work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The page wouldn't scroll&lt;/strong&gt; — leftover Vite starter CSS had &lt;code&gt;#root { width: 1126px; min-height: 100svh; display: flex; flex-direction: column; }&lt;/code&gt; which broke the scroll chain. Replaced it with a 4-line reset (&lt;code&gt;html, body, #root { height: 100%; } body { margin: 0; }&lt;/code&gt;) and let Tailwind handle the rest.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After that, &lt;code&gt;npm run build &amp;amp;&amp;amp; npm start&lt;/code&gt; boots in 3 seconds, the health check pings green, and the Vite bundle is served from the same Express process. The site finally works in production — which is, in retrospect, the most basic requirement of "finished."&lt;/p&gt;




&lt;h2&gt;
  
  
  My Experience with GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;Copilot was a co-author on basically every part of this project — but the way I used it changed shape across the two phases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: shipping features (the original sessions)
&lt;/h3&gt;

&lt;p&gt;I used Copilot heavily in agent mode to design the role-based auth flow. The thing I kept getting wrong on my own was the &lt;em&gt;invariants&lt;/em&gt; of the role transition: a new user should land as &lt;code&gt;buyer&lt;/code&gt;, a buyer should be able to &lt;em&gt;apply&lt;/em&gt; to become a seller (which sets &lt;code&gt;role: 'seller'&lt;/code&gt; and &lt;code&gt;sellerStatus: 'pending'&lt;/code&gt; simultaneously), but a pending seller should not be able to flip themselves to &lt;code&gt;active&lt;/code&gt; and start posting. That's a tiny state machine with a security guarantee attached to it, and I was over-engineering it.&lt;/p&gt;

&lt;p&gt;What worked was describing the state machine to Copilot in plain English and asking it to write the Firestore rule helpers first, then the React context code second. The helpers — &lt;code&gt;isAdmin()&lt;/code&gt;, &lt;code&gt;isSelf()&lt;/code&gt;, &lt;code&gt;isActiveSeller()&lt;/code&gt;, &lt;code&gt;userRequestsSellerRoleChange()&lt;/code&gt; — came out almost unchanged from its first draft, and they made the rest of the system obviously correct. I wasn't learning "how to use Firestore rules" — I was learning "the &lt;em&gt;shape&lt;/em&gt; of a security-relevant state machine" by watching one get drawn.&lt;/p&gt;

&lt;p&gt;Same story on the seller portal UI. I had a vague "sellers should be able to manage their own products" idea. Copilot turned that into a &lt;code&gt;&amp;lt;SellerPortal /&amp;gt;&lt;/code&gt; page with a form, an inventory list, an image upload to Cloudinary, and a "pending review" badge — all in a single iteration. I edited maybe 20% of it (mostly the date/time formatting and the stock number input), but the &lt;em&gt;shape&lt;/em&gt; was right on the first pass.&lt;/p&gt;

&lt;p&gt;On the Razorpay side, the lesson was: the integration is short, but the &lt;em&gt;trust boundary&lt;/em&gt; is the part you have to be careful about. The whole "create order" endpoint is 20 lines. The verify endpoint is 30. The reason I shipped it confidently is that Copilot and I went over the signature verification together: it wrote the &lt;code&gt;crypto.createHmac('sha256', key).update(sign).digest('hex')&lt;/code&gt; snippet, I asked "what if the order id is missing," and it added the input check. That's not autocomplete — that's a code review partner.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: finishing the deploy (this week)
&lt;/h3&gt;

&lt;p&gt;The deployment debugging was where Copilot's value flipped. Most of this work was &lt;em&gt;not&lt;/em&gt; writing new code — it was reading logs, forming hypotheses, and making tiny targeted changes. Copilot was great at the "here's a thing I broke, explain why" loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Why would &lt;code&gt;npm start &amp;amp;&amp;amp; npm run dev&lt;/code&gt; fail on Render?" → It explained the &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; short-circuit and the missing &lt;code&gt;start&lt;/code&gt; script in one sentence, then suggested the right &lt;code&gt;start&lt;/code&gt; script body. I would've eventually figured this out from the deploy log, but Copilot got me there in seconds.&lt;/li&gt;
&lt;li&gt;"What does &lt;code&gt;Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@vitejs/plugin-react'&lt;/code&gt; mean in this context?" → Instantly: "Vite is in &lt;code&gt;devDependencies&lt;/code&gt; and Render's &lt;code&gt;NODE_ENV=production&lt;/code&gt; is hiding them. Use &lt;code&gt;--include=dev&lt;/code&gt; or set &lt;code&gt;NPM_CONFIG_PRODUCTION=false&lt;/code&gt;." That was the &lt;em&gt;exact&lt;/em&gt; fix I needed.&lt;/li&gt;
&lt;li&gt;"Show me an Express middleware that serves &lt;code&gt;dist/&lt;/code&gt; for a Vite SPA and falls back to &lt;code&gt;index.html&lt;/code&gt; for any non-&lt;code&gt;/api/*&lt;/code&gt; route." → Drop-in snippet, I pasted it, it worked, I moved on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thing I appreciated most was the discipline to not &lt;em&gt;over&lt;/em&gt;-use it. When the page wouldn't scroll and I suspected the leftover &lt;code&gt;#root { width: 1126px; }&lt;/code&gt; from the Vite starter, I could've asked Copilot to rewrite the whole stylesheet. Instead I asked "is this specific CSS rule the problem?" — Copilot said yes, I deleted 100 lines and wrote 4, and the scrollbar came back. Sometimes the right Copilot prompt is the short one.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I still pushed back on
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Copilot's first draft of the Firestore &lt;code&gt;create&lt;/code&gt; rule on &lt;code&gt;users/{userId}&lt;/code&gt; was permissive about &lt;code&gt;role&lt;/code&gt; changes. I tightened it: only admins can set &lt;code&gt;role: 'admin'&lt;/code&gt;, and the seller-role upgrade has to come with &lt;code&gt;sellerStatus: 'pending'&lt;/code&gt;. Verified by writing a failing test case (a non-admin trying to set &lt;code&gt;role: 'admin'&lt;/code&gt;) and watching the rule correctly reject it.&lt;/li&gt;
&lt;li&gt;The first version of the Express static-file handler was &lt;code&gt;app.get('*', ...)&lt;/code&gt; which would have intercepted &lt;code&gt;/api/*&lt;/code&gt; requests too. I caught it and switched to the &lt;code&gt;^(?!api/).*&lt;/code&gt; regex.&lt;/li&gt;
&lt;li&gt;I didn't let Copilot touch &lt;code&gt;firestore.rules&lt;/code&gt; "for me" — security rules are the one place I want my name on every line. It helped me &lt;em&gt;think&lt;/em&gt; about the rule shape, but I wrote and tested them by hand against the Firebase emulator.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Vortex is in a state I'm actually happy with. Things I might still do, in priority order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Replace the hardcoded admin email in &lt;code&gt;isAdmin()&lt;/code&gt; with a real Firebase Auth custom claim, so admin status is provable and revocable from the Firebase console.&lt;/li&gt;
&lt;li&gt;Add a "withdraw product" flow so sellers can delist items without admin intervention.&lt;/li&gt;
&lt;li&gt;Wire up a proper email receipt via the existing SMTP path.&lt;/li&gt;
&lt;li&gt;Add Playwright e2e tests for the three role flows (buyer checkout, seller CRUD, admin moderation).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of those are blockers for calling this &lt;em&gt;finished&lt;/em&gt;. The thing this challenge gave me was the deadline pressure to actually ship what I had, instead of letting the next-feature itch keep the project in permanent limbo.&lt;/p&gt;

&lt;p&gt;Thanks for reading — and if you've got an abandoned side project, this is your sign: open the deploy logs, fix the typo, push the button, and call it done.&lt;/p&gt;

</description>
      <category>githubchallenge</category>
      <category>devchallenge</category>
      <category>githubcopilot</category>
      <category>react</category>
    </item>
  </channel>
</rss>
