<?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: Ravi</title>
    <description>The latest articles on DEV Community by Ravi (@ravipurohit1991).</description>
    <link>https://dev.to/ravipurohit1991</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%2F4023193%2F23065849-edf2-438e-8bf4-c20471ca13aa.jpg</url>
      <title>DEV Community: Ravi</title>
      <link>https://dev.to/ravipurohit1991</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ravipurohit1991"/>
    <language>en</language>
    <item>
      <title>Mirchi &amp; Mirabelle: every dish on this menu is two places that never met 🍲</title>
      <dc:creator>Ravi</dc:creator>
      <pubDate>Tue, 11 Aug 2026 12:18:56 +0000</pubDate>
      <link>https://dev.to/ravipurohit1991/mirchi-mirabelle-every-dish-on-this-menu-is-two-places-that-never-met-2hof</link>
      <guid>https://dev.to/ravipurohit1991/mirchi-mirabelle-every-dish-on-this-menu-is-two-places-that-never-met-2hof</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge - Comfort Food Edition, Perfect Landing&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;Mirchi &amp;amp; Mirabelle&lt;/strong&gt; is the landing page for a supper club that doesn't exist. It serves six courses, and every course is two places that have never met, put on one plate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; I am not an expert JS developer. Just a guy with some thinking capacity and a lot of time to bother Claude to build something from that.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;🔗 &lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://ravipurohit1991.github.io/MirchiMirabelle/" rel="noopener noreferrer"&gt;https://ravipurohit1991.github.io/MirchiMirabelle/&lt;/a&gt;&lt;br&gt;
📦 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/ravipurohit1991/MirchiMirabelle" rel="noopener noreferrer"&gt;https://github.com/ravipurohit1991/MirchiMirabelle&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: Code is completely written by Claude, the story and the dishes are mine :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Motivation:
&lt;/h2&gt;

&lt;p&gt;I was born in Jodhpur, grew up in Coimbatore and Tiruppur, spent ten years moving between Bordeaux, Metz, Poitiers, Toulouse and Grenoble, and now live in Copenhagen. Once a year I land in Kolkata, where my brother is.&lt;/p&gt;

&lt;p&gt;For a long time I thought that made my food palette incoherent. It turns out it just makes the coincidences visible. Lets take beans for example: Rajasthan dries beans because nothing fresh survives the desert; Toulouse soaks beans overnight because that's how cassoulet works. Nobody copied anybody. Both places just looked at a dried bean and reached the same conclusion.&lt;/p&gt;

&lt;p&gt;So the menu is built out of those collisions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Course&lt;/th&gt;
&lt;th&gt;Dish&lt;/th&gt;
&lt;th&gt;Parents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;I&lt;/td&gt;
&lt;td&gt;Cassoulet de Sangri&lt;/td&gt;
&lt;td&gt;Ker sangri, Marwar × cassoulet, Toulouse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;II&lt;/td&gt;
&lt;td&gt;Degree Coffee Canelé&lt;/td&gt;
&lt;td&gt;Kumbakonam decoction × canelé bordelais&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;III&lt;/td&gt;
&lt;td&gt;Gratin Dauphinois, Kongu Tadka&lt;/td&gt;
&lt;td&gt;Grenoble × Coimbatore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IV&lt;/td&gt;
&lt;td&gt;Rugbrød Baati&lt;/td&gt;
&lt;td&gt;Dal baati × Danish rye&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V&lt;/td&gt;
&lt;td&gt;Mirabelle Phuchka&lt;/td&gt;
&lt;td&gt;Kolkata street water × Lorraine plums&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VI&lt;/td&gt;
&lt;td&gt;Mishti Skyr&lt;/td&gt;
&lt;td&gt;Bengal clay pot × Nordic straining&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Underneath the menu is the part I actually enjoyed building: &lt;strong&gt;the pairing board&lt;/strong&gt;. Five places, ten intersections, pick any two and the dish between them appears.&lt;/p&gt;

&lt;p&gt;Halfway through writing those ten pairings I realised I wasn't inventing all of them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rajasthan's &lt;strong&gt;mirchi vada&lt;/strong&gt; and Tamil Nadu's &lt;strong&gt;milagai bajji&lt;/strong&gt; are the same chilli in the same batter, 1,800 km apart, neither borrowed.&lt;/li&gt;
&lt;li&gt;Tamil &lt;strong&gt;pazhaya sadam&lt;/strong&gt; and Bengali &lt;strong&gt;panta bhat&lt;/strong&gt; are both last night's rice left in water overnight, and both regions will tell you it's the best breakfast in India.&lt;/li&gt;
&lt;li&gt;France calls its best pastry &lt;em&gt;viennoiserie&lt;/em&gt;, "the Viennese things." Denmark calls its best pastry &lt;em&gt;wienerbrød&lt;/em&gt;, "Vienna bread." The rest of the world calls it Danish. Nobody involved is claiming it.&lt;/li&gt;
&lt;li&gt;And France was in Bengal long before I got to France. &lt;strong&gt;Chandannagar&lt;/strong&gt; flew the tricolour until 1950, an hour up the Hooghly from my brother's flat.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I'd do next:
&lt;/h3&gt;

&lt;p&gt;I want the pairing board to accept places it's never heard of. Right now it knows five, the five I have lived in; potentially allowing people to add their places. &lt;/p&gt;

&lt;p&gt;There's a small thing at the bottom of the page that writes you a line and copies it to your clipboard, and it exists entirely so I can ask this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What are your two places?&lt;/strong&gt; The ones that got mixed together somewhere along the way, and what's the dish that lives between them? I'll take any pairing, including the ones you think don't work, those are usually the good ones.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I used to smoke 20 a day. I'm down to 5. Not me, but AI built the app that helped.</title>
      <dc:creator>Ravi</dc:creator>
      <pubDate>Thu, 06 Aug 2026 11:46:34 +0000</pubDate>
      <link>https://dev.to/ravipurohit1991/i-used-to-smoke-20-a-day-im-down-to-5-not-me-but-ai-built-the-app-that-helped-434j</link>
      <guid>https://dev.to/ravipurohit1991/i-used-to-smoke-20-a-day-im-down-to-5-not-me-but-ai-built-the-app-that-helped-434j</guid>
      <description>&lt;p&gt;Let me start with the honest part, because it's the whole point of this post.&lt;/p&gt;

&lt;p&gt;I am not an Android developer. Before this I had never written a line of Kotlin. I didn't know what Jetpack Compose was, or what a widget actually is under the hood. What I had was an idea and a problem: I was smoking about 20 cigarettes a day and I wanted to stop.&lt;/p&gt;

&lt;p&gt;I built it for myself. It's on GitHub (Free) now in case it helps someone else: if it helps even one person, that's mission success for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why counting worked when nothing else did
&lt;/h2&gt;

&lt;p&gt;I tried the usual things. Willpower. "I'll stop after this pack." None of it stuck, because I genuinely had no idea how much I was smoking. It was just... a thing my hands did (not sure if I am addicted to the nicotine, the action of mere smoking, or the warmth of the smoke in my mouth, etc..). Research papers are filled with non-correlative or non-convincing conclusions.&lt;/p&gt;

&lt;p&gt;The app &lt;strong&gt;Pace&lt;/strong&gt; does one thing: count. One tap, one cigarette. And then it showed me the number I had never actually looked at before.&lt;/p&gt;

&lt;p&gt;Seeing "14" at 4pm hits differently than a vague feeling of "I smoke a bit much."&lt;/p&gt;

&lt;p&gt;Then I added the second half, which turned out to be the important one: &lt;strong&gt;how much time is left until the next one&lt;/strong&gt;. Not a rule, not a punishment: just a countdown. Tuns out, for me countdown provided the patience and will power to hold on for another 30 minutes. And something about waiting for a timer instead of reaching for a pocket changed the whole thing for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The widget that appears on the homescreen is the part that actually changed my habit
&lt;/h2&gt;

&lt;p&gt;This is the bit I want to say properly.&lt;br&gt;
Our phones have become body parts. We look at them every few minutes without deciding to. So I put the counter and the countdown on my home screen as a widget.&lt;/p&gt;

&lt;p&gt;Now I don't have to remember to open an app during a craving. I unlock my phone for something completely unrelated and the reminder is just &lt;em&gt;there&lt;/em&gt;. "3 today. 22 minutes to go."&lt;/p&gt;

&lt;p&gt;It's constant, but it doesn't nag. And honestly, seeing that little card a hundred times a day is the reason my confidence has changed. For the first time I actually believe I will be able to quit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding: Five minutes of distraction gets you past it
&lt;/h2&gt;

&lt;p&gt;Here's the other thing I learned about myself: a craving is not permanent. It's about five minutes long. If I can get through those five minutes, I am fine.&lt;/p&gt;

&lt;p&gt;So Pace sends me a notification when a craving is likely, and it opens straight into a chat. Ollama LLM model (with a personality) then tries to distract. Not a lecture, just something to talk about, or a riddle, or one silly specific thing to do for two minutes. The distraction eats the five minutes, and then it passes, and then it's ok.&lt;/p&gt;

&lt;p&gt;That's the entire trick. It sounds too simple to work. It worked for me. Sharing it here not for views or stars or collaboration or anything. Even if it helps 1 person, I am happy, thats the purpose of this post. &lt;/p&gt;

&lt;h2&gt;
  
  
  So how did someone who can't code build an Android app?
&lt;/h2&gt;

&lt;p&gt;I used AI. Specifically Claude Code completely to build the app and improve it.&lt;/p&gt;

&lt;p&gt;I want to be clear about the split of work, because I think people oversell this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I did:&lt;/strong&gt; described what I wanted in plain English. Used the app every single day on my own phone. Found the things that felt wrong and said so "the countdown on the widget is frozen", "I tapped it by accident in my pocket and it logged a cigarette I never smoked", "this animation feels sluggish". Made pretty much every decision about how it should behave.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the AI did:&lt;/strong&gt; all of the actual Kotlin. The database, the Compose screens, the widget, the notifications, the tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'm honest about:&lt;/strong&gt; I do not fully understand every file in that repository. I can read it and follow it, but I couldn't have written it. If a real Android developer looks at the code and finds things to criticise, they're probably right, and I would like to hear it.&lt;/p&gt;

&lt;p&gt;The parts I did go through carefully are the ones about my own data: where things get stored, what leaves the phone. (Answer: everything stays on the device. There's no server, no account, no analytics. The AI chat is optional and uses your own API key.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I am now
&lt;/h2&gt;

&lt;p&gt;From ~20 a day to &lt;strong&gt;5–6 a day&lt;/strong&gt;. Some days 4.&lt;br&gt;
I haven't quit yet. Iam not going to pretend I have. But I'm not where I was, and for the first time it feels like a matter of when rather than if.&lt;/p&gt;

&lt;p&gt;If you are in the same boat, the code is here: &lt;strong&gt;&lt;a href="https://github.com/ravipurohit1991/pace" rel="noopener noreferrer"&gt;github.com/ravipurohit1991/pace&lt;/a&gt;&lt;/strong&gt;. It's free, there's nothing to sign up for, and I'm not selling anything.&lt;/p&gt;

&lt;p&gt;And if you have got an idea for something small that would make your own life a bit better: I think this is genuinely the moment to try building it. Not knowing how to code used to be the end of the conversation. It isn't anymore.&lt;/p&gt;

&lt;p&gt;Good luck. Genuinely.&lt;/p&gt;

</description>
      <category>community</category>
      <category>discuss</category>
      <category>ai</category>
      <category>resources</category>
    </item>
    <item>
      <title>What Is the Most Technically Impressive Useless Thing You Have Built?</title>
      <dc:creator>Ravi</dc:creator>
      <pubDate>Thu, 23 Jul 2026 20:59:41 +0000</pubDate>
      <link>https://dev.to/ravipurohit1991/what-is-the-most-technically-impressive-useless-thing-you-have-built-7eg</link>
      <guid>https://dev.to/ravipurohit1991/what-is-the-most-technically-impressive-useless-thing-you-have-built-7eg</guid>
      <description>&lt;p&gt;We have plenty of posts on DEV announcing new tools, libraries, agents, and frameworks.&lt;/p&gt;

&lt;p&gt;I want to hear about the opposite.&lt;/p&gt;

&lt;p&gt;What did you build that worked perfectly, perhaps even taught you a lot, but nobody actually needed?&lt;/p&gt;

&lt;p&gt;Maybe it solved a problem that only you had or perhaps did not exist. Maybe an existing tool already did it better. Maybe you spent three weekends automating something that took five minutes per year. Or perhaps you built it simply because the idea was fun.&lt;/p&gt;

&lt;p&gt;There is nothing wrong with building for curiosity. The problem begins when we confuse “I enjoyed making this” with “the world needs this product.”&lt;/p&gt;

&lt;p&gt;Before starting another project, I am trying to ask:&lt;br&gt;
Who actually has this problem?&lt;br&gt;
How are they solving it today?&lt;br&gt;
Would they change their current process to use my solution?&lt;br&gt;
Am I building this because it is needed, or simply because I want to build it?&lt;/p&gt;

&lt;p&gt;Both answers can be valid. A hobby project does not need a market. But we should at least be honest with ourselves about which one it is.&lt;/p&gt;

&lt;p&gt;So, what is your most impressive useless project?&lt;br&gt;
What did it do, why did nobody use it, and what did you learn from building it?&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>programming</category>
      <category>showdev</category>
      <category>career</category>
    </item>
    <item>
      <title>Why do so many comments here Feel AI-Generated?</title>
      <dc:creator>Ravi</dc:creator>
      <pubDate>Fri, 10 Jul 2026 17:49:04 +0000</pubDate>
      <link>https://dev.to/ravipurohit1991/why-do-so-many-comments-here-feel-ai-generated-5h16</link>
      <guid>https://dev.to/ravipurohit1991/why-do-so-many-comments-here-feel-ai-generated-5h16</guid>
      <description>&lt;p&gt;I have used AI tools quite a lot for writing, so I have started noticing certain patterns: polished but vague compliments, repeated sentence structures, overly balanced phrasing, and the occasional suspiciously enthusiastic “Great insights!”.&lt;/p&gt;

&lt;p&gt;I absolutely hate the frequent em dashes "—", which screams AI-generative text. (happy hunting).&lt;/p&gt;

&lt;p&gt;Of course, none of these things prove that a comment was written by AI. Plenty of people naturally write this way (not that I know any of them), and many others may use AI simply to improve their grammar or express themselves more clearly.&lt;/p&gt;

&lt;p&gt;But it does make me wonder:&lt;/p&gt;

&lt;p&gt;Are people genuinely reading posts and responding because they have something to say?&lt;/p&gt;

&lt;p&gt;Or are some comments being generated mainly for visibility, engagement, badges, or profile activity?&lt;/p&gt;

&lt;p&gt;I don’t think using AI to help write a comment is necessarily bad. I do it too, I see the benefit of it. The real issue is whether the person behind the comment actually reads the post and do they mean what they write ?&lt;/p&gt;

&lt;p&gt;And yes, absolutely, I am fully aware of the irony.&lt;br&gt;
The main purpose of this post is also visibility and engagement.&lt;/p&gt;

</description>
      <category>devto</category>
      <category>ai</category>
      <category>productivity</category>
      <category>community</category>
    </item>
    <item>
      <title>I made a template for building full-stack app from the stack I keep reusing</title>
      <dc:creator>Ravi</dc:creator>
      <pubDate>Thu, 09 Jul 2026 23:36:41 +0000</pubDate>
      <link>https://dev.to/ravipurohit1991/i-made-a-template-for-building-full-stack-app-from-the-stack-i-keep-reusing-5ank</link>
      <guid>https://dev.to/ravipurohit1991/i-made-a-template-for-building-full-stack-app-from-the-stack-i-keep-reusing-5ank</guid>
      <description>&lt;p&gt;Over the past few years I have had to build a few different apps (having no formal CS background), usually some version of a full-stack product with an API, web UI, CLI tooling, packaging, etc.&lt;/p&gt;

&lt;p&gt;Using the tech stack and the knowledge acquired during the last few years, I have tried to ask Claude to make a reuseable template based on that. Putting it out here, in case it helps anyone.&lt;/p&gt;

&lt;p&gt;OpenProductKit: github.com/ravipurohit1991/OpenProductKit&lt;/p&gt;

&lt;p&gt;It’s a Copier template that generates a product repo with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI backend&lt;/li&gt;
&lt;li&gt;React/Vite frontend&lt;/li&gt;
&lt;li&gt;Typer CLI&lt;/li&gt;
&lt;li&gt;pywebview desktop app&lt;/li&gt;
&lt;li&gt;plugin API&lt;/li&gt;
&lt;li&gt;licensing hooks&lt;/li&gt;
&lt;li&gt;generated OpenAPI TypeScript client&lt;/li&gt;
&lt;li&gt;an Agent.md file so any AI agent can fill in your app idea into the template&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main idea is to keep the business logic in a framework-free Python core (this was a personal choice), and then build the web, CLI, and desktop surfaces around that same core.&lt;/p&gt;

&lt;p&gt;It is still early, and I’m sure there are things that are overbuilt, missing, or just based too much on my own way of working.&lt;/p&gt;

&lt;p&gt;I would love feedback from people who have built/shipped full-stack apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;does this structure feel useful?&lt;/li&gt;
&lt;li&gt;what would you change based on your experience? &lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>react</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A product template your AI agent can finish</title>
      <dc:creator>Ravi</dc:creator>
      <pubDate>Thu, 09 Jul 2026 20:33:27 +0000</pubDate>
      <link>https://dev.to/ravipurohit1991/a-product-template-your-ai-agent-can-finish-3c29</link>
      <guid>https://dev.to/ravipurohit1991/a-product-template-your-ai-agent-can-finish-3c29</guid>
      <description>&lt;p&gt;OpenProductKit is a white-label, full-stack template for shipping commercial apps across web, CLI and desktop from one decoupled core.&lt;/p&gt;

&lt;p&gt;To prove the pitch, I generated a project from it and reworked that generated app into &lt;a href="https://github.com/ravipurohit1991/tally-time-tracker" rel="noopener noreferrer"&gt;Tally&lt;/a&gt;, a real freelancer time tracker, using an AI coding agent that followed nothing but the generated project's own &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The repo history is the tutorial: the first commit is the pristine template output, and every commit after it is one layer of the rework.&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%2F99fgmcmckryrv55f01k7.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%2F99fgmcmckryrv55f01k7.png" alt="OpenProductKit architecture infographic" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap between a boilerplate and a product
&lt;/h2&gt;

&lt;p&gt;Every developer has a folder full of app ideas. Most die in the same place: not at the idea, but at the product part.&lt;/p&gt;

&lt;p&gt;The licensing. The desktop packaging. The typed API client that drifts out of sync. The migrations. The "how do I rebrand this template without a find-and-replace mess" problem.&lt;/p&gt;

&lt;p&gt;Boilerplates solve day one. Almost none solve week four, when you need to sell a license key, ship a Windows build, or pull upstream template fixes into a project you have heavily customized.&lt;/p&gt;

&lt;p&gt;And in 2026 there is a question classic boilerplates were not designed to answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does your AI coding agent do with the template?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You are probably not hand-writing every route and component anymore. You generate a project, open Claude Code, Cursor or Codex, and say: "build my idea into this." Most templates give the agent no rules, no map, and no replacement recipe, so it guesses. The architecture starts eroding from the first commit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ravipurohit1991/OpenProductKit" rel="noopener noreferrer"&gt;OpenProductKit&lt;/a&gt; is built to close both gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you start from
&lt;/h2&gt;

&lt;p&gt;OpenProductKit is a &lt;a href="https://copier.readthedocs.io" rel="noopener noreferrer"&gt;Copier&lt;/a&gt; template. One command, a few questions, and you get a branded monorepo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One decoupled core&lt;/strong&gt;: &lt;code&gt;packages/core&lt;/code&gt; is pure Python with dataclasses, ports and services. No FastAPI, no database, no HTTP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thin adapters&lt;/strong&gt;: FastAPI, Typer, React and pywebview sit around the same business logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop without a sidecar&lt;/strong&gt;: the same React UI runs in a native window and calls the core in-process over a JS bridge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real licensing&lt;/strong&gt;: Ed25519-signed offline tokens, vendor tooling, route gates and lock-card UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugins&lt;/strong&gt;: Python entry-point plugins with license gating and an admin UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generated typed client&lt;/strong&gt;: OpenAPI to TypeScript, so backend drift becomes a compile error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrations, tests, docs and CI&lt;/strong&gt;: all rendered into the generated project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/strong&gt;: pre-rendered with your package names, CLI command and architecture rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The template ships a deliberately small demo domain, a notes-and-tags "Resource Vault", threaded through every layer. Every demo line is fenced with a grep-able &lt;code&gt;[demo]&lt;/code&gt; marker. It exists to be deleted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proof: building Tally
&lt;/h2&gt;

&lt;p&gt;Tally is a local-first time tracker for freelancers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clients with hourly rates&lt;/li&gt;
&lt;li&gt;One running timer at a time&lt;/li&gt;
&lt;li&gt;Recent time entries&lt;/li&gt;
&lt;li&gt;Weekly billable reports&lt;/li&gt;
&lt;li&gt;CSV export as a paid feature&lt;/li&gt;
&lt;li&gt;Web, CLI and desktop surfaces&lt;/li&gt;
&lt;li&gt;Same core, same data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the generated app after replacing the demo with realistic Tally data:&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%2Fxba174waqfbmo9wdiu9a.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%2Fxba174waqfbmo9wdiu9a.png" alt="Tally timer screenshot" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the weekly billable report:&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%2Fb7e11f5k1xlsc2p1afjz.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%2Fb7e11f5k1xlsc2p1afjz.png" alt="Tally report screenshot" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: generate the project
&lt;/h2&gt;

&lt;p&gt;The first commit is just the output of the template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx copier copy gh:ravipurohit1991/OpenProductKit tally
&lt;span class="c"&gt;# project_name=Tally, cli_name=tally, pkg_slug=tally&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That initial project already runs. It has a web UI, CLI, desktop shell, licensing, plugins, migrations, docs, CI and tests. That matters because it makes the rest of the history honest: everything after commit zero is the product rework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: let the agent read AGENTS.md
&lt;/h2&gt;

&lt;p&gt;The generated &lt;code&gt;AGENTS.md&lt;/code&gt; opens with the rule that keeps the codebase sane:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Business logic never leaves &lt;code&gt;packages/core&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;FastAPI, Typer, React and pywebview are delivery mechanisms. If a product decision is encoded inside a route, CLI command or React component, it belongs in a core service instead.&lt;/p&gt;

&lt;p&gt;The file also gives the agent a layer-by-layer recipe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the new core domain.&lt;/li&gt;
&lt;li&gt;Add ports and services.&lt;/li&gt;
&lt;li&gt;Test the core with in-memory fakes.&lt;/li&gt;
&lt;li&gt;Add persistence.&lt;/li&gt;
&lt;li&gt;Add API routes.&lt;/li&gt;
&lt;li&gt;Regenerate the typed client.&lt;/li&gt;
&lt;li&gt;Replace the frontend.&lt;/li&gt;
&lt;li&gt;Replace CLI commands.&lt;/li&gt;
&lt;li&gt;Rework plugins.&lt;/li&gt;
&lt;li&gt;Delete every &lt;code&gt;[demo]&lt;/code&gt; marker.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the important bit: the agent is not guessing the architecture. It is following the project's own instruction manual.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: build the core first
&lt;/h2&gt;

&lt;p&gt;Tally's product rules live in the core package:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Client&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TimeEntry&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TimerService&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReportService&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The central rule is simple but important: &lt;strong&gt;only one timer can run at a time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That rule is enforced in the core, so the web UI, CLI and desktop app cannot disagree. The CLI does not need its own timer logic. The React app does not need its own timer logic. The API route does not need its own timer logic. They all call the same service.&lt;/p&gt;

&lt;p&gt;Core tests run against in-memory fakes, so the most important behavior can be tested before touching FastAPI, SQLModel, React or pywebview.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: add persistence and API
&lt;/h2&gt;

&lt;p&gt;After the core exists, the backend becomes an adapter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQLModel rows&lt;/li&gt;
&lt;li&gt;Repositories implementing the core ports&lt;/li&gt;
&lt;li&gt;Alembic migrations&lt;/li&gt;
&lt;li&gt;FastAPI routes over the services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The paid CSV export is just a route gate:&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="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/export&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dependencies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;export_report_csv&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests cover both sides of the gate: the dev license stub grants &lt;code&gt;pro&lt;/code&gt;, while a free plan returns a structured 403.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: regenerate the typed frontend client
&lt;/h2&gt;

&lt;p&gt;After changing the API, the agent regenerates the frontend schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run tally gen
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The React UI uses types derived from the backend OpenAPI schema. If an endpoint changes and the UI call site is wrong, TypeScript catches it.&lt;/p&gt;

&lt;p&gt;Tally's frontend is intentionally modest: a timer view, a report view, plugin settings and license status. The point is not to be flashy. The point is that the product can move from generated scaffold to working application without breaking the architectural line.&lt;/p&gt;

&lt;p&gt;The plugin manager is still there too, now reworked for the Tally domain:&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%2Fcz8dglrlx1uuf93hafgo.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%2Fcz8dglrlx1uuf93hafgo.png" alt="Tally plugins screenshot" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: make the CLI a real adapter
&lt;/h2&gt;

&lt;p&gt;The CLI is not a second implementation. It is another adapter over the same services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ tally client add "Acme Corp" --rate 100
$ tally start "Acme Corp" "api integration"
Timer started for Acme Corp.  api integration

$ tally status
Running: Acme Corp  0:03  api integration

$ tally stop
Stopped: Acme Corp  0:09

$ tally report --week
Client                      Hours     Rate     Amount
Acme Corp                    0.00   100.00       0.25
TOTAL                        0.00                0.25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one-running-timer rule is enforced here too, because it comes from &lt;code&gt;TimerService&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: delete the demo
&lt;/h2&gt;

&lt;p&gt;This is where the &lt;code&gt;[demo]&lt;/code&gt; markers pay off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rg &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="s2"&gt;demo&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; packages apps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent can find the entire old demo surface, delete pure-demo files, trim marked sections from mixed files, regenerate the typed client and keep the project clean.&lt;/p&gt;

&lt;p&gt;After the purge, Tally had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;45 tests&lt;/li&gt;
&lt;li&gt;Zero &lt;code&gt;[demo]&lt;/code&gt; markers&lt;/li&gt;
&lt;li&gt;No dead Resource Vault scaffold&lt;/li&gt;
&lt;li&gt;Web, CLI and desktop still sharing one core&lt;/li&gt;
&lt;/ul&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%2Fr5bieq7viemqjgwefcce.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%2Fr5bieq7viemqjgwefcce.png" alt="OpenProductKit rework flow infographic" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The desktop app required no product work
&lt;/h2&gt;

&lt;p&gt;This was the nicest validation.&lt;/p&gt;

&lt;p&gt;Tally's desktop app did not need a separate rewrite. The pywebview shell already knows how to load the same frontend and dispatch requests into the FastAPI app in-process. No sidecar. No local port. No second backend.&lt;/p&gt;

&lt;p&gt;Once the web UI and API were reworked, the desktop app came along for free:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run tally build web
uv run tally desktop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is what the "core never assumed HTTP" rule buys you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dogfooding caught a real bug
&lt;/h2&gt;

&lt;p&gt;Full honesty: the first CI run of the generated project failed.&lt;/p&gt;

&lt;p&gt;The generated workflow file is copied verbatim because GitHub Actions syntax uses &lt;code&gt;${{ }}&lt;/code&gt;, which collides with Jinja templating. One desktop smoke-test step still contained an unrendered &lt;code&gt;{{ pkg_slug }}_desktop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Tally was the first generated project to run its own CI. It caught that bug quickly, and the fix went back upstream.&lt;/p&gt;

&lt;p&gt;That is exactly why I wanted a real proof project instead of a polished demo video. Building something from your own template finds the bugs that documentation misses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it with your own idea
&lt;/h2&gt;

&lt;p&gt;The workflow Tally validated is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. uvx copier copy gh:ravipurohit1991/OpenProductKit my-product
2. Open it with your coding agent:
   "Read AGENTS.md, then replace the demo with &amp;lt;my idea&amp;gt;."
3. Review the diffs layer by layer.
4. Ship.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent does not guess the architecture. It is told.&lt;/p&gt;

&lt;p&gt;It does not hunt for the demo. It greps the fence.&lt;/p&gt;

&lt;p&gt;It does not hand-write client types. The recipe tells it when to regenerate.&lt;/p&gt;

&lt;p&gt;And you review commits shaped like product layers instead of one giant rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is not
&lt;/h2&gt;

&lt;p&gt;OpenProductKit is intentionally lean and opinionated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQLite, SQLModel and Alembic&lt;/li&gt;
&lt;li&gt;React and Vite&lt;/li&gt;
&lt;li&gt;Typer&lt;/li&gt;
&lt;li&gt;pywebview and PyInstaller&lt;/li&gt;
&lt;li&gt;Dev-time Python entry-point plugins&lt;/li&gt;
&lt;li&gt;Local/offline licensing primitives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not a multi-tenant SaaS starter with Stripe, teams, billing portals and Kubernetes. Runtime plugin installation is still on the roadmap. Code signing is documented, not solved.&lt;/p&gt;

&lt;p&gt;But if your idea is a product that a customer downloads or logs into, with a license key, plugins, and web + CLI + desktop surfaces, this is the fastest honest path from template to shipped that I know how to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Template: &lt;a href="https://github.com/ravipurohit1991/OpenProductKit" rel="noopener noreferrer"&gt;github.com/ravipurohit1991/OpenProductKit&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Proof project: &lt;a href="https://github.com/ravipurohit1991/tally-time-tracker" rel="noopener noreferrer"&gt;github.com/ravipurohit1991/tally-time-tracker&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs article: &lt;a href="https://github.com/ravipurohit1991/OpenProductKit/blob/main/docs/blog/introducing-openproductkit.md" rel="noopener noreferrer"&gt;OpenProductKit blog draft&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both projects are MIT licensed.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>python</category>
      <category>react</category>
    </item>
  </channel>
</rss>
