<?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: Ibrahim Edhem Harbutlu</title>
    <description>The latest articles on DEV Community by Ibrahim Edhem Harbutlu (@ibrh96prog).</description>
    <link>https://dev.to/ibrh96prog</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%2F3961417%2F958c8b4b-8640-4f8a-bc6a-b50b05262fb9.png</url>
      <title>DEV Community: Ibrahim Edhem Harbutlu</title>
      <link>https://dev.to/ibrh96prog</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ibrh96prog"/>
    <language>en</language>
    <item>
      <title>I'm not a software engineer, but I shipped an Obsidian plugin with zero servers and offline licensing</title>
      <dc:creator>Ibrahim Edhem Harbutlu</dc:creator>
      <pubDate>Fri, 19 Jun 2026 18:36:32 +0000</pubDate>
      <link>https://dev.to/ibrh96prog/im-not-a-software-engineer-but-i-shipped-an-obsidian-plugin-with-zero-servers-and-offline-4i14</link>
      <guid>https://dev.to/ibrh96prog/im-not-a-software-engineer-but-i-shipped-an-obsidian-plugin-with-zero-servers-and-offline-4i14</guid>
      <description>&lt;p&gt;I am an engineer by training, but not a software engineer. I have no coding background. Over the past months I have been building and shipping small commercial tools entirely with AI assistance, and the latest one is an Obsidian plugin. I want to write up the decisions that went into it, because a few of them go against how most paid AI plugins are built.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I was solving for
&lt;/h2&gt;

&lt;p&gt;I kept seeing the same complaint across reading and PKM communities: people sync hundreds of highlights from Readwise, Snipd, or Kindle into their vault, and then never open the folder again. The highlights pile up into a graveyard. The effort of saving them is spent, but nothing comes back out.&lt;/p&gt;

&lt;p&gt;I am not a heavy highlighter myself. I found this pain through research, not from my own giant pile of notes. That distinction matters to me, so I am stating it plainly rather than inventing a personal origin story.&lt;/p&gt;

&lt;p&gt;The plugin reads the whole folder and writes one synthesis note: a summary, the key claims, and a topic list per source, then the themes that recur across more than one source, with where sources agree and where they pull apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: no server, at all
&lt;/h2&gt;

&lt;p&gt;Most paid AI Obsidian plugins I looked at validate a subscription against a backend. A typical one runs a Cloudflare Worker, checks your license against the cloud, and bills monthly. That is a reasonable business model, but it means a server I have to run, a database, a recurring cost, and a place where user data could flow.&lt;/p&gt;

&lt;p&gt;I went the other way: zero server, zero database, zero recurring cost. The plugin runs entirely on the user's device. There is nothing for me to operate and nothing to leak on my side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: bring your own key (BYOK)
&lt;/h2&gt;

&lt;p&gt;The plugin uses the user's own API key for whichever provider they pick (Anthropic, OpenAI, OpenRouter, or any OpenAI-compatible endpoint). I never see their key and I never proxy their requests.&lt;/p&gt;

&lt;p&gt;One honesty point I want to be clear about, because it is easy to oversell: BYOK does not mean the data stays on the device. To synthesize highlights, the plugin sends the highlight text to whatever model provider the user configured. So the accurate privacy claim is "no server, no account, no backend data collection on my side", not "your notes never leave your machine". I refuse to make the second claim because it is not true for any BYOK tool that calls a hosted model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: offline license verification with Ed25519
&lt;/h2&gt;

&lt;p&gt;If there is no server, how do you sell a Pro license? I used offline signature verification. Each plugin has its own Ed25519 keypair. The private key lives only in my password manager. The public key is embedded in the plugin. A license key is a small signed payload: product id, email, issue date, plus a detached signature.&lt;/p&gt;

&lt;p&gt;Verification is fully offline using tweetnacl:&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;import&lt;/span&gt; &lt;span class="nx"&gt;nacl&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tweetnacl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// publicKey embedded in the plugin, payloadBytes + signature decoded from the license key&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nacl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payloadBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it verifies and the product id matches, Pro is active. No network call, no phone-home, works on a plane. A note for anyone trying this: &lt;a class="mentioned-user" href="https://dev.to/noble"&gt;@noble&lt;/a&gt;/ed25519 did not work in Obsidian's Electron environment for me, tweetnacl did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free tier is a lifetime cap, not a monthly reset
&lt;/h2&gt;

&lt;p&gt;The free tier allows 3 syncs total, for the lifetime of the install, not 3 per month. A monthly reset lets people wait out the counter and never convert. A lifetime cap creates a real decision point. Generating the report from already-synced highlights stays free forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  On building this without being a software engineer
&lt;/h2&gt;

&lt;p&gt;The whole thing is TypeScript, and I wrote essentially none of it by hand. Claude wrote the code, I made the product and architecture decisions, tested every step in Obsidian, and pushed back when something was wrong. The parsing is deliberately defensive because highlight exports vary wildly between people's templates: callout blocks, heading-plus-bullet, block ids, or plain text all have to be read, with a whole-body fallback when no markers are found.&lt;/p&gt;

&lt;p&gt;It is in the Obsidian community plugin directory now. If you have a highlight backlog like this, I would genuinely like to hear how you currently deal with it, by hand, with a Dataview query, or not at all.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/ibrh96-prog/obsidian-highlights-synthesizer" rel="noopener noreferrer"&gt;https://github.com/ibrh96-prog/obsidian-highlights-synthesizer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Obsidian directory: &lt;a href="https://community.obsidian.md/plugins/highlight-inbox-synthesizer" rel="noopener noreferrer"&gt;https://community.obsidian.md/plugins/highlight-inbox-synthesizer&lt;/a&gt;&lt;/p&gt;

</description>
      <category>obsidian</category>
      <category>typescript</category>
      <category>ai</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>I kept losing leads because I was too slow to check my inbox. So I automated it.</title>
      <dc:creator>Ibrahim Edhem Harbutlu</dc:creator>
      <pubDate>Wed, 17 Jun 2026 21:16:59 +0000</pubDate>
      <link>https://dev.to/ibrh96prog/i-kept-losing-leads-because-i-was-too-slow-to-check-my-inbox-so-i-automated-it-2ekl</link>
      <guid>https://dev.to/ibrh96prog/i-kept-losing-leads-because-i-was-too-slow-to-check-my-inbox-so-i-automated-it-2ekl</guid>
      <description>&lt;p&gt;For the last few months I was running everything through a basic contact form on a side project. Every time someone filled it out, I had to remember to check my email, copy their info into a spreadsheet, and reply manually. Half the time I forgot. A couple of leads went cold because I noticed the email two days late, which is annoying when you're the one who built the form in the first place.&lt;/p&gt;

&lt;p&gt;So I built an n8n workflow to handle it instead of relying on my own memory.&lt;/p&gt;

&lt;p&gt;What it actually does&lt;/p&gt;

&lt;p&gt;Seven nodes, end to end:&lt;/p&gt;

&lt;p&gt;A webhook catches the form submission&lt;br&gt;
A Set node normalizes the fields (name, email, message, date) regardless of which form tool sent them&lt;br&gt;
An IF node checks the email is present and contains an @; anything that fails gets dropped silently, so bot spam never touches my spreadsheet&lt;br&gt;
Google Sheets appends the lead as a new row&lt;br&gt;
Gmail sends me an internal notification&lt;br&gt;
A second Gmail node sends the person an automatic "we got your message" reply&lt;br&gt;
A NoOp node closes out the run&lt;/p&gt;

&lt;p&gt;The whole thing fires the second someone hits submit. No checking inboxes, no copy-paste, no two-day delay.&lt;/p&gt;

&lt;p&gt;The two things that actually took effort&lt;/p&gt;

&lt;p&gt;Field name mapping. Every form tool sends data slightly differently. Tally, Jotform, Typeform, a raw HTML form, sometimes the fields show up nested under body, sometimes flat. I put all the mapping in one Set node early in the chain. If I switch form tools later I only update expressions in that one node instead of hunting through the whole thing.&lt;/p&gt;

&lt;p&gt;Pulling data by node name instead of position. Both Gmail nodes reference the Set node directly, something like {{ $('Set').item.json.email }} rather than {{ $json.email }}. That way if I reorder nodes later, or stick something new in the middle, the emails don't quietly break because they were reading from whatever happened to run right before them.&lt;/p&gt;

&lt;p&gt;The spam filter is also worth calling out. It's nothing clever, just "is the email field non-empty and does it contain an @ sign," but it cut a noticeable amount of junk before it ever touched my sheet.&lt;/p&gt;

&lt;p&gt;Why I'm posting this here&lt;/p&gt;

&lt;p&gt;A couple of people asked how I built it, so I cleaned it up, wrote a proper install guide, and packaged the whole thing as a template. I also just wanted to see whether a personal automation could hold up as an actual product, separate from the curiosity.&lt;/p&gt;

&lt;p&gt;If you're dealing with the same problem, form submissions you have to manually track, the template is on Gumroad: &lt;a href="https://ibrh96.gumroad.com/l/gqvnho" rel="noopener noreferrer"&gt;https://ibrh96.gumroad.com/l/gqvnho&lt;/a&gt;. Credentials ship empty, you connect your own Google account, and it runs on n8n Cloud or self-hosted.&lt;/p&gt;

&lt;p&gt;If you've built something similar, I'm curious how you handled spam filtering. Mine is intentionally basic (email present, contains an @, that's it) and I keep wondering if I'm missing an obvious next step.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>productivity</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Cloudflare Pages + Railway + Neon: deploying a self-hosted app for $0</title>
      <dc:creator>Ibrahim Edhem Harbutlu</dc:creator>
      <pubDate>Sat, 06 Jun 2026 10:42:22 +0000</pubDate>
      <link>https://dev.to/ibrh96prog/cloudflare-pages-railway-neon-deploying-a-self-hosted-app-for-0-5ac2</link>
      <guid>https://dev.to/ibrh96prog/cloudflare-pages-railway-neon-deploying-a-self-hosted-app-for-0-5ac2</guid>
      <description>&lt;h1&gt;
  
  
  Cloudflare Pages + Railway + Neon: deploying a self-hosted app for $0
&lt;/h1&gt;

&lt;p&gt;No software background. Built a license key manager called KeyMint with Cursor, deployed it across three free-tier services, and hit five errors I didn't see coming. Here's what they were.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;Frontend on Cloudflare Pages, API on Railway, database on Neon. All free. React 19 + Vite on the frontend, Express 5 + TypeScript on the backend, standard PostgreSQL through Drizzle ORM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error 1: Railway free tier limit
&lt;/h2&gt;

&lt;p&gt;Went to create a new Railway project for KeyMint. Railway said no — already at the limit with two existing projects.&lt;/p&gt;

&lt;p&gt;Fix: don't create a new project. Add KeyMint as a new &lt;em&gt;service&lt;/em&gt; inside the existing project. Railway lets you run multiple services under one project. I had just assumed each app needed its own. Two minutes to figure out, zero code changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error 2: Supabase was full too
&lt;/h2&gt;

&lt;p&gt;Free tier on Supabase is two projects. Both slots were taken.&lt;/p&gt;

&lt;p&gt;I switched to Neon instead of deleting something. Same free-tier PostgreSQL, same &lt;code&gt;pg&lt;/code&gt; driver, different connection string. Swapped the DATABASE_URL in Railway's env vars and moved on. No code changes.&lt;/p&gt;

&lt;p&gt;If you're on free tiers, don't get attached to one database host. They're interchangeable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error 3: pg in the wrong package.json
&lt;/h2&gt;

&lt;p&gt;API server deployed and immediately crashed on boot. Generic module resolution error, not helpful.&lt;/p&gt;

&lt;p&gt;The actual problem: &lt;code&gt;pg&lt;/code&gt; was a dependency of the shared &lt;code&gt;lib/db&lt;/code&gt; package, not of &lt;code&gt;@keymint/api-server&lt;/code&gt; directly. esbuild bundled the api-server, marked &lt;code&gt;pg&lt;/code&gt; as external because it wasn't in that package's own dependencies, and at runtime Node couldn't find it.&lt;/p&gt;

&lt;p&gt;Fix — add &lt;code&gt;pg&lt;/code&gt; directly to the api-server's package.json:&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;"dependencies"&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;"pg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^8.11.0"&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;Monorepo gotcha. esbuild doesn't crawl the whole workspace to figure out what you need at runtime. If a dep lives in a sibling package, it gets left out of the bundle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error 4: Wrong Cloudflare product
&lt;/h2&gt;

&lt;p&gt;Cloudflare has Pages and Workers. They look similar in the dashboard. They are not the same thing.&lt;/p&gt;

&lt;p&gt;I ended up in Workers first. Workers wants &lt;code&gt;wrangler deploy&lt;/code&gt;, a config file, edge functions. That's not what I needed — I just wanted to host a Vite build.&lt;/p&gt;

&lt;p&gt;Pages is the right product. Connect GitHub, set build command, set output directory, done.&lt;/p&gt;

&lt;p&gt;Settings that worked for a pnpm monorepo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build command: &lt;code&gt;pnpm install &amp;amp;&amp;amp; pnpm --filter @keymint/web build&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Output directory: &lt;code&gt;artifacts/keymint/dist&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Environment variable: &lt;code&gt;NODE_VERSION=20&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one matters. Cloudflare Pages defaults to an older Node version. Without setting it to 20, the build fails on syntax it doesn't recognize. Spent 15 minutes on that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error 5: wrangler deploy kept failing
&lt;/h2&gt;

&lt;p&gt;Related to error 4 — while I was in the Workers section, the dashboard was telling me to run &lt;code&gt;wrangler deploy&lt;/code&gt;. It kept failing because I had no &lt;code&gt;wrangler.toml&lt;/code&gt; and wasn't writing a Worker.&lt;/p&gt;

&lt;p&gt;Once I found the Pages section and used the GUI deploy flow instead, it worked.&lt;/p&gt;

&lt;p&gt;This sounds obvious. It wasn't obvious when I was in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  End result
&lt;/h2&gt;

&lt;p&gt;KeyMint is live at &lt;a href="https://keymint-rho.vercel.app" rel="noopener noreferrer"&gt;keymint-rho.vercel.app&lt;/a&gt;. Database on Neon, free across the board.&lt;/p&gt;

&lt;p&gt;Source code on GitHub: &lt;a href="https://github.com/ibrh96-prog/keymint" rel="noopener noreferrer"&gt;github.com/ibrh96-prog/keymint&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Packaged it as a boilerplate if you want to skip the setup: &lt;a href="https://ibrh96.gumroad.com/l/nvkfg" rel="noopener noreferrer"&gt;ibrh96.gumroad.com/l/nvkfg&lt;/a&gt; — $49, one-time, full source.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; I've since moved the whole thing to a single Vercel project — Vite frontend and the Express API as one serverless function, same origin. The Cloudflare Pages + Railway split here still worked, but Railway's free credit runs down and the demo kept going to sleep. The migration had its own set of errors (including a great one where &lt;code&gt;pnpm build:api&lt;/code&gt; died with "command not found" because of a stale root-directory setting). That's a separate post.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built with Cursor. 0 sales so far — testing whether dev.to drives any.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>devops</category>
      <category>cloudflarechallenge</category>
      <category>postgres</category>
    </item>
    <item>
      <title>I built a self-hosted license key manager for my Gumroad products (0 sales so far)</title>
      <dc:creator>Ibrahim Edhem Harbutlu</dc:creator>
      <pubDate>Thu, 04 Jun 2026 23:16:40 +0000</pubDate>
      <link>https://dev.to/ibrh96prog/i-built-a-self-hosted-license-key-manager-for-my-gumroad-products-0-sales-so-far-10di</link>
      <guid>https://dev.to/ibrh96prog/i-built-a-self-hosted-license-key-manager-for-my-gumroad-products-0-sales-so-far-10di</guid>
      <description>&lt;p&gt;I sell boilerplates on Gumroad. Source code, you download a ZIP, you run it. Over the last few months one thought kept nagging me: if I ever wanted to actually license one of these products, or sell a paid tool that checks a key when it runs, I would have to sign up for some license-key SaaS and pay monthly for the privilege. The cheap ones start around 20 a month. For a side project that makes nothing yet, that math is bad.&lt;br&gt;
So I built the thing instead. It's called KeyMint, and it's a license key manager you host yourself.&lt;br&gt;
Demo (with seeded data so it doesn't look like an empty shell): &lt;a href="https://keymint.pages.dev" rel="noopener noreferrer"&gt;https://keymint.pages.dev&lt;/a&gt;&lt;br&gt;
What it actually does&lt;br&gt;
You create a product, generate a batch of keys, and hand them out. Then your own software checks a key with a single request:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const res = await fetch("https://your-keymint.app/api/validate", {&lt;br&gt;
  method: "POST",&lt;br&gt;
  headers: { "Content-Type": "application/json" },&lt;br&gt;
  body: JSON.stringify({ key, productSlug: "subsaver", activate: true }),&lt;br&gt;
});&lt;br&gt;
const { valid } = await res.json();&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That valid boolean is the whole point for most integrations. The response also tells you why a key failed (revoked, expired, activation limit hit, wrong product) and how many activations are left, so you can do seat limits or device limits without writing that logic yourself. Pass activate: true and the check also burns one activation.&lt;br&gt;
The dashboard handles the boring side: bulk key generation, revoke, expiry dates, and an activity log that records every validation attempt with its result and IP. I added a 30-day trend chart mostly because an empty dashboard is depressing and a populated one sells the idea in two seconds.&lt;br&gt;
The stack, and the part that fought back&lt;br&gt;
React 19 and Vite on the front, Express 5 and Drizzle on the back, Postgres underneath, all in a pnpm monorepo. Nothing exotic.&lt;br&gt;
Deploying it as a broke person was the interesting part. The frontend is static, so it went on Cloudflare Pages for free. The API went on Railway. Then I hit Railway's free-tier limit when I tried to add a second service, and my Supabase free projects were already full from two earlier apps. So the database ended up on Neon, which gives you a real Postgres instance on a free tier that doesn't expire. Three providers, zero dollars a month. The schema and a seed script are in the repo, so spinning up your own copy is a SQL paste and two deploys.&lt;br&gt;
One gotcha worth writing down: I bundle the API with esbuild, and pg has to be listed as a direct dependency of the server package or the deployed bundle can't find it at runtime. It built fine locally and crashed on first boot until I caught that. Exactly the kind of thing that wastes an hour.&lt;br&gt;
Honest status&lt;br&gt;
Zero sales. The product went live today. I have done this enough times now (this is my fourth boilerplate) to know that "published on Gumroad" and "someone bought it" are very far apart, and the gap is distribution, not the product. Writing this post is part of closing that gap, which I am aware is a little circular.&lt;br&gt;
It's 49 dollars, MIT licensed, you get the full source. What it does not do, on purpose: it does not take payments (it manages keys, not money), and the admin dashboard has no auth by default because it's meant to run privately. The README shows the roughly ten lines to gate it if you want to expose it. No bank integrations, no hosted version, no monthly anything.&lt;br&gt;
Link, if you want it: &lt;a href="https://ibrh96.gumroad.com/l/nvkfg" rel="noopener noreferrer"&gt;https://ibrh96.gumroad.com/l/nvkfg&lt;/a&gt;&lt;br&gt;
The other ones&lt;br&gt;
If you're the type who buys boilerplates, these are the other three I've shipped, same stack, same idea of skipping the setup grind:&lt;/p&gt;

&lt;p&gt;SubSaver, a subscription and savings tracker: &lt;a href="https://ibrh96.gumroad.com/l/gytqdv" rel="noopener noreferrer"&gt;https://ibrh96.gumroad.com/l/gytqdv&lt;/a&gt;&lt;br&gt;
FreelanceFlow, a client, project and invoice tracker: &lt;a href="https://ibrh96.gumroad.com/l/bcsufq" rel="noopener noreferrer"&gt;https://ibrh96.gumroad.com/l/bcsufq&lt;/a&gt;&lt;br&gt;
MetricMint, an MRR dashboard for indie hackers: &lt;a href="https://ibrh96.gumroad.com/l/jwdmao" rel="noopener noreferrer"&gt;https://ibrh96.gumroad.com/l/jwdmao&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A real question&lt;br&gt;
Two things I keep going back and forth on, and I'd take actual opinions:&lt;/p&gt;

&lt;p&gt;For a tool like this, does keeping the admin side auth-free by default make it feel unfinished, or is "private by default, gate it yourself" the right call for a starter kit?&lt;br&gt;
If you sell digital products, how do you handle licensing today? Do you bother at all, or just trust people? I genuinely can't tell if I built something useful or solved a problem only I have.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>node</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Finally Shipped MetricMint — My MRR Dashboard for Indie Hackers (Built with GitHub Copilot)</title>
      <dc:creator>Ibrahim Edhem Harbutlu</dc:creator>
      <pubDate>Tue, 02 Jun 2026 19:39:24 +0000</pubDate>
      <link>https://dev.to/ibrh96prog/i-finally-shipped-metricmint-my-mrr-dashboard-for-indie-hackers-built-with-github-copilot-4302</link>
      <guid>https://dev.to/ibrh96prog/i-finally-shipped-metricmint-my-mrr-dashboard-for-indie-hackers-built-with-github-copilot-4302</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-05-21"&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;MetricMint is an MRR dashboard for indie hackers who want to track their revenue without signing up for another SaaS tool.&lt;/p&gt;

&lt;p&gt;The idea had been sitting in a notes file for months. I build and sell web app boilerplates on Gumroad — things like subscription trackers and freelance project managers. And I kept hitting the same wall: I had no clean way to track my own monthly recurring revenue. Every MRR tool I found was either way too big for what I needed, expensive, or required connecting a payment processor before you could see anything useful.&lt;/p&gt;

&lt;p&gt;So I wanted something simple. Paste your numbers in. See your trends. That's it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://metricmint.vercel.app" rel="noopener noreferrer"&gt;https://metricmint.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ibrh96-prog/metricmint" rel="noopener noreferrer"&gt;https://github.com/ibrh96-prog/metricmint&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Features: MRR dashboard with month-over-month growth, manual revenue entry (no bank sync, no Stripe connect), 12-month area chart, churn tracking, dark/light mode, PostgreSQL + Drizzle ORM on the backend, mobile responsive.&lt;/p&gt;

&lt;p&gt;Stack: React 19 + Vite, Tailwind CSS v4, shadcn/ui, Express 5 + TypeScript, Drizzle ORM, Recharts, deployed on Railway with Supabase.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://metricmint.vercel.app" rel="noopener noreferrer"&gt;https://metricmint.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Seeded with realistic data so you can explore everything right away.&lt;/p&gt;

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

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

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

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




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

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

&lt;p&gt;I started MetricMint on a weekend when I got annoyed enough to actually do something about not tracking my revenue. I scaffolded a React frontend, wrote a couple of API routes, and then... stopped. Something came up. The folder sat on my desktop for weeks.&lt;/p&gt;

&lt;p&gt;When the Finish-Up-A-Thon came along I figured this was the one to revive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two broken React components, no routing&lt;/li&gt;
&lt;li&gt;No database schema at all&lt;/li&gt;
&lt;li&gt;One API route — a health check&lt;/li&gt;
&lt;li&gt;No dark mode&lt;/li&gt;
&lt;li&gt;No way to deploy it&lt;/li&gt;
&lt;li&gt;No seed data, so the demo looked completely empty&lt;/li&gt;
&lt;li&gt;No README&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I committed to finishing it properly. Not just getting it to run, but shipping it to a standard where I could actually sell it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full MRR dashboard with 5 metrics&lt;/li&gt;
&lt;li&gt;PostgreSQL with Drizzle ORM, pushed to Supabase&lt;/li&gt;
&lt;li&gt;8 REST endpoints, OpenAPI documented&lt;/li&gt;
&lt;li&gt;12-month area chart&lt;/li&gt;
&lt;li&gt;Dark / light mode&lt;/li&gt;
&lt;li&gt;Mobile responsive&lt;/li&gt;
&lt;li&gt;Railway deployment (API and frontend as separate services)&lt;/li&gt;
&lt;li&gt;Seed data SQL so the demo looks like a real product in use&lt;/li&gt;
&lt;li&gt;Full README with setup instructions and schema docs&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;I should be upfront about one thing: I'm not a software developer. No CS background, no formal training. I build web apps with AI assistance and sell the results as boilerplates on Gumroad. So I wasn't using Copilot to speed up code I already understood. I was using it to build things I genuinely couldn't have built alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The database layer&lt;/strong&gt; was the clearest example. I described the schema in plain English and Copilot generated the Drizzle ORM schema, migration logic, and Zod validation schemas. I didn't write any of that by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The API routes&lt;/strong&gt; went the same way. I described what each endpoint should do and Copilot wrote the Express handlers, validation, and error handling. It also caught a bug I'd never have noticed on my own: the monthly trend aggregation was grouping entries incorrectly for records near midnight UTC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The monorepo wiring&lt;/strong&gt; was the hardest part. MetricMint uses pnpm workspaces with shared packages across the DB client, API spec, and generated React Query hooks. Getting those cross-package TypeScript imports to resolve in a Railway production build is genuinely tricky. Copilot figured out the problem (tsc not resolving workspace paths at build time) and suggested switching to esbuild with explicit bundle flags. Three minutes. I think that would have taken me a full day otherwise, if I figured it out at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I had to manage myself:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scope. Copilot kept suggesting things I didn't ask for — authentication, Stripe integration, CSV export. I had to be explicit every time: no auth, no payments, keep it simple.&lt;/p&gt;

&lt;p&gt;Stack consistency. When I wasn't precise upfront, Copilot would sometimes drift toward Prisma instead of Drizzle, or axios instead of React Query. I learned to front-load the full stack spec at the start of every prompt.&lt;/p&gt;

&lt;p&gt;The seed data also needed a second pass. The first version had round numbers and perfectly linear growth — it looked fake. I asked Copilot to add realistic variance: a couple of dips, one rough month, uneven growth. The demo is a lot more believable now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MetricMint went from a dead folder on my desktop to a live, deployed product in one session. That's what actually happened. I don't have a better way to describe it than that.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Update (June 2026):&lt;/strong&gt; I've since moved MetricMint off Railway to a single Vercel project — Vite frontend and the Express API as one serverless function on the same origin. Railway's free credit runs down over time and the demo kept going to sleep, so the buyer's click sometimes landed on a cold or dead app. On Vercel the demo stays up. The DB is on Supabase's pooler, so I kept postgres.js (made serverless-safe with prepare:false and max:1) rather than swapping to a Neon-only driver. New live URL above.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
    </item>
    <item>
      <title>I built a freelance client + invoice tracker in ~3 hours using Cursor — here's everything I shipped</title>
      <dc:creator>Ibrahim Edhem Harbutlu</dc:creator>
      <pubDate>Sun, 31 May 2026 17:10:09 +0000</pubDate>
      <link>https://dev.to/ibrh96prog/i-built-a-freelance-client-invoice-tracker-in-3-hours-using-cursor-heres-everything-i-shipped-36b6</link>
      <guid>https://dev.to/ibrh96prog/i-built-a-freelance-client-invoice-tracker-in-3-hours-using-cursor-heres-everything-i-shipped-36b6</guid>
      <description>&lt;p&gt;Three hours. That's roughly how long it took me to go from blank screen to a &lt;br&gt;
working full-stack app with a real database.&lt;/p&gt;

&lt;p&gt;I'm not a developer by background. I use Cursor as my main build tool and &lt;br&gt;
Supabase for the database. No local PostgreSQL setup, no fighting with configs — &lt;br&gt;
just a Transaction Pooler connection string and a prompt.&lt;/p&gt;

&lt;p&gt;Here's what came out: &lt;strong&gt;FreelanceFlow&lt;/strong&gt;, a client and project tracker built &lt;br&gt;
specifically for freelancers who are tired of juggling invoices in spreadsheets.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Client management — add clients, track contact info, see all their projects 
in one place&lt;/li&gt;
&lt;li&gt;Project tracking — status (active, on hold, completed), deadlines, linked 
to a client&lt;/li&gt;
&lt;li&gt;Invoice generation — create invoices per project, mark them paid/unpaid, 
track totals&lt;/li&gt;
&lt;li&gt;Dashboard — monthly revenue, outstanding amount, active project count, 
recent activity&lt;/li&gt;
&lt;li&gt;12-month revenue chart built with Recharts&lt;/li&gt;
&lt;li&gt;Dark/light mode, mobile responsive down to 375px&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No bank sync. No Stripe. No OAuth. You install it, you own it, you run it &lt;br&gt;
yourself. That was intentional — the simpler the setup, the more useful it &lt;br&gt;
actually is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;React 19 + Vite, Express 5, TypeScript, PostgreSQL via Drizzle ORM, &lt;br&gt;
Tailwind CSS v4 + shadcn/ui, Framer Motion, Recharts. pnpm workspaces &lt;br&gt;
monorepo. The full API contract is defined in OpenAPI 3.1 and Recharts &lt;br&gt;
handles the charts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Cursor handled
&lt;/h2&gt;

&lt;p&gt;Honestly, most of it. The entire monorepo structure, all the API routes, &lt;br&gt;
the Drizzle schema, the React Query hooks, the dashboard layout. I wrote &lt;br&gt;
the initial prompt, reviewed the output, fixed two routing bugs, and &lt;br&gt;
that was it.&lt;/p&gt;

&lt;p&gt;The part I still had to think through: the invoice status logic and making &lt;br&gt;
sure the revenue chart was pulling from real data, not placeholder seed data. &lt;br&gt;
Those took an extra 20-30 minutes of iteration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm doing with it
&lt;/h2&gt;

&lt;p&gt;I packaged it and put it on Gumroad for $49. The pitch is simple: if you're &lt;br&gt;
a freelancer or developer who wants a working starting point instead of &lt;br&gt;
spending 2-3 days on boilerplate, this is it.&lt;/p&gt;

&lt;p&gt;Live demo: &lt;a href="https://freelanceflow-flame-mu.vercel.app" rel="noopener noreferrer"&gt;https://freelanceflow-flame-mu.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gumroad: &lt;a href="https://ibrh96.gumroad.com/l/bcsufq" rel="noopener noreferrer"&gt;https://ibrh96.gumroad.com/l/bcsufq&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest take
&lt;/h2&gt;

&lt;p&gt;Zero sales so far. I posted on Indie Hackers and got useful feedback — &lt;br&gt;
the main note was that $39 was too cheap and the channel I was using &lt;br&gt;
(IH) is better for feedback than for actual conversions. Both turned out &lt;br&gt;
to be true. Price is now $49. Writing this post is me testing a different &lt;br&gt;
channel.&lt;/p&gt;

&lt;p&gt;I don't know if this will sell. But I do know the app works end-to-end, &lt;br&gt;
the demo is populated with real data, and the code is clean enough that &lt;br&gt;
a developer could extend it in an afternoon.&lt;/p&gt;

&lt;p&gt;If you've built something similar or sold boilerplates before — what &lt;br&gt;
actually drove your first sale?&lt;/p&gt;




&lt;p&gt;Update (June 2026): I've moved the demo off Railway to a single Vercel project — Vite frontend and the Express API as one serverless function, same origin. Railway's free credit runs down and the demo kept going to sleep, so a click would sometimes land on a cold app. On Vercel it stays up. The DB is on Supabase's pooler, so I kept postgres.js made serverless-safe (prepare:false, max:1). New live URL above.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
