<?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: Azad Mohamed</title>
    <description>The latest articles on DEV Community by Azad Mohamed (@azadhmhd).</description>
    <link>https://dev.to/azadhmhd</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%2F4096202%2F4f5a2f1b-8f70-4741-9d1a-c84ad6ad2f14.jpg</url>
      <title>DEV Community: Azad Mohamed</title>
      <link>https://dev.to/azadhmhd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/azadhmhd"/>
    <language>en</language>
    <item>
      <title>I Got Tired of Losing to ATS Robots in the UAE Job Market, So I Built My Own Career Command Center</title>
      <dc:creator>Azad Mohamed</dc:creator>
      <pubDate>Sun, 25 Oct 2026 15:07:14 +0000</pubDate>
      <link>https://dev.to/azadhmhd/i-got-tired-of-losing-to-ats-robots-in-the-uae-job-market-so-i-built-my-own-career-command-center-4bc0</link>
      <guid>https://dev.to/azadhmhd/i-got-tired-of-losing-to-ats-robots-in-the-uae-job-market-so-i-built-my-own-career-command-center-4bc0</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;"I don't always apply for jobs. But when I do, I apply to 47 of them before lunch and hear back from zero."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let me be honest with you. Getting an interview in the UAE right now is hard. Not "a bit competitive" hard. &lt;strong&gt;"Your CV went into a black hole and the black hole didn't even send a rejection email"&lt;/strong&gt; hard.&lt;/p&gt;

&lt;p&gt;This post is about how that frustration turned into a side project, and how that side project turned into the tool I now use every single day of my job search.&lt;/p&gt;




&lt;h2&gt;
  
  
  😩 The problem: the job hunt is now a full-time job
&lt;/h2&gt;

&lt;p&gt;If you're job hunting in the UAE (or honestly anywhere in 2026), you probably know this loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;There are too many places to look.&lt;/strong&gt; LinkedIn, Indeed, Bayt, GulfTalent, Naukrigulf, plus every company's own career page. Each one has its own login, its own form, its own "upload your CV and then type your CV again manually" experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every JD wants a different CV.&lt;/strong&gt; ATS systems scan for keywords. The same experience written slightly differently is the difference between "shortlisted" and "auto-rejected by a robot at 3 AM." So you end up with &lt;code&gt;CV_final.pdf&lt;/code&gt;, &lt;code&gt;CV_final_v2.pdf&lt;/code&gt;, &lt;code&gt;CV_final_REAL_this_one.pdf&lt;/code&gt;...&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You lose track.&lt;/strong&gt; Which company did I apply to? Which CV did I send? Did that recruiter reply? Was the interview Tuesday or Thursday?&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;"My spreadsheet of job applications had more tabs than my browser. And that's saying something."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🤯 And then you finally get the interview...
&lt;/h2&gt;

&lt;p&gt;Here's the part nobody talks about.&lt;/p&gt;

&lt;p&gt;A lot of us build with AI now. Copilot, Claude, Cursor: they're part of how modern developers ship. That's fine. That's the job now.&lt;/p&gt;

&lt;p&gt;But then you sit in a system design round and the interviewer asks:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"So, why did you choose a message queue here instead of a direct call?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And your brain goes: &lt;em&gt;"...because the AI suggested it and it worked?"&lt;/em&gt; 😅&lt;/p&gt;

&lt;p&gt;Being able to &lt;strong&gt;explain&lt;/strong&gt; architecture, trade-offs and decisions out loud is a separate skill. And you can't build it by reading random "Top 50 Interview Questions" blog posts. You build it by practicing on the questions &lt;strong&gt;you&lt;/strong&gt; actually get asked, and fixing the gaps &lt;strong&gt;you&lt;/strong&gt; actually have.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Interviewer: Walk me through your architecture. Me: &lt;em&gt;walks out of the room&lt;/em&gt;. Not what they meant, apparently."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🛠️ So I built Interview Command Center
&lt;/h2&gt;

&lt;p&gt;I didn't set out to build a product. I built it for myself, because I needed one place to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;track&lt;/strong&gt; every application and interview,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;auto-apply&lt;/strong&gt; to jobs that actually match me,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tailor&lt;/strong&gt; my resume for each JD so it survives the ATS,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;practice&lt;/strong&gt; coding the way interviews are run now,&lt;/li&gt;
&lt;li&gt;and &lt;strong&gt;prepare a crash course&lt;/strong&gt; based on the exact job description and round.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's &lt;strong&gt;open source&lt;/strong&gt;, &lt;strong&gt;self-hosted&lt;/strong&gt;, and it works with whatever AI you have (or none at all).&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/azadhmhd/interview-command-center" rel="noopener noreferrer"&gt;github.com/azadhmhd/interview-command-center&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's everything it does.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✨ Features (all of them, because I'm proud of my child)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. 📋 Application tracker
&lt;/h3&gt;

&lt;p&gt;Table, Kanban and timeline views. Custom statuses, duplicate detection (no more applying to the same role twice, oops), contacts, documents, &lt;strong&gt;which CV version went to which application&lt;/strong&gt;, follow-up reminders, and you can import a job post straight from its URL.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  2. 🤖 Auto Apply
&lt;/h3&gt;

&lt;p&gt;This is the one that saves me hours every week.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finds jobs by your &lt;strong&gt;role and location&lt;/strong&gt; through Google Jobs and Jooble, covering &lt;strong&gt;LinkedIn, Indeed, Bayt, GulfTalent, Naukrigulf&lt;/strong&gt; and company pages. Searching "UAE" also covers every emirate: Dubai, Abu Dhabi, Sharjah, Ajman...&lt;/li&gt;
&lt;li&gt;Watches company boards directly (Greenhouse, Lever, Ashby, SmartRecruiters, Workable), and you can even paste job alert emails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scores each job with reasons&lt;/strong&gt; and hard blockers, so you know &lt;em&gt;why&lt;/em&gt; it's a fit (or why it isn't).&lt;/li&gt;
&lt;li&gt;Builds an &lt;strong&gt;apply kit&lt;/strong&gt; from answers you've saved once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checks your CV's ATS keyword match&lt;/strong&gt; before applying, and tailors a copy if the score is low.&lt;/li&gt;
&lt;li&gt;Applies through your own browser (Claude in Chrome or a local runner). It works on forms that reject normal bots.&lt;/li&gt;
&lt;li&gt;Guardrails built in: it stops on CAPTCHAs and logins, &lt;strong&gt;never guesses answers&lt;/strong&gt;, &lt;strong&gt;never applies twice&lt;/strong&gt;, and keeps proof of every submission.&lt;/li&gt;
&lt;li&gt;A live &lt;strong&gt;Mission Control&lt;/strong&gt; page shows the whole pipeline. Recruiter contacts get saved for later outreach.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;"Auto Apply doesn't get tired, doesn't get sad, and doesn't refresh its inbox every 4 minutes. It's everything I aspire to be."&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h3&gt;
  
  
  3. 📄 Resume builder with AI tailoring
&lt;/h3&gt;

&lt;p&gt;Unlimited resumes and job-specific copies with the same design, a live A4 preview and PDF export. AI tailoring per job &lt;strong&gt;may only reword and reorder. It never invents facts.&lt;/strong&gt; Your CV stays honest; it just speaks the JD's language.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  4. 📓 Interview journal
&lt;/h3&gt;

&lt;p&gt;Every company gets its own sequence of rounds. You record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;exact questions&lt;/strong&gt; you were asked,&lt;/li&gt;
&lt;li&gt;your &lt;strong&gt;original answer&lt;/strong&gt; (locked once saved, so no rewriting history 😄),&lt;/li&gt;
&lt;li&gt;missed concepts, mistakes, code, follow-ups,&lt;/li&gt;
&lt;li&gt;and then improved, approved answers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paste your messy post-interview notes and it splits them into questions automatically.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  5. 🎓 Crash courses built from the JD
&lt;/h3&gt;

&lt;p&gt;This is my answer to the "explain your architecture" problem. Give it the &lt;strong&gt;job description, round type, deadline, your CV and your recorded weak spots&lt;/strong&gt;, and it builds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prioritised topics (with the reason each one matters),&lt;/li&gt;
&lt;li&gt;a time-boxed schedule,&lt;/li&gt;
&lt;li&gt;lessons and model answers,&lt;/li&gt;
&lt;li&gt;flashcards and &lt;strong&gt;mock interviews&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;and readiness tracking so you know if you're actually ready.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;"Crash course" because that's exactly what happens to my confidence 2 days before an interview. Now at least it's a planned crash.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;h3&gt;
  
  
  6. 💻 Coding practice
&lt;/h3&gt;

&lt;p&gt;Multi-stage problems in the style of modern AI-assisted coding interviews, plus pattern sections, in &lt;strong&gt;Python or JavaScript&lt;/strong&gt;. Tests run in your browser, and an AI review scores you on &lt;strong&gt;correctness, approach, verification, code quality and communication&lt;/strong&gt;. Yes, communication. Because "it works on my machine" is not an explanation.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  7. 🧠 Personal knowledge base + spaced repetition
&lt;/h3&gt;

&lt;p&gt;Full-text search across every interview you've ever done. If two companies ask the same question, it links to one entry. A spaced-repetition revision queue makes sure you don't forget what you learned the hard way.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. 🎯 Practice &amp;amp; evaluation
&lt;/h3&gt;

&lt;p&gt;Rubric scoring on correctness, completeness, depth and clarity. Model answers &lt;strong&gt;stay hidden until you try first&lt;/strong&gt; (no cheating, I'm watching you). Retry and compare with your previous attempts.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. ⏰ Automatic prep schedule
&lt;/h3&gt;

&lt;p&gt;Milestones at 3, 2 and 1 day before the interview and on the day itself, with quiet hours, plus a prompt after the interview to capture what happened while it's still fresh.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. 🔌 Bring your own AI
&lt;/h3&gt;

&lt;p&gt;Use any OpenAI-compatible model (Ollama, LM Studio, vLLM, hosted providers), &lt;strong&gt;or&lt;/strong&gt; your existing Claude Code / Cursor subscription. No new subscription needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  11. 🏷️ Honest labels
&lt;/h3&gt;

&lt;p&gt;Every question is marked as &lt;em&gt;actually asked&lt;/em&gt;, &lt;em&gt;public report&lt;/em&gt;, &lt;em&gt;AI prediction&lt;/em&gt; or &lt;em&gt;practice&lt;/em&gt;. Facts pulled from your CV stay "AI-inferred" until you confirm them. No hallucinated confidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  12. 🔒 Private by default &amp;amp; works offline
&lt;/h3&gt;

&lt;p&gt;Self-hosted, multi-user workspaces, per-record sharing, audit log, export and account deletion. And with &lt;strong&gt;no AI configured at all&lt;/strong&gt;, tracking, journaling, search, revision and scheduling still work.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧱 Tech stack (for my fellow nerds)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js 16&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API:&lt;/strong&gt; NestJS 11&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; PostgreSQL 17 (the source of truth, so nothing is lost on restart)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue:&lt;/strong&gt; Redis + BullMQ, with a separate worker for background tasks and scheduling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; MinIO / S3 or local disk&lt;/li&gt;
&lt;li&gt;One &lt;code&gt;docker compose up&lt;/code&gt; and you're running.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/azadhmhd/interview-command-center.git
&lt;span class="nb"&gt;cd &lt;/span&gt;interview-command-center
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# set APP_ENCRYPTION_KEY&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:3000&lt;/code&gt; and you're in.&lt;/p&gt;




&lt;h2&gt;
  
  
  🙏 I need your help (yes, you)
&lt;/h2&gt;

&lt;p&gt;I built this for my own job search, but I know I'm not the only one stuck in this loop. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use it.&lt;/strong&gt; It's free and open source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Break it.&lt;/strong&gt; Then tell me how you broke it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give me feedback.&lt;/strong&gt; What's missing? What's annoying? What would make &lt;em&gt;your&lt;/em&gt; job search easier?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contribute.&lt;/strong&gt; PRs are very welcome.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's still under construction. It's in the "we'll get back to you" stage of its own hiring process. 😄 But with your feedback we can make it much better, and help each other land bigger opportunities and build better futures.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Rejection is just redirection. Usually towards another 'Apply Now' button."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;⭐ If this sounds useful, star the repo, try it out and drop your thoughts in the comments or open an issue:&lt;br&gt;
👉 &lt;a href="https://github.com/azadhmhd/interview-command-center" rel="noopener noreferrer"&gt;github.com/azadhmhd/interview-command-center&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let's get hired. 💼&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Are you job hunting in the UAE or the Gulf right now? What's the hardest part for you: getting the interview or surviving it? Tell me in the comments. 👇&lt;/em&gt;&lt;/p&gt;

</description>
      <category>career</category>
      <category>opensource</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Six tests that passed without testing what they claimed</title>
      <dc:creator>Azad Mohamed</dc:creator>
      <pubDate>Sun, 18 Oct 2026 14:58:26 +0000</pubDate>
      <link>https://dev.to/azadhmhd/six-tests-that-passed-without-testing-what-they-claimed-4h1c</link>
      <guid>https://dev.to/azadhmhd/six-tests-that-passed-without-testing-what-they-claimed-4h1c</guid>
      <description>&lt;p&gt;I recently built a multi-tenant booking API (FastAPI, SQLAlchemy, Postgres, Redis, and a background worker) with Claude Code, as the running example for a book. Writing good tests was an explicit goal from the first commit. The suite grew to 97 tests against real Postgres and Redis, with no mocked database.&lt;/p&gt;

&lt;p&gt;Six of those tests passed without testing what their names claimed. None was careless. Each was plausible, read well, and went green. A green run can't tell a test that checks the behaviour from one that checks nothing, and that's as true when you write the test as when an agent does.&lt;/p&gt;

&lt;p&gt;Here they are, in the order we found them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. "Expired tokens are rejected"
&lt;/h2&gt;

&lt;p&gt;The test built a token with an &lt;code&gt;exp&lt;/code&gt; in the past and asserted a 401. It passed.&lt;/p&gt;

&lt;p&gt;It signed the token with a hard-coded secret, and the app used a different one from &lt;code&gt;.env&lt;/code&gt;. The token was rejected for its &lt;strong&gt;signature&lt;/strong&gt;, not its expiry. With expiry checking switched off, the test still passed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is a positive control.&lt;/strong&gt; Next to the negative assertion, take the same token, change only the property under test, and assert it's &lt;em&gt;accepted&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;expired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&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="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jwt_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;post_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;

&lt;span class="n"&gt;still_valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
                         &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;jwt_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;post_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;still_valid&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;  &lt;span class="c1"&gt;# same token, future exp: accepted
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the control fails, the negative test was never testing expiry.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. "A 2,070-character webhook URL returns 422, not 500"
&lt;/h2&gt;

&lt;p&gt;Contract fuzzing had found a real 500: URLs between 2,049 and 2,083 characters passed the URL validator and then overflowed a 2,048-character column. The regression test used a 2,560-character URL. It got a 422 because the &lt;em&gt;URL validator's own length limit&lt;/em&gt; rejected it, so the database was never reached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; the test asserts its precondition (&lt;code&gt;assert 2048 &amp;lt; len(url) &amp;lt; 2083&lt;/code&gt;), so it can't drift out of the window that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. "Login fails closed when Redis is down"
&lt;/h2&gt;

&lt;p&gt;Login checks two rate limits: per IP and per email. Flipping the per-IP check to fail &lt;em&gt;open&lt;/em&gt; left the test green, because the per-email check still failed closed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; one planted bug per guard, and a test path that exercises each guard alone. Registration only has the per-IP check, and it had no Redis-down test at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. "Two owners removing each other can't leave zero owners"
&lt;/h2&gt;

&lt;p&gt;Two threads, two requests, one assertion. It passed 10 out of 10 runs &lt;strong&gt;with the protective lock removed&lt;/strong&gt;. The threads never actually interleaved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; force the interleaving. Open a second database connection, lock the rows the request needs, start the request, then commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT 1 FROM memberships WHERE org_id = :o &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                       &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AND role = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; FOR UPDATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;org_id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE FROM memberships WHERE org_id = :o AND user_id = :u&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;org_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;u&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;second_owner&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remove_first_owner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# blocks on the lock
&lt;/span&gt;    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the lock in place, it passes 3 out of 3. Without it, it fails 3 out of 3. That's a race test.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. "A booking of exactly 12 hours is allowed"
&lt;/h2&gt;

&lt;p&gt;A Hypothesis property test generated random durations between −24 and +24 hours and checked the rule &lt;code&gt;0 &amp;lt; duration &amp;lt;= 12h&lt;/code&gt;. Moving the boundary by one microsecond (&lt;code&gt;&amp;lt;=&lt;/code&gt; to &lt;code&gt;&amp;lt;&lt;/code&gt;) didn't fail it. Random durations practically never land on exactly 12:00:00.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; pin the boundaries with &lt;code&gt;@example(duration=MAX_BOOKING)&lt;/code&gt; and its neighbours. Property tests are great at the edges of the input space you give them, and blind to the edges inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. No test fetched a single booking
&lt;/h2&gt;

&lt;p&gt;This one isn't a lying test but a missing one. While practising a refactor, we broke &lt;code&gt;GET /bookings/{id}&lt;/code&gt;, and the whole suite stayed green. No test had ever fetched one booking successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one habit that found all six
&lt;/h2&gt;

&lt;p&gt;Every one was found the same way: &lt;strong&gt;break the code on purpose and check that a test goes red.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We turned that into a script. It holds a list of realistic bugs (a missing tenant filter, expiry not enforced, the overlap constraint neutralized, a limiter that fails open, and so on). It applies each bug in turn, runs the tests expected to catch it, and fails if any bug survives. There are 22 now, and the script runs in CI. A pull request that deletes or weakens the test for a protection fails the build, even when every remaining test passes.&lt;/p&gt;

&lt;p&gt;It isn't a general mutation-testing tool. It's a hand-picked list of the bugs this particular service must never ship. Because each bug runs only the test files expected to catch it, it's fast enough for every pull request: about a minute and a half for all 22 on a laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means when an agent writes your tests
&lt;/h2&gt;

&lt;p&gt;AI-written tests fail in exactly these ways, only faster and in greater volume. Three instructions change the outcome more than any amount of "be thorough":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Write the test first and show me it failing, and why."&lt;/strong&gt; The failure message should name the behaviour, not a setup error or a 404 for a route that doesn't exist yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;"Every negative test needs a positive control."&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;"For each new protection, add a planted bug and show it's caught."&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then read &lt;em&gt;which&lt;/em&gt; test went red. A planted bug caught by an unrelated test is a warning, not a success.&lt;/p&gt;




&lt;p&gt;This is adapted from Chapter 9 of my book, &lt;em&gt;Claude Code for Backend Developers&lt;/em&gt;. The book comes with the runnable service these tests are from: &lt;a href="https://azadhmhd.gumroad.com/l/claude-code-backend" rel="noopener noreferrer"&gt;https://azadhmhd.gumroad.com/l/claude-code-backend&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>python</category>
      <category>claude</category>
      <category>ai</category>
    </item>
    <item>
      <title>The 5 Queries I Run First When Postgres Is Slow</title>
      <dc:creator>Azad Mohamed</dc:creator>
      <pubDate>Sun, 18 Oct 2026 08:08:41 +0000</pubDate>
      <link>https://dev.to/azadhmhd/the-5-queries-i-run-first-when-postgres-is-slow-6a</link>
      <guid>https://dev.to/azadhmhd/the-5-queries-i-run-first-when-postgres-is-slow-6a</guid>
      <description>&lt;p&gt;When an API slows down and the database is the suspect, the hardest part isn't the fix. It's knowing where to look while everyone is watching. Here are five read-only queries that find the most common causes of "Postgres is slow" in production, in the order I run them. All of them are safe on a busy primary. Run them as a role with &lt;code&gt;pg_monitor&lt;/code&gt; so you can see other sessions' queries.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tip: start each session with &lt;code&gt;SET statement_timeout = '5s'; SET lock_timeout = '1s';&lt;/code&gt; so even a diagnostic query can't pile onto the problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Is anything blocked, and by whom?
&lt;/h2&gt;

&lt;p&gt;One session holding a lock can stall hundreds of requests. Find every waiting query and the session it's waiting for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;waiting_pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_start&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;waiting_for&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;waiting_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;blocker_pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;blocker_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xact_start&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;blocker_xact_age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;blocker_query&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;
&lt;span class="k"&gt;CROSS&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;LATERAL&lt;/span&gt; &lt;span class="k"&gt;unnest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_blocking_pids&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;bp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait_event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Lock'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at &lt;code&gt;blocker_state&lt;/code&gt;. If it says &lt;code&gt;idle in transaction&lt;/code&gt;, an application (or a forgotten psql session) opened a transaction and walked away while holding locks. A trap worth knowing: a migration &lt;em&gt;waiting&lt;/em&gt; for an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock also blocks every later &lt;code&gt;SELECT&lt;/code&gt; on that table, so a quick &lt;code&gt;ALTER TABLE&lt;/code&gt; stuck behind one slow query can freeze a whole table.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Who is sitting in an open transaction?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;usename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;application_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;xact_start&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;xact_age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;state_change&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;idle_for&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;last_query&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'idle in transaction%'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;state_change&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'1 minute'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;state_change&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These sessions hold locks and stop vacuum from cleaning up. The usual cause is application code that makes an HTTP call or does slow work inside a transaction. The guardrail is &lt;code&gt;ALTER ROLE app SET idle_in_transaction_session_timeout = '60s';&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. How close are we to max_connections?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'max_connections'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'idle'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;idle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'idle in transaction%'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;idle_in_tx&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;backend_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'client backend'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mostly &lt;code&gt;idle&lt;/code&gt; means app pools are oversized (pool size × processes × instances). Mostly &lt;code&gt;active&lt;/code&gt; means slow queries are holding connections. Raising &lt;code&gt;max_connections&lt;/code&gt; is rarely the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Which foreign keys have no index?
&lt;/h2&gt;

&lt;p&gt;Deleting a parent row makes Postgres look up child rows by the FK columns. Without an index, that's a full scan of the child table, inside your transaction, holding locks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conrelid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;string_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;', '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;unnest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conkey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pg_attribute&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attrelid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conrelid&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attnum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;columns&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_constraint&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'f'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_index&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indrelid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conrelid&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string_to_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indkey&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;int2&lt;/span&gt;&lt;span class="p"&gt;[])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;array_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conkey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;@&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conkey&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string_to_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indkey&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="n"&gt;int2&lt;/span&gt;&lt;span class="p"&gt;[])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;array_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conkey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;@&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conkey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix it with &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt;, outside a transaction. In Django that's &lt;code&gt;AddIndexConcurrently&lt;/code&gt; in a migration with &lt;code&gt;atomic = False&lt;/code&gt;; in Rails, &lt;code&gt;algorithm: :concurrently&lt;/code&gt; with &lt;code&gt;disable_ddl_transaction!&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Is any ID about to run out?
&lt;/h2&gt;

&lt;p&gt;Not a slowdown, but the outage nobody sees coming: an &lt;code&gt;integer&lt;/code&gt; primary key stops at 2,147,483,647.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sequencename&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;seq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_value&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2147483647&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;pct_of_int_max&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_sequences&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2147483647&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_value&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(This simple version assumes integer columns. The full check in the kit looks up each sequence's actual column type.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What I don't run first
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;VACUUM FULL&lt;/code&gt; (an exclusive lock for the whole rewrite), restarting the server (it wipes the evidence and the cache), or raising settings at random. Diagnose first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning this into a tool
&lt;/h2&gt;

&lt;p&gt;I packaged these five checks as a free, MIT-licensed CLI that runs them read-only and writes a ranked report: &lt;a href="https://github.com/azadhmhd/postgres-triage-kit" rel="noopener noreferrer"&gt;github.com/azadhmhd/postgres-triage-kit&lt;/a&gt;. The full &lt;strong&gt;Postgres Triage Kit&lt;/strong&gt; adds 15 more checks (top queries, vacuum lag, wraparound, unused and duplicate indexes, replication slots and more) and 9 incident runbooks: &lt;a href="https://azadhmhd.gumroad.com/l/postgres-triage-kit?ref=devto" rel="noopener noreferrer"&gt;Postgres Triage Kit on Gumroad&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All queries tested on PostgreSQL 13–18. Originally published at &lt;a href="https://azadhmhd.com/blog/postgres-slow-first-five-queries" rel="noopener noreferrer"&gt;azadhmhd.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
      <category>sql</category>
    </item>
    <item>
      <title>How to Structure a NestJS Backend for AI-Assisted Development</title>
      <dc:creator>Azad Mohamed</dc:creator>
      <pubDate>Sat, 17 Oct 2026 20:05:56 +0000</pubDate>
      <link>https://dev.to/azadhmhd/how-to-structure-a-nestjs-backend-for-ai-assisted-development-4b7f</link>
      <guid>https://dev.to/azadhmhd/how-to-structure-a-nestjs-backend-for-ai-assisted-development-4b7f</guid>
      <description>&lt;p&gt;AI coding agents are good at writing NestJS code and bad at guessing &lt;em&gt;your&lt;/em&gt; NestJS conventions. Left alone, they put queries in controllers, return entities straight from the database, add a fourth way of throwing errors, and cheerfully offer to run &lt;code&gt;migration:run&lt;/code&gt; against whatever database your &lt;code&gt;.env&lt;/code&gt; points at.&lt;/p&gt;

&lt;p&gt;The fix is not a cleverer prompt. It's a repository that states its rules where agents read them, makes correct changes cheap to verify, and enforces the few rules that must never be broken. Here's the structure I use. None of it needs a particular agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. One instruction file, and only one
&lt;/h2&gt;

&lt;p&gt;Every major agent now reads a project instruction file. Codex, Cursor, GitHub Copilot and Gemini CLI (with one setting) read &lt;code&gt;AGENTS.md&lt;/code&gt;. Claude Code reads &lt;code&gt;CLAUDE.md&lt;/code&gt;, which can import another file. So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- CLAUDE.md --&amp;gt;&lt;/span&gt;
@AGENTS.md

&lt;span class="gu"&gt;## Claude Code specifics&lt;/span&gt;
(skills, subagents, permission rules: only what is genuinely Claude-specific)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.gemini/settings.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"fileName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"AGENTS.md"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules live in &lt;code&gt;AGENTS.md&lt;/code&gt; only. The day you have the same rule in two files, they start to drift, and some tools (Copilot CLI, for one) merge every instruction file they find without a precedence order. Keep &lt;code&gt;AGENTS.md&lt;/code&gt; short (a project map, commands, conventions, safety rules, report format) and link to &lt;code&gt;docs/&lt;/code&gt; for detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Write stop rules, not style advice
&lt;/h2&gt;

&lt;p&gt;"Write clean code" changes nothing. What changes behaviour is a short list of things the agent must not do, written as rules with the exact words you want back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Stop rules&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Do not generate, run or revert migrations until the user tells you to in this conversation.
   An entity change is not permission. Make the code change and report:
   "Schema change detected. Migration required. Not generated or run: no explicit approval."
&lt;span class="p"&gt;2.&lt;/span&gt; Do not run or edit seeds until the user tells you to.
&lt;span class="p"&gt;3.&lt;/span&gt; Do not format code. The pre-commit hook formats staged files; CI checks formatting.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule 3 is a cost rule: formatting runs burn agent time and produce noisy diffs, and Husky + lint-staged already do it on every commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Make the architecture boring and stated
&lt;/h2&gt;

&lt;p&gt;Agents copy what they see. If the codebase has one pattern, they copy that one. Pick an explicit structure and write it down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/&amp;lt;feature&amp;gt;/
  controllers/   HTTP only: routing, guards, Swagger decorators
  services/      business rules; one class per responsibility
  dto/           request DTOs (class-validator, length limits) and response DTOs
  entities/      the data model; no queries, HTTP, email or JWT logic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the dependency rule in one line: &lt;strong&gt;Controller → Service → &lt;code&gt;Repository&amp;lt;Entity&amp;gt;&lt;/code&gt;&lt;/strong&gt;. Other modules inject the owning &lt;em&gt;service&lt;/em&gt;, never another module's repository. And a few "never" rules that agents otherwise break constantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Responses map entities through &lt;code&gt;ResponseDto.fromEntity()&lt;/code&gt;; never return an entity (that's how password hashes end up in JSON).&lt;/li&gt;
&lt;li&gt;User-owned data is looked up by the authenticated ID from &lt;code&gt;@CurrentUser()&lt;/code&gt;, never by an ID from the request alone.&lt;/li&gt;
&lt;li&gt;Clients never set server-owned fields (status, verification state, roles).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A global &lt;code&gt;ValidationPipe&lt;/code&gt; with &lt;code&gt;whitelist: true, forbidNonWhitelisted: true&lt;/code&gt; backs the last rule up mechanically: a DTO that doesn't declare &lt;code&gt;status&lt;/code&gt; can't receive it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. One error contract
&lt;/h2&gt;

&lt;p&gt;Give the agent exactly one way to fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppException&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* status + stable code */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotFoundError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AppException&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConflictError&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AppException&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConflictError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EMAIL_ALREADY_REGISTERED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email is already registered&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single global exception filter turns every error, including unexpected ones, into the same shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EMAIL_ALREADY_REGISTERED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/v1/auth/register"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"requestId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6f1c..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stable &lt;code&gt;UPPER_SNAKE&lt;/code&gt; codes give agents (and frontends) something to assert on, and the filter is the one place that guarantees no SQL, stack trace or SMTP detail leaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Make verification cheap, or it won't happen
&lt;/h2&gt;

&lt;p&gt;An agent will run the checks you make easy. If tests need a database, Docker and seed data, the agent either skips them or starts containers you didn't ask for. So make the default suites infrastructure-free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests&lt;/strong&gt; construct services directly with a small in-memory repository that implements only the TypeORM methods your services use:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="cm"&gt;/** Minimal in-memory stand-in for the TypeORM repository methods used by the services. */&lt;/span&gt;
  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MockRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// findOne, findAndCount, update... only what's used&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;E2E tests&lt;/strong&gt; boot the real &lt;code&gt;AppModule&lt;/code&gt; with the real pipes, guards, filters and throttler, and override only the repositories and the mail service. That tests authorization and validation where the bugs actually are, in about ten seconds, with no database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then tell the agent the exact commands, narrowest first (&lt;code&gt;npx jest &amp;lt;path&amp;gt;&lt;/code&gt;), and that a red test is fixed for a real reason, never by weakening the assertion.&lt;/p&gt;

&lt;p&gt;Be honest about the gap: in-memory repositories don't test SQL, constraints or cascades. Say so in &lt;code&gt;docs/testing.md&lt;/code&gt;, and check migrations by hand (up, down, up) against a disposable PostgreSQL before a release.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Enforce the rules that matter; write down the rest
&lt;/h2&gt;

&lt;p&gt;Written rules are followed most of the time. For the few operations that can destroy data, add enforcement your tool supports. In Claude Code, &lt;code&gt;ask&lt;/code&gt; permission rules prompt before matching commands even in permissive modes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ask"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Bash(npm run migration:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(npm run seed *)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(psql *)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"Bash(docker compose down *)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git push *)"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Read(**/.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(**/.env.production)"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Know the limits: these rules match command text. A script that runs a migration indirectly isn't caught, and a deny on reading &lt;code&gt;.env&lt;/code&gt; doesn't stop code that opens the file. For other agents, configure their command approval for the same commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Demand a fixed report
&lt;/h2&gt;

&lt;p&gt;The most useful line in my &lt;code&gt;AGENTS.md&lt;/code&gt; is the report format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Implemented       what changed and why
Files changed     paths
Verification      commands actually run and their actual results
Not performed     e.g. migration generation, seed execution, Docker, deploy
Unverified        anything you could not check
Never claim a command passed if you did not run it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It turns "Done! Everything works" into something you can check in thirty seconds, and the "Not performed" line is where the pending migration shows up instead of silently happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Turn recurring jobs into procedures
&lt;/h2&gt;

&lt;p&gt;When you notice you're giving the same multi-step instruction for the third time, make it a file. In Claude Code these are skills (&lt;code&gt;.claude/skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;), loaded on demand; for other agents the same Markdown works as "follow &lt;code&gt;.claude/skills/debug/SKILL.md&lt;/code&gt;". Good candidates in a NestJS API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;generate&lt;/strong&gt;: inspect an existing module first, reuse before creating, add tests, report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;database-change&lt;/strong&gt;: check entity conventions, decide whether a migration is needed, review an existing migration's &lt;code&gt;down()&lt;/code&gt;, and never generate one without a request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;debug&lt;/strong&gt;: map symptoms to the right file (&lt;code&gt;400 property X should not exist&lt;/code&gt; → the DTO is missing a field), reproduce with a database-free test, fix at the root.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Name them so they don't shadow built-in commands (&lt;code&gt;review-change&lt;/code&gt;, not &lt;code&gt;review&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;One &lt;code&gt;AGENTS.md&lt;/code&gt;; tool files import it.&lt;/li&gt;
&lt;li&gt;Stop rules with exact words, not style advice.&lt;/li&gt;
&lt;li&gt;One structure, one dependency direction, a few explicit "nevers".&lt;/li&gt;
&lt;li&gt;One error class hierarchy and one filter.&lt;/li&gt;
&lt;li&gt;Default test suites that need no infrastructure.&lt;/li&gt;
&lt;li&gt;Tool-enforced prompts for destructive commands.&lt;/li&gt;
&lt;li&gt;A fixed report that separates "verified" from "not performed".&lt;/li&gt;
&lt;li&gt;Recurring tasks as procedure files.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;I've packaged this structure, with a complete NestJS 11 + PostgreSQL implementation (auth, admin API, migration, 96 tests, Docker, and the agent files above), as a paid starter kit. If you'd rather start from it than build your own, it's here: &lt;a href="https://azadhmhd.gumroad.com/l/nestjs-production-starter-kit?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=launch_v1&amp;amp;utm_content=structure_article" rel="noopener noreferrer"&gt;NestJS Production Starter Kit for AI Coding Agents&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>typescript</category>
      <category>ai</category>
      <category>backend</category>
    </item>
    <item>
      <title>I Kept Forgetting to Drink Water While Coding, So I Built Kairo</title>
      <dc:creator>Azad Mohamed</dc:creator>
      <pubDate>Wed, 26 Aug 2026 18:22:42 +0000</pubDate>
      <link>https://dev.to/azadhmhd/i-kept-forgetting-to-drink-water-while-coding-so-i-built-kairo-4dd2</link>
      <guid>https://dev.to/azadhmhd/i-kept-forgetting-to-drink-water-while-coding-so-i-built-kairo-4dd2</guid>
      <description>&lt;p&gt;I spend a lot of time in front of a computer.&lt;/p&gt;

&lt;p&gt;And like probably a lot of developers, when I'm deep into work, I completely forget about everything around me.&lt;/p&gt;

&lt;p&gt;Drink water? Forgot.&lt;/p&gt;

&lt;p&gt;Take a break? Forgot.&lt;/p&gt;

&lt;p&gt;Look away from the screen? Forgot.&lt;/p&gt;

&lt;p&gt;Move around for a few minutes? Maybe after another three hours. 😅&lt;/p&gt;

&lt;p&gt;Living in the UAE makes this even easier. It's hot outside for a good part of the year, so it's very easy to stay indoors, sit in front of a screen, turn on the AC, and just keep working.&lt;/p&gt;

&lt;p&gt;At some point I thought:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why don't I just build something to remind me?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Kairo&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;Kairo is a small desktop companion designed to help build healthier computer habits.&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💧 Remind me to drink water&lt;/li&gt;
&lt;li&gt;👀 Remind me to rest my eyes&lt;/li&gt;
&lt;li&gt;🚶 Remind me to take breaks&lt;/li&gt;
&lt;li&gt;🧘 Encourage healthier working habits&lt;/li&gt;
&lt;li&gt;📊 Keep track of what I've actually been doing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something that felt less like a notification from an operating system and more like a small companion sitting alongside me while I work.&lt;/p&gt;

&lt;p&gt;That's where the animated character comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  I wanted it to be private
&lt;/h2&gt;

&lt;p&gt;One thing I didn't want was another productivity application that required an account and sent all my activity to some server.&lt;/p&gt;

&lt;p&gt;So I designed Kairo around an &lt;strong&gt;offline-first&lt;/strong&gt; approach.&lt;/p&gt;

&lt;p&gt;The core application works locally and stores its data on the machine.&lt;/p&gt;

&lt;p&gt;AI is also completely optional.&lt;/p&gt;

&lt;p&gt;If you want AI-powered coaching, Kairo can connect to providers such as &lt;strong&gt;Ollama&lt;/strong&gt; or &lt;strong&gt;LM Studio&lt;/strong&gt;, so you can run the AI locally instead of sending your data to a cloud service.&lt;/p&gt;

&lt;p&gt;The application should still work perfectly fine without AI.&lt;/p&gt;

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

&lt;p&gt;Honestly, this wasn't originally meant to become a big open-source project.&lt;/p&gt;

&lt;p&gt;I built it because &lt;strong&gt;I needed it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I kept noticing the same pattern in my own work:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Start coding → get focused → lose track of time → forget water → forget breaks → realize hours have passed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If Kairo can interrupt that cycle even a little, then it has already done what I wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  A little bit about the engineering
&lt;/h2&gt;

&lt;p&gt;Although the idea is simple, I wanted to use the project to experiment with building a proper desktop application.&lt;/p&gt;

&lt;p&gt;Kairo is built with &lt;strong&gt;Flutter&lt;/strong&gt; and is currently focused on macOS.&lt;/p&gt;

&lt;p&gt;Some of the things I'm working with include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flutter desktop&lt;/li&gt;
&lt;li&gt;SQLite for local persistence&lt;/li&gt;
&lt;li&gt;Event-driven communication&lt;/li&gt;
&lt;li&gt;Modular packages&lt;/li&gt;
&lt;li&gt;Native desktop integration&lt;/li&gt;
&lt;li&gt;Offline-first architecture&lt;/li&gt;
&lt;li&gt;Optional AI providers&lt;/li&gt;
&lt;li&gt;Local AI through OpenAI-compatible endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One design decision I'm particularly happy with is keeping AI &lt;strong&gt;out of the critical reminder path&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reminder system shouldn't stop working just because an AI provider is unavailable.&lt;/p&gt;

&lt;p&gt;AI should enhance the experience, not become a dependency for the basic functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's still a work in progress
&lt;/h2&gt;

&lt;p&gt;There are plenty of things I want to improve.&lt;/p&gt;

&lt;p&gt;I'm still figuring out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better reminder behaviour&lt;/li&gt;
&lt;li&gt;More useful habit insights&lt;/li&gt;
&lt;li&gt;Better AI coaching&lt;/li&gt;
&lt;li&gt;More customization&lt;/li&gt;
&lt;li&gt;Improving the desktop experience&lt;/li&gt;
&lt;li&gt;Supporting more platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And this is where I'd really like some feedback.&lt;/p&gt;

&lt;p&gt;If you spend most of your day working at a computer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What would actually help you build healthier working habits?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not another productivity dashboard.&lt;/p&gt;

&lt;p&gt;Not another app asking you to track 20 things.&lt;/p&gt;

&lt;p&gt;Just something that quietly helps you remember the basic things we tend to forget when we're focused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try Kairo
&lt;/h2&gt;

&lt;p&gt;The project is open source:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/azadhmhd/kairo" rel="noopener noreferrer"&gt;https://github.com/azadhmhd/kairo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you find it useful, give it a try.&lt;/p&gt;

&lt;p&gt;And if you have ideas, suggestions, or even things you think I've approached badly, I'd love to hear them.&lt;/p&gt;

&lt;p&gt;I built Kairo for myself, but maybe it can become something useful for other developers too.&lt;/p&gt;

&lt;p&gt;Maybe we can build a healthier workspace together.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
