<?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: Pulkit Goyal</title>
    <description>The latest articles on DEV Community by Pulkit Goyal (@pulkito4).</description>
    <link>https://dev.to/pulkito4</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%2F3983027%2F6b4103d3-6e82-48f0-928e-6882e6ad2299.jpg</url>
      <title>DEV Community: Pulkit Goyal</title>
      <link>https://dev.to/pulkito4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pulkito4"/>
    <language>en</language>
    <item>
      <title>SongBhaav v3: Deduplication, Realtime, and Building a Self-Healing Pipeline</title>
      <dc:creator>Pulkit Goyal</dc:creator>
      <pubDate>Sat, 20 Jun 2026 17:33:30 +0000</pubDate>
      <link>https://dev.to/pulkito4/songbhaav-v3-deduplication-realtime-and-building-a-self-healing-pipeline-3o0h</link>
      <guid>https://dev.to/pulkito4/songbhaav-v3-deduplication-realtime-and-building-a-self-healing-pipeline-3o0h</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/pulkito4/i-built-an-app-to-finally-understand-the-songs-i-listen-to-and-then-had-to-rewrite-the-entire-3bj3"&gt;Part 1&lt;/a&gt;, I covered how SongBhaav's backend broke two days after launch and how the v2 rewrite solved the serverless timeout problem with an async job queue.&lt;/p&gt;

&lt;p&gt;That architecture held up well. But a few things still bothered me — some were UX rough edges, one was a ticking time bomb I found out about from an email.&lt;/p&gt;

&lt;p&gt;This post covers the three changes that make up v3: synchronous lyrics scraping for faster failure feedback, job deduplication, switching from polling to Realtime, and a self-healing system for a Spotify policy change that was about to break my daily sync pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 1: "Lyrics Not Found" Took Too Long to Tell You
&lt;/h2&gt;

&lt;p&gt;In v2, the flow was: kick off a background job → wait → eventually find out if lyrics existed at all.&lt;/p&gt;

&lt;p&gt;This meant that if a song's lyrics genuinely couldn't be found anywhere — LRCLIB, Genius, none of it — the user would sit through the entire async handoff, watch a loading state, and &lt;em&gt;then&lt;/em&gt; get told there was nothing to show. The failure mode was slow in exactly the case where it should have been instant.&lt;/p&gt;

&lt;p&gt;The fix in v3 was to move lyrics fetching back into the synchronous part of the request, but only the fetching — not the AI processing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User → POST /api/start-job
         ├─ Check processed_songs (cache hit? return immediately)
         ├─ Check background_jobs (job already in-flight? return existing job_id)
         └─ New song: run fetchLyricsCascade() synchronously
                ├─ No lyrics found anywhere → return immediately, no job ever created
                └─ Lyrics found → save to DB → create job → push to QStash → return job_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If lyrics genuinely don't exist, the user gets that answer in the time it takes to query three lyrics providers — a couple of seconds — instead of going through the full async detour first. Only the slow, unpredictable part (Gemini analysis) stays async. The fast, deterministic part (does this lyric exist anywhere) happens upfront.&lt;/p&gt;

&lt;p&gt;This also let me split the loading UI into two honest stages instead of one generic spinner — an orange "hunting down the lyrics" stage while scraping runs, and a purple "breaking down the bars" stage once AI processing kicks in. Small thing, but it makes the wait feel like progress rather than a black box.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 2: Two Users, Same Song, Double the Work
&lt;/h2&gt;

&lt;p&gt;This one only shows up at a small scale of concurrent traffic, but it's a real bug. If two people searched the same uncached song within a few seconds of each other, both requests would independently check the cache, both would miss, and both would spin up separate QStash jobs — duplicate lyrics fetches, duplicate Gemini calls, duplicate API spend, for the exact same song.&lt;/p&gt;

&lt;p&gt;The fix is a simple in-flight check before creating a new job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;checking background_jobs for this spotify_id:
  - status = 'pending' or 'processing' already exists?
      → return that existing job_id to the new request
  - otherwise:
      → proceed to create a new job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both users end up subscribed to the same job, watching the same background worker resolve once, and both get the result when it completes. No wasted API calls, no race condition, no double billing on Gemini calls for a song that was already being processed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 3: Polling Worked, But Realtime Is Just Better Here
&lt;/h2&gt;

&lt;p&gt;In v2, the frontend polled &lt;code&gt;/api/check-job&lt;/code&gt; every few seconds to check status. It worked, and I wrote about why polling was a reasonable choice over WebSockets at the time — simpler, no persistent connection overhead.&lt;/p&gt;

&lt;p&gt;In v3, I switched to Supabase Realtime instead. A few reasons pushed this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Polling means either over-fetching (checking too often, wasting requests) or under-fetching (checking too rarely, feeling laggy). Realtime sidesteps that tradeoff entirely — the update arrives the instant the row changes, no guessing on interval timing.&lt;/li&gt;
&lt;li&gt;With deduplication now in place, multiple clients can be waiting on the &lt;em&gt;same&lt;/em&gt; job. Realtime handles "many clients subscribed to one row" more naturally than each client running its own polling loop against the same endpoint.&lt;/li&gt;
&lt;li&gt;The actual implementation overhead turned out to be smaller than I expected — subscribing to a filtered channel on &lt;code&gt;job_id&lt;/code&gt; is a few lines, and Supabase handles the connection lifecycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this wasn't a case of "polling was wrong" — it was right for v2's constraints. It just stopped being the better tradeoff once concurrent job-sharing entered the picture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem 4: An Email From Spotify About Something That Hadn't Broken Yet
&lt;/h2&gt;

&lt;p&gt;SongBhaav runs a daily sync via GitHub Actions that pulls my recently played tracks from Spotify, so songs are pre-processed before anyone searches for them. This depends on a Spotify refresh token staying valid indefinitely.&lt;/p&gt;

&lt;p&gt;Spotify recently emailed every developer with a policy change: starting July 20, 2026, refresh tokens will expire after six months. Once expired, any attempt to refresh returns an &lt;code&gt;invalid_grant&lt;/code&gt; error, and the only fix is sending the user through the sign-in flow again.&lt;/p&gt;

&lt;p&gt;This hadn't broken anything yet. But it was going to, on a specific date, with zero warning beyond that email — exactly the kind of failure that's easy to forget about until your daily sync silently stops working months from now and you have no idea why.&lt;/p&gt;

&lt;p&gt;So instead of waiting for it to break, I built the recovery flow ahead of time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Daily sync script runs
  → reads refresh_token from system_credentials table (DB, not env vars)
  → requests new access token from Spotify
  → if invalid_grant:
        → generate one-time secure ticket, insert into admin_sessions (1hr expiry)
        → POST a magic link to a private Discord webhook
        → exit gracefully (no retry, no crash loop)

When I click the Discord link:
  → /api/admin/spotify-login validates the ticket, marks it used, redirects to Spotify OAuth
  → /api/admin/spotify-callback exchanges the auth code, UPSERTs new refresh_token into system_credentials

Next sync run → reads the new token → succeeds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few deliberate choices here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credentials live in the database, not environment variables.&lt;/strong&gt; This means re-authorizing doesn't require a redeploy — the next scheduled run just picks up the new token from the DB automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ticket is single-use and time-boxed.&lt;/strong&gt; Even if the Discord webhook URL ever leaked, an expired or already-used ticket can't be replayed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The script exits cleanly on &lt;code&gt;invalid_grant&lt;/code&gt; instead of retrying.&lt;/strong&gt; Retrying a dead token wastes calls and clutters logs with the same error repeatedly. Better to fail once, loudly, and wait for the human-in-the-loop fix.&lt;/p&gt;

&lt;p&gt;The actual mechanism is simple — a magic link sent over a webhook. What made it worth building was reading the policy email in advance and treating "this will eventually fail" with the same urgency as "this is currently failing."&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed, In Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;v2&lt;/th&gt;
&lt;th&gt;v3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lyrics-not-found feedback&lt;/td&gt;
&lt;td&gt;Async, slow&lt;/td&gt;
&lt;td&gt;Synchronous, instant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate concurrent searches&lt;/td&gt;
&lt;td&gt;Each spawns a new job&lt;/td&gt;
&lt;td&gt;Deduplicated, shared job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client notification&lt;/td&gt;
&lt;td&gt;Polling &lt;code&gt;/api/check-job&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Supabase Realtime subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spotify token expiry&lt;/td&gt;
&lt;td&gt;Not handled&lt;/td&gt;
&lt;td&gt;Self-healing via Discord + OAuth&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these were emergencies. That's sort of the point — v2 wasn't broken, it just had room to get better, and one looming policy change that hadn't caused damage yet but eventually would have.&lt;/p&gt;




&lt;p&gt;If you want to try SongBhaav yourself, drop any song in the search bar: &lt;a href="https://song-bhaav.vercel.app" rel="noopener noreferrer"&gt;song-bhaav.vercel.app&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>nextjs</category>
      <category>architecture</category>
    </item>
    <item>
      <title>I Built an App to Finally Understand the Songs I Listen To (And Then Had to Rewrite the Entire Backend)</title>
      <dc:creator>Pulkit Goyal</dc:creator>
      <pubDate>Sat, 13 Jun 2026 19:40:19 +0000</pubDate>
      <link>https://dev.to/pulkito4/i-built-an-app-to-finally-understand-the-songs-i-listen-to-and-then-had-to-rewrite-the-entire-3bj3</link>
      <guid>https://dev.to/pulkito4/i-built-an-app-to-finally-understand-the-songs-i-listen-to-and-then-had-to-rewrite-the-entire-3bj3</guid>
      <description>&lt;p&gt;I listen to a lot of Punjabi music. Always have. The beats are incredible — but I'd be lying if I said I understood more than 60% of what was actually being said. The usual fix is to Google the lyrics, find a translation, switch tabs, lose the vibe entirely. Or paste the lyrics into ChatGPT every single time. It works, but it's friction.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;SongBhaav&lt;/strong&gt; — a webapp where you search any song and get line-by-line translations, emotional interpretation, cultural context, and what I like to call the "kavi kya kehna chahte hain" treatment. The thing your English teacher used to do with poems in school. Breaking down what the artist actually meant, not just what the words say.&lt;/p&gt;

&lt;p&gt;It started as a weekend side project. Then I made it a proper webapp. Then two days after going live, half the backend broke in production.&lt;/p&gt;

&lt;p&gt;This post is about how the app works, what broke, and the architectural decisions that came out of fixing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SongBhaav Actually Does
&lt;/h2&gt;

&lt;p&gt;The core flow is simple: search for a song → get a full breakdown.&lt;/p&gt;

&lt;p&gt;The breakdown includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Line-by-line translation — not just a raw word-for-word translation but one that preserves the intent&lt;/li&gt;
&lt;li&gt;Interpretation — what the song is actually about, the emotional arc, the metaphors&lt;/li&gt;
&lt;li&gt;Fun facts — context about the artist, the album, cultural references in the lyrics&lt;/li&gt;
&lt;li&gt;Emotional themes — the overall sentiment and mood&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I use it daily now. Mostly for Punjabi tracks but it works across languages — anything Gemini can process, which is quite a lot.&lt;/p&gt;

&lt;p&gt;The tech stack: Next.js (App Router) on the frontend, Supabase (PostgreSQL) as the database, Gemini models for AI processing, and — as we'll get to — Upstash QStash for the async architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  How v1 Worked (And Why It Was Naive)
&lt;/h2&gt;

&lt;p&gt;The first version was a straightforward synchronous pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User searches song → POST /api/process → Fetch lyrics → Gemini AI → Return response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For songs already in the database, it was fast — just a cache hit on Supabase and done. The problem was cache misses. Any song that hadn't been analyzed before required the full pipeline: fetch lyrics from an external source, send them to Gemini, wait for a structured JSON analysis to come back. That whole flow was consistently taking 15 to 25 seconds depending on song complexity and Gemini's response time.&lt;/p&gt;

&lt;p&gt;Vercel's Hobby tier enforces a hard 10-second execution limit on serverless functions. So for any new song, users would wait, the browser would spin indefinitely, and they'd eventually get a &lt;code&gt;FUNCTION_INVOCATION_TIMEOUT&lt;/code&gt; error. Not a great first impression.&lt;/p&gt;

&lt;p&gt;I knew this was a problem during development. I launched anyway, telling myself it "probably wouldn't be that bad in practice."&lt;/p&gt;

&lt;p&gt;It was that bad in practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Broke in Production
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Problem 1: Cloudflare Killed My Lyrics Scraper
&lt;/h3&gt;

&lt;p&gt;Before integrating official APIs, I was scraping lyrics directly from Genius. It worked fine locally, worked fine in the first couple of days after launch.&lt;/p&gt;

&lt;p&gt;Then Genius added Cloudflare protection to their website.&lt;/p&gt;

&lt;p&gt;The errors started showing up in my &lt;code&gt;pipeline_logs&lt;/code&gt; table — not clean API errors, but this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SyntaxError: Unexpected token '&amp;lt;', "&amp;lt;!DOCTYPE "... is not valid JSON&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;What was happening: my serverless function would make a request to scrape the Genius page, Cloudflare would intercept it, identify it as an automated agent, and serve back an HTML "Access Denied" page instead. My code would then try to parse that HTML as JSON and crash immediately.&lt;/p&gt;

&lt;p&gt;The fix was straightforward once I understood the cause — register an official Genius Developer API client, switch to authenticated API calls with Bearer tokens, bypass the anti-bot layer entirely. Since making that switch, lyrics fetching has had zero failures.&lt;/p&gt;

&lt;p&gt;But the lesson was more important than the fix: &lt;strong&gt;you cannot deploy a web app and consider it done&lt;/strong&gt;. Third-party services update, add protections, change behavior. The two days between launch and breakage weren't anything I did wrong — the external environment just changed. Production monitoring isn't optional; it's how you find out about this before your users do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 2: The 10-Second Ceiling
&lt;/h3&gt;

&lt;h2&gt;
  
  
  This one I knew was coming and hadn't properly solved. The serverless timeout wasn't an occasional edge case — it was guaranteed for every new song search. There was no optimizing my way out of it within a synchronous request model. The question was: what's the right architecture for a task that inherently takes longer than your platform allows?
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The v2 Rewrite: Going Async
&lt;/h2&gt;

&lt;p&gt;The core insight was simple: &lt;strong&gt;don't make the user wait for the slow thing. Start the slow thing, tell the user you've started it, then notify them when it's done.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a standard pattern in distributed systems — sometimes called a "front desk / back office" model. The front desk takes your request immediately and gives you a ticket. The back office does the actual work. You don't stand at the counter for 25 seconds.&lt;/p&gt;

&lt;p&gt;Here's what the new flow looks like:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Three components make this work:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upstash QStash&lt;/strong&gt; is the message broker. When a user requests a new song, &lt;code&gt;/api/start-job&lt;/code&gt; pushes a message to QStash with the track metadata and immediately returns a &lt;code&gt;job_id&lt;/code&gt; to the client — the whole thing resolves in under 50ms. QStash then handles triggering the background worker, with automatic retries built in if anything fails.&lt;/p&gt;

&lt;p&gt;One practical note: QStash delivers messages via HTTP POST to your worker endpoint, which means it can't reach &lt;code&gt;localhost&lt;/code&gt; in local development. I handled this by detecting the environment in &lt;code&gt;/api/start-job&lt;/code&gt; and calling the worker directly in dev, bypassing QStash entirely. Not elegant, but it works cleanly and keeps the local development loop fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The polling loop&lt;/strong&gt; handles the feedback to the user. While the background worker runs, the frontend periodically hits &lt;code&gt;/api/check-job?id=...&lt;/code&gt; every few seconds, checking the &lt;code&gt;background_jobs&lt;/code&gt; table until the status flips from processing to completed. Once it does, the client fetches the result and swaps the skeleton loader for the actual data. Simple, lightweight, and it avoids the complexity and connection quota costs of maintaining a persistent WebSocket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The worker&lt;/strong&gt; runs the actual pipeline: concurrent lyrics fetching via &lt;code&gt;Promise.any&lt;/code&gt; across LRCLIB and Genius (whichever resolves first wins), then Gemini processing, then writing the structured result to &lt;code&gt;processed_songs&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling Gemini 429s
&lt;/h3&gt;

&lt;p&gt;One edge case I didn't think about until it happened: what if Gemini rate limits mid-job?&lt;/p&gt;

&lt;p&gt;Before adding proper error handling, a 429 from Gemini would cause the worker to throw an unhandled error. QStash would mark the delivery as failed, and &lt;code&gt;background_jobs&lt;/code&gt; would stay stuck at &lt;code&gt;processing&lt;/code&gt; forever. The user would sit on a skeleton loader indefinitely with no feedback or resolution.&lt;/p&gt;

&lt;p&gt;The fix: explicitly return a 500 status when Gemini throws a 429 or 503. QStash treats any non-2xx response as a failed delivery and automatically schedules a retry using exponential backoff. QStash's backoff formula is &lt;code&gt;min(86400, e^(2.5*n))&lt;/code&gt; seconds — so roughly 12 seconds after the first failure, ~2.5 minutes after the second, ~30 minutes after the third. Aggressive enough to let Gemini's rate window reset, spaced out enough not to hammer it again immediately.&lt;/p&gt;

&lt;p&gt;This means transient rate limit errors self-heal entirely without manual intervention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Securing the Worker Endpoint
&lt;/h3&gt;

&lt;p&gt;Easy thing to overlook with async architectures: your background worker is just an HTTP endpoint sitting on the public internet. Anyone who finds the URL can POST to it and trigger AI processing, draining your API credits.&lt;/p&gt;

&lt;p&gt;QStash solves this with cryptographic signature verification. Every message QStash sends includes an upstash-signature header signed with your secret key. The worker validates this using the QStash SDK before executing anything — invalid or missing signatures get a 401 immediately.&lt;/p&gt;

&lt;p&gt;For rate limiting the public-facing endpoints, I used Upstash Redis with the &lt;code&gt;@upstash/ratelimit&lt;/code&gt; SDK inside Next.js middleware. Since middleware runs at the edge (CDN level on Vercel), spam requests are blocked before they ever touch the database or trigger any compute.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;Start async from day one. The v1 synchronous architecture was always going to hit the timeout wall for new songs. Building the QStash pipeline upfront isn't significantly more complex than the naive approach — it would have saved the rushed production rewrite.&lt;/p&gt;

&lt;p&gt;Set up alerting before going live, not after. I found out about the Cloudflare failure by manually checking pipeline_logs. A simple alert on error rate spikes would have caught it immediately. Logging without alerting is only half the job.&lt;/p&gt;

&lt;p&gt;Don't assume external services stay stable. Genius adding Cloudflare wasn't predictable. The right response isn't to avoid third-party dependencies — official APIs are fine — but to monitor them actively and build fallbacks where possible.&lt;/p&gt;




&lt;p&gt;If you want to try SongBhaav, drop any song in the search bar: &lt;a href="https://song-bhaav.vercel.app/" rel="noopener noreferrer"&gt;SongBhaav&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback welcome — especially if something in the architecture could be done better.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>nextjs</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
