<?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: Muhammad Ayaan</title>
    <description>The latest articles on DEV Community by Muhammad Ayaan (@muhammad_ayaan_12954eb4fd).</description>
    <link>https://dev.to/muhammad_ayaan_12954eb4fd</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%2F3960108%2F1f72d307-1134-4b0f-a7a2-ce5a832fe454.jpeg</url>
      <title>DEV Community: Muhammad Ayaan</title>
      <link>https://dev.to/muhammad_ayaan_12954eb4fd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muhammad_ayaan_12954eb4fd"/>
    <language>en</language>
    <item>
      <title>I Turned My Football Group Chat Arguments Into an Actual Scoreboard!</title>
      <dc:creator>Muhammad Ayaan</dc:creator>
      <pubDate>Sun, 12 Jul 2026 23:11:38 +0000</pubDate>
      <link>https://dev.to/muhammad_ayaan_12954eb4fd/i-turned-my-football-group-chat-arguments-into-an-actual-scoreboard-2pn7</link>
      <guid>https://dev.to/muhammad_ayaan_12954eb4fd/i-turned-my-football-group-chat-arguments-into-an-actual-scoreboard-2pn7</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-07-09"&gt;Weekend Challenge: Passion Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Called It!&lt;/strong&gt; is a social football prediction platform. You predict match outcomes before kickoff, earn points based on how accurate you were, and compete with friends. No squad management, no transfers, no budgets. Just your ability to read a game.&lt;/p&gt;

&lt;p&gt;The problem is something I've felt for years. I have a group chat where we all throw out predictions before big matches. Someone says 2-1 with a specific goalscorer. Someone else says 3-0. Then the game ends, the chat scrolls past, and nobody remembers who actually got it right. There is no scoreboard for "I told you so."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Called It!&lt;/strong&gt; gives that dynamic a scoreboard. Every prediction is timestamped, locked at kickoff, and scored automatically against real match data. Get the exact score and the first goalscorer right in the same prediction and you earn a Called It card, a kind of digital receipt you can share. Those are genuinely hard to get!&lt;/p&gt;

&lt;p&gt;There is something about prediction that overlaps with the same part of your brain that betting does. You pick a score, you commit to a goalscorer, and for 90 minutes you are wired to every pass and shot in a way you would not be otherwise. The difference is there is no money on the line; it's not a harmful habit and there's no loss. You are not chasing a payout. You are chasing a spot above your mate on a leaderboard, or a Called It card you can throw in the group chat, which makes the whole concept very healthy! The incentive is social, not financial. That sounds like a small distinction but it changes the whole feel. Nobody loses anything. Nobody chases losses. Worst case, you got a match wrong and you drop a few ranking spots. Best case, you called a 3-1 with the exact scorer and everyone knows it.&lt;/p&gt;

&lt;p&gt;I built this over a weekend. It has live sports data from football-data.org, automated result processing, friend requests, and leaderboards. It is deployed and functional. I kept the scope tight because weekend hackathons punish ambition that drifts too far from what one person can actually ship.&lt;/p&gt;

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

&lt;p&gt;The live app is deployed at &lt;a href="https://called-it-two.vercel.app" rel="noopener noreferrer"&gt;https://called-it-two.vercel.app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can create an account, pick a favorite team, predict upcoming fixtures, and see your ranking and points update as results come in. If you nail a prediction completely, the Called It card page is public and shareable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The full source is at &lt;a href="https://github.com/Raiden505/called-it" rel="noopener noreferrer"&gt;https://github.com/Raiden505/called-it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Stack: Next.js App Router with TypeScript, Tailwind CSS for styling, Supabase for auth and Postgres, and the football-data.org API v4 for live match data.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Architecture choice: a modular monolith
&lt;/h3&gt;

&lt;p&gt;The most consequential decision I made early on was staying with a single Next.js app instead of splitting into separate services. A weekend build with separate frontend and backend deployments would have eaten half the time just in configuration and debugging. Instead, everything lives in one codebase with server actions and route handlers handling the backend work. The domain logic sits under &lt;code&gt;lib/&lt;/code&gt; in bounded modules (scoring, cards, leaderboards, sports sync), each with its own tests.&lt;/p&gt;

&lt;p&gt;This is not the architecture I would recommend for a team of ten, but for a solo weekend build it meant I spent time on features instead of infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scoring engine
&lt;/h3&gt;

&lt;p&gt;The scoring is pure math, no AI involved. Correct outcome is worth 3 points. Matching the exact goal difference adds 2. Getting the score exactly right adds 3. Naming the first goalscorer is worth 4. You can attach a confidence multiplier (1x, 2x, or 3x) to any prediction, but you only get three of each multiplier per tournament round, enforced atomically through a Postgres RPC.&lt;/p&gt;

&lt;p&gt;A 0-0 match with "no goalscorer" predicted correctly still earns the full 4 points for first goalscorer. Own goals do not count. Knockout matches use regulation-time scoring only.&lt;/p&gt;

&lt;p&gt;The engine is 86 lines of TypeScript and was the first thing I wrote tests for. When a result gets corrected (because real match data is occasionally wrong), the entire scoring pipeline reruns idempotently. Predictions get rescored, Called It cards get reissued or revoked, and deduplication keys prevent double notifications. The atomic RPC that handles this is one of the most carefully designed pieces of the whole app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security: RLS on everything
&lt;/h3&gt;

&lt;p&gt;Every user-facing table has Row Level Security enabled. Predictions are hidden from other users before kickoff. Friends can see them after the match starts. Clients cannot insert Called It cards or modify scoring fields. The sports sync tables are locked down so only the service role can touch them. The cron endpoint is protected by a shared secret.&lt;/p&gt;

&lt;p&gt;I set this up early because retrofitting RLS is a nightmare. It paid off almost immediately when I caught a bug where a raw Supabase query in the browser client would have leaked prediction data if the RLS policy had not blocked it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Live data pipeline
&lt;/h3&gt;

&lt;p&gt;The football-data.org integration was the trickiest part. Their API v4 has rate limits (10 requests per minute on the free tier), and match data changes state over time. A fixture goes from scheduled to live to finished, and even after finishing, the result can be corrected.&lt;/p&gt;

&lt;p&gt;I built a due-work system that runs once per minute via Supabase Cron. Each run picks up to 8 fixtures that are due for a refresh, processes them in batches of 4, and prioritizes finished-but-unconfirmed results. Results go through a two-observation confirmation gate: the system reads the match result twice, 90 seconds apart, and only scores predictions when both observations produce the same stable hash. This catches the case where football-data.org shows a provisional score that gets corrected minutes later.&lt;/p&gt;

&lt;p&gt;The whole pipeline is gated behind a &lt;code&gt;SPORTS_SYNC_ENABLED&lt;/code&gt; flag so I could deploy it disabled and verify behaviour in dry-run mode before turning on live writes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The dark theme
&lt;/h3&gt;

&lt;p&gt;The UI uses a dark "match-night" palette. Near-black backgrounds, acid-lime accents for calls to action and verification badges, and a condensed typography stack (Bahnschrift Condensed, Arial Narrow, Impact for display, Trebuchet MS and Segoe UI for body). On desktop there is a 248px side navigation rail. On mobile it collapses to a bottom tab bar.&lt;/p&gt;

&lt;p&gt;I spent more time on the Called It card visuals than I want to admit. Each card renders as a football match ticket with a lime verification stripe down one edge. The public card page shows the exact prediction, the rarity, and share actions (copy link, native share, SVG download). It looks like something you would actually want to post in a group chat.&lt;/p&gt;

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

&lt;p&gt;The initial release is scoped to a single tournament, but the match data pipeline was built to handle any football competition. Adding the Premier League or the Champions League is really just a configuration change and another season of seeded data. The hardest part was building the sync infrastructure, not the per-competition logic.&lt;/p&gt;

&lt;p&gt;Event-by-event prediction is the feature I most want to add. Guessing the minute of the first goal, the number of corners, whether there will be a red card. That kind of granularity turns a 90-minute match into a series of smaller moments to care about, which is where prediction gets genuinely fun.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
    </item>
    <item>
      <title>Finally Finished Memex: Turning a CLI Based MVP into a Production-Ready Web Service</title>
      <dc:creator>Muhammad Ayaan</dc:creator>
      <pubDate>Mon, 08 Jun 2026 04:15:00 +0000</pubDate>
      <link>https://dev.to/muhammad_ayaan_12954eb4fd/finally-finished-memex-turning-a-cli-based-mvp-into-a-production-ready-web-service-4nej</link>
      <guid>https://dev.to/muhammad_ayaan_12954eb4fd/finally-finished-memex-turning-a-cli-based-mvp-into-a-production-ready-web-service-4nej</guid>
      <description>&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;Memex is a personal memory app. You tell it things in plain language, and you ask it questions later.&lt;/p&gt;

&lt;p&gt;Most notes apps ask you to organise before you can capture. You pick the folder, name the file, decide whether this belongs in tasks or references or ideas. Memex skips all of that. You type what you heard, what occurred to you, what you want to remember. It saves it. Later you ask "where did I hear about those jackets?" and it tells you. Same account, same memories, whether you're at your terminal or in a browser.&lt;/p&gt;

&lt;p&gt;Under the hood it's a RAG system. Every memory gets embedded using Gemini's &lt;code&gt;gemini-embedding-001&lt;/code&gt; model at 768 dimensions, stored in Supabase with pgvector, and retrieved by cosine similarity when you ask a question. Gemini synthesises an answer grounded only in what you actually told it. If it finds nothing relevant, it says so, and the model never gets to guess.&lt;/p&gt;

&lt;p&gt;Two clients share one backend: a Python CLI and a Next.js web app, both reading and writing the same Supabase account.&lt;/p&gt;

&lt;p&gt;The finished version of the web app has a chat page, a Memory Library, and a settings panel. The chat handles three kinds of input: storing a memory, recalling one (with optional time-scoping), and general conversation. Greetings get a warm reply. "What did I save this week?" lists everything from that window. "Forget what I said about the dentist" finds the matching memory, shows it to you, and only deletes it when you confirm. Answers stream in token by token. Save confirmations rotate through a small set of acknowledgements so the app doesn't feel like a form submission every time.&lt;/p&gt;

&lt;p&gt;The Memory Library lets you browse, search, pin, edit, and delete memories directly, without going to the CLI or the Supabase dashboard. Memories get auto-tagged at save time into lightweight categories like idea, task, person, and place. Ones that carry a future date — "renew passport next month", "dentist on Tuesday" — get a due date extracted and show up in a due/upcoming view at the right time. The settings panel has a working dark mode, an export option that dumps everything to JSON, and an import path that re-embeds on the way back in with deduplication.&lt;/p&gt;

&lt;p&gt;Here's what the finished version ships with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three-way intent routing: storing a memory, recalling one, and general conversation are handled separately — greetings never pollute your store&lt;/li&gt;
&lt;li&gt;Temporal recall with timezone-aware windows: "what did I save today/this week/yesterday" works correctly in the user's local day, not UTC&lt;/li&gt;
&lt;li&gt;Due-date extraction: memories with a future date show up in an upcoming view at the right time&lt;/li&gt;
&lt;li&gt;Natural-language forget with two-step confirmation: the server never deletes on the first request&lt;/li&gt;
&lt;li&gt;Lossless streaming via JSON-encoded SSE: answers grow token by token without dropping multi-line content&lt;/li&gt;
&lt;li&gt;Relevance floor before synthesis: weak matches are filtered out before reaching the model, so one unrelated memory can't derail an answer&lt;/li&gt;
&lt;li&gt;Memory Library: browse, search, pin, edit, and delete — no CLI required&lt;/li&gt;
&lt;li&gt;Auto-tagging into categories (idea, task, person, place) at save time without blocking the core flow&lt;/li&gt;
&lt;li&gt;Dark mode with full token-based theming, no hardcoded hex values left&lt;/li&gt;
&lt;li&gt;Export to JSON and re-import with deduplication&lt;/li&gt;
&lt;li&gt;Python CLI and Next.js web app sharing one Supabase backend and Gemini API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The initial version worked — but only in a terminal, only for one user, with no auth, no web UI, greetings stored as memories, and no way to ask what you'd saved today. It proved the core idea. It wasn't something you could hand to anyone else.&lt;/p&gt;

&lt;p&gt;Turning that into a finished product — with a web app, real multi-device auth, a Memory Library, dark mode, due-date reminders, natural-language deletion, and all the correctness work that makes it trustworthy — is what this submission is really about.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live Site:&lt;/strong&gt;&lt;a href="https://memex-web-eta.vercel.app/" rel="noopener noreferrer"&gt;https://memex-web-eta.vercel.app/&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Finished repo:&lt;/strong&gt; &lt;a href="https://github.com/Raiden505/memex-web" rel="noopener noreferrer"&gt;https://github.com/Raiden505/memex-web&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Before version repo:&lt;/strong&gt;&lt;a href="https://github.com/Raiden505/memex-cli" rel="noopener noreferrer"&gt;https://github.com/Raiden505/memex-cli&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faukrpy9uwwij0sogqwbc.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%2Faukrpy9uwwij0sogqwbc.png" alt=" " width="522" height="754"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1pyw7res7j8m1s5fd0c8.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%2F1pyw7res7j8m1s5fd0c8.png" alt=" " width="800" height="898"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuqcvqqcq5kl03txs1yi7.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%2Fuqcvqqcq5kl03txs1yi7.png" alt=" " width="800" height="614"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The original Memex was a terminal tool. It stored memories, retrieved them semantically, and answered questions — but only from a command line, only for one account with no real auth, with no way to use it from a browser or a phone. Saying "hi" tried to store "hi" as a memory. There was no concept of today or this week. Deletion required knowing the memory's UUID. I could demo it. I couldn't give it to anyone.&lt;/p&gt;

&lt;p&gt;That's the gap nobody talks about when they ship a working concept. The demo path works great. Everything slightly off the demo path doesn't. Saying "hi" stores "hi" as a memory. Clicking a memory card to ask about it sometimes re-saves it instead. "What did I tell you today?" returns nothing because the regex only matches the bare word, not the word inside a sentence. An answer about one thing gets quietly distorted by a weakly related memory that happened to score high enough to be included. None of those kill the demo. All of them kill the product.&lt;/p&gt;

&lt;p&gt;The next 17 phases were about finding and fixing that category of problem — the things that work in a controlled walkthrough and break in real use.&lt;/p&gt;

&lt;p&gt;The first thing Memex had to get right was correctness. A memory tool that confidently tells you the wrong thing is a different kind of failure than most software. It fails by lying about your own past. So the no-hallucination rule went in from the start: if a personal-recall question returns no relevant memories, the system short-circuits before calling the model and returns a fixed message. The LLM never gets the chance to guess at a personal fact. That constraint stayed intact through every subsequent phase.&lt;/p&gt;

&lt;p&gt;Early versions classified every message as either "store" or "query." That worked for the obvious cases but produced embarrassing results at the edges. Say "hi" and the app would try to store "hi" as a memory. Ask "what's the capital of France?" and it would either store the question or return "I don't have anything saved about that." Neither is acceptable for something you want to use every day.&lt;/p&gt;

&lt;p&gt;So a third intent, &lt;code&gt;general&lt;/code&gt;, got added. Greetings and general-knowledge questions get a short conversational reply from Gemini. They're never stored and never trigger a memory search. A fast-path handles the obvious cases without any LLM call: there's a small hardcoded set of known strings — "hi", "thanks", "good morning", and a few more — that resolve directly to &lt;code&gt;general&lt;/code&gt; before the classifier even runs. The three-way LLM classifier handles everything ambiguous, running at temperature 0 and defaulting to &lt;code&gt;store&lt;/code&gt; on any failure.&lt;/p&gt;

&lt;p&gt;The no-hallucination rule got sharpened by this change. The GENERAL fallback applies only to messages classified &lt;code&gt;general&lt;/code&gt; up front. A &lt;code&gt;query&lt;/code&gt; that finds no matching memories still returns the fixed "nothing saved" message, every time. "What's the capital of France?" gets answered from general knowledge. "Where did I park?" does not get guessed at. Those are handled by different branches and that separation is deliberate.&lt;/p&gt;

&lt;p&gt;Temporal recall came from actually using the app. "What did I tell you today?" is one of the most natural questions to ask a memory tool. The original system couldn't answer it because semantic search has no sense of when something was saved. A &lt;code&gt;temporal.extract_range&lt;/code&gt; function now parses time-window phrases from queries, and &lt;code&gt;list_memories_in_range&lt;/code&gt; handles the date-filtered fetch from Supabase. One bug took a while to notice: the original implementation used &lt;code&gt;re.fullmatch("today", text)&lt;/code&gt;, which only matched the bare word. "What did I tell you today?" as a natural sentence never triggered it. Switching to &lt;code&gt;re.search(r"\btoday\b", text)&lt;/code&gt; was the fix.&lt;/p&gt;

&lt;p&gt;The timezone handling matters more than it might look. "Today" means the user's local day, not UTC. Someone in UTC+5 asking about today at 10pm expects their local calendar day, not a UTC window that cut off five hours ago. The web client reads the timezone from &lt;code&gt;Intl.DateTimeFormat().resolvedOptions().timeZone&lt;/code&gt; and sends it with every chat request. All the window calculations run in the supplied timezone before converting to UTC for the database query.&lt;/p&gt;

&lt;p&gt;The streaming bug was invisible until you looked for it. Replies appeared to work — answers came back, content was correct — but nothing ever actually streamed, and some multi-line answers arrived truncated. The cause was in how SSE frames were being parsed. The backend emitted each model token as &lt;code&gt;data: &amp;lt;raw token&amp;gt;\n\n&lt;/code&gt;. The frontend split the buffer on &lt;code&gt;"\n"&lt;/code&gt; and discarded any line that didn't start with &lt;code&gt;"data: "&lt;/code&gt;. A token containing a newline produced a second line that got silently dropped. Multi-line content was silently truncated. The non-streaming fallback returned the full response, which is why the product appeared correct but never actually grew text on screen.&lt;/p&gt;

&lt;p&gt;The fix was JSON-encoding every payload instead of putting raw text directly in the SSE field. The backend now emits &lt;code&gt;data: {"t": "&amp;lt;token&amp;gt;"}\n\n&lt;/code&gt; for each chunk, so newlines inside tokens are escaped as &lt;code&gt;\n&lt;/code&gt; and never break SSE framing. The frontend parses events by splitting on &lt;code&gt;"\n\n"&lt;/code&gt; (event boundaries) and JSON-parses each one. Backend and frontend had to ship together since the wire format changed, but after that streaming worked properly for the first time.&lt;/p&gt;

&lt;p&gt;The accuracy of answers also improved significantly after filtering weak matches before synthesis. The original retrieval returned up to five results and passed all of them to the model regardless of relevance. One weakly related memory could distort an answer about something completely different. A relevance floor now filters results before they reach the synthesiser: anything within 0.2 of the top similarity score survives, with a hard floor at 0.2. If nothing clears the floor, the no-hallucination short-circuit fires and the model is never called. The quality improvement on real-world questions was noticeable immediately.&lt;/p&gt;

&lt;p&gt;Clicking a saved memory card to ask about it exposed a routing problem. The click was inserting the memory content into the chat as a plain query string, which then went through intent routing. If the note contained certain words — a date, "due", "task" — the router could classify the input as STORE and try to save the memory again instead of recalling it. A &lt;code&gt;mode: "recall"&lt;/code&gt; parameter on the API now bypasses intent routing entirely for those requests, going straight to semantic search.&lt;/p&gt;

&lt;p&gt;Natural-language deletion was designed around a two-step confirm protocol. The first request resolves candidates but deletes nothing. The server returns &lt;code&gt;forget_candidates&lt;/code&gt; in the response: a list of memories that matched, with content and dates. The client shows them in a confirm card. On confirm, the client re-posts the original message with &lt;code&gt;confirm_forget: [ids]&lt;/code&gt;. The server re-checks ownership via &lt;code&gt;user_id&lt;/code&gt; before deleting anything. "Forget everything" style requests get an even stronger confirmation step. The rule is that the server never deletes on the first turn, no exceptions. Deletion is irreversible and the confirm step is not optional.&lt;/p&gt;

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

&lt;p&gt;Copilot was genuinely impressive throughout this project, and not just for boilerplate. It understood the shape of the problems I was working through and produced accurate, specific fixes — not generic suggestions I had to rework.&lt;/p&gt;

&lt;p&gt;The FastAPI backend is a good example of how well it read context. The Python package already had well-defined module contracts — &lt;code&gt;add_memory&lt;/code&gt;, &lt;code&gt;search_memories&lt;/code&gt;, &lt;code&gt;list_memories_in_range&lt;/code&gt;, and so on — because the CLI called them directly. I didn't have to explain the isolation rules. Copilot looked at what was already there and scaffolded the route handlers, request and response models, JWT dependency injection, and streaming wiring in a way that preserved the existing boundaries. The CLI and the web backend ended up calling the same Python modules without any duplication, and Copilot kept that consistent as the API surface grew.&lt;/p&gt;

&lt;p&gt;The streaming transport fix is where it really stood out. I described the symptom — replies appearing correct but never actually streaming, and multi-line answers arriving truncated — and Copilot traced the cause correctly: raw token text in the SSE &lt;code&gt;data:&lt;/code&gt; field breaks framing when a token contains a newline. It suggested JSON-encoding the payload so newlines get escaped, and produced the correct backend change (&lt;code&gt;_sse({"t": token})&lt;/code&gt;) and the matching frontend parser in one shot. That's not a simple find-and-replace; it's understanding an SSE framing contract across two different runtimes.&lt;/p&gt;

&lt;p&gt;The word-boundary fix for &lt;code&gt;temporal.extract_range&lt;/code&gt; was similar. I mentioned that "what did I tell you today?" wasn't triggering temporal recall even though the word "today" was in it. Copilot immediately identified that &lt;code&gt;re.fullmatch&lt;/code&gt; was the issue and replaced it with &lt;code&gt;re.search(r"\btoday\b", text)&lt;/code&gt; — exactly the right change, first try.&lt;/p&gt;

&lt;p&gt;It was also consistently good at the fixes that are easy to skip when moving fast: the relevance floor filtering before synthesis, the &lt;code&gt;mode: "recall"&lt;/code&gt; bypass for memory card clicks, the atomic &lt;code&gt;user_id&lt;/code&gt; re-check in the forget confirm flow, the &lt;code&gt;reasons === "install"&lt;/code&gt; guard to show onboarding once. Each of those required understanding what invariant was being protected, not just what line to change. Copilot got them right.&lt;/p&gt;

&lt;p&gt;The part it couldn't do was figure out which problems were worth solving in the first place. Whether the relevance floor belonged at 0.2. Whether "forget everything from yesterday" should still bulk-delete after adding single-item precision. Whether clicking a memory card should trigger a recall mode or just populate the input. Those came from using the product and noticing what felt wrong — and once I knew what I was trying to fix, Copilot was fast and accurate at fixing it.&lt;/p&gt;

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

&lt;p&gt;The biggest thing this project taught me is that "working" and "finished" are completely different states, and the distance between them is easy to underestimate.&lt;/p&gt;

&lt;p&gt;Before working on this finish-up-athon. The concept was proven and everything "worked". Every core feature I'd set out to build was there. But working means the happy path runs cleanly. Finished means real users can hand it to, without you standing behind them explaining what to avoid. That's a much higher bar, and almost none of the work that gets you there shows up in a demo.&lt;/p&gt;

&lt;p&gt;The greeting problem is the most obvious example. In a concept, it doesn't matter that saying "hi" stores "hi" as a memory — you're demonstrating the store-and-recall loop, not stress-testing edge inputs. But hand it to a new user and the first thing they do is say hello. If their first interaction with the product is watching it store "hi" and reply "got it, I'll remember that," you've lost them before they've tried the actual feature. Fixing it meant adding a whole new intent class, a greeting fast-path, and a three-way LLM classifier. That's a non-trivial amount of work for something that looks like a papercut.&lt;/p&gt;

&lt;p&gt;The relevance floor was subtler. Answers seemed correct — the right topic, reasonable phrasing, grounded in real memories. But occasionally an answer felt slightly off, like it was pulling from something adjacent rather than the actual relevant note. The cause was that retrieval returned up to five matches and handed all of them to the model regardless of how relevant they actually were. One weakly related memory, scored just high enough to be included, could quietly skew the answer. You'd only notice after using the app extensively enough to know what it should say. A concept doesn't need that level of correctness. A finished product does.&lt;/p&gt;

&lt;p&gt;The same pattern showed up in smaller places too: the memory card click that sometimes re-saved instead of recalled, the confirm dialog that sat below the chat input on mobile and was barely tappable, the timezone handling that made "today" mean UTC midnight instead of the user's local day. Each one was fine in a controlled walkthrough. Each one would have broken trust within the first few minutes of real use.&lt;/p&gt;

&lt;p&gt;Finishing is mostly invisible work. It's transport correctness and timezone math and confirm dialogs and cursor focus behaviour and relevance filtering and error messages that say something useful instead of a stack trace. None of it makes the feature list longer. All of it is the difference between something you can demo and something you can actually give to people.&lt;/p&gt;

&lt;p&gt;The web UX details are a good example of how subtle this gets. None of these appear in a feature list, but all of them matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cursor returns to the input automatically after a reply, so you don't have to click back&lt;/li&gt;
&lt;li&gt;Textarea stays editable during streaming, even though sending is gated until the response finishes&lt;/li&gt;
&lt;li&gt;Login navigates optimistically to the chat shell and loads prior memories into it in the background, so there's no blank wait&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The concept proved the idea was worth building. The finish-up proved it was worth using. Those are genuinely different achievements, and the second one took longer.&lt;/p&gt;

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

&lt;p&gt;The longer-term goal is a proper mobile app. Most people aren't going to capture memories from a terminal or even a browser tab — they're going to want to open their phone, say or type something quickly, and move on. The FastAPI backend already serves both the CLI and the web client through the same endpoints, so a React Native or native app would slot in without any backend changes.&lt;/p&gt;

&lt;p&gt;Mobile also unlocks the features that make a second brain actually sticky over time. Due-date reminders as push notifications. Background checks that surface overdue items. Voice input so you can capture something while you're walking. The architecture supports all of it — the &lt;code&gt;due_at&lt;/code&gt; column is indexed, the temporal query paths exist, and the reminder logic is already in the backend. What's missing is the client that can actually deliver a notification to your lock screen.&lt;/p&gt;

&lt;p&gt;Beyond that, the roadmap includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Voice input so you can speak a memory instead of typing it&lt;/li&gt;
&lt;li&gt;Push notifications for due-date reminders, delivered at the right time in the user's local timezone&lt;/li&gt;
&lt;li&gt;An installable PWA as a stepping stone while native apps are in progress&lt;/li&gt;
&lt;li&gt;Export to Markdown in addition to JSON, so your data stays readable outside the app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If anyone has built second-brain or personal-knowledge-management tools before and has thoughts on what actually drives long-term retention, I'd genuinely like to hear them.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
