<?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>Forem: Perufitlife</title>
    <description>The latest articles on Forem by Perufitlife (@perufitlife).</description>
    <link>https://forem.com/perufitlife</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3897417%2F545b848b-bfb9-4725-95f7-29d6af2e1bc7.png</url>
      <title>Forem: Perufitlife</title>
      <link>https://forem.com/perufitlife</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/perufitlife"/>
    <language>en</language>
    <item>
      <title>10 free security scanners for the most popular BaaS platforms (2026 edition)</title>
      <dc:creator>Perufitlife</dc:creator>
      <pubDate>Mon, 18 May 2026 07:20:52 +0000</pubDate>
      <link>https://forem.com/perufitlife/10-free-security-scanners-for-the-most-popular-baas-platforms-2026-edition-51lh</link>
      <guid>https://forem.com/perufitlife/10-free-security-scanners-for-the-most-popular-baas-platforms-2026-edition-51lh</guid>
      <description>&lt;h2&gt;
  
  
  10 free security scanners for the most popular BaaS platforms (2026 edition)
&lt;/h2&gt;

&lt;p&gt;If you're shipping on Supabase, Firebase, Strapi, Directus, Payload CMS, Convex, Hasura, PocketBase, Appwrite, or Nhost — &lt;strong&gt;you've already trusted your platform to keep customer data private&lt;/strong&gt;. The fine print is that the platform only enforces the access controls &lt;em&gt;you&lt;/em&gt; configured. Forget one row-level rule, one role permission, one access function — and the platform happily serves your users' data to anyone with your public URL.&lt;/p&gt;

&lt;p&gt;Across 100+ projects I've audited in the last 12 months:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;22% of Supabase&lt;/strong&gt; projects leak data anonymously through forgotten RLS policies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;23% of Firebase&lt;/strong&gt; projects have &lt;code&gt;firestore.rules&lt;/code&gt; with &lt;code&gt;if true&lt;/code&gt; or &lt;code&gt;request.auth != null&lt;/code&gt; without ownership check&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strapi templates&lt;/strong&gt; ship with Public-role &lt;code&gt;find&lt;/code&gt; enabled on &lt;code&gt;users-permissions/users&lt;/code&gt; — exposes every signed-up user&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Directus&lt;/strong&gt; with default Public-role &lt;code&gt;read&lt;/code&gt; on &lt;code&gt;directus_users&lt;/code&gt; leaks hashed passwords + tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WordPress&lt;/strong&gt; (not BaaS but worth mentioning) exposes &lt;code&gt;/wp-json/wp/v2/users&lt;/code&gt; to anonymous callers by default&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix in every case takes 5-30 minutes once you know what's exposed. The hard part is &lt;strong&gt;finding out&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Below are 10 free scanners — one per platform — that probe your project for the most common anonymous-readable patterns and return a verbatim &lt;code&gt;curl&lt;/code&gt; an attacker would run + the exact code/admin steps to fix each finding. All run on the Apify free tier (no credit card needed).&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;a href="https://apify.com/renzomacar/supabase-rls-scanner" rel="noopener noreferrer"&gt;Supabase RLS Scanner&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Probes ~47 common table names via &lt;code&gt;Prefer: count=exact&lt;/code&gt; + &lt;code&gt;Range: 0-0&lt;/code&gt; — confirms which tables are anon-readable without ever pulling row data. Returns severity-coded findings (CRITICAL for &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;orders&lt;/code&gt;, &lt;code&gt;sessions&lt;/code&gt;; HIGH for &lt;code&gt;posts&lt;/code&gt;, &lt;code&gt;messages&lt;/code&gt;). Includes a &lt;strong&gt;demo mode&lt;/strong&gt; (click Run with no input) that scans a real sacrificial Supabase project I maintain so you can see what the report looks like before pasting your own URL + anon key.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;a href="https://apify.com/renzomacar/firebase-security-auditor" rel="noopener noreferrer"&gt;Firebase Security Auditor&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Two-mode probe: provide either &lt;code&gt;projectId&lt;/code&gt; (sends anonymous GET to your Firestore REST endpoint to confirm live leaks) or &lt;code&gt;rulesContent&lt;/code&gt; (paste your &lt;code&gt;firestore.rules&lt;/code&gt; for static analysis catching the 7 most common bad patterns: bare &lt;code&gt;if true&lt;/code&gt;, &lt;code&gt;if request.auth != null&lt;/code&gt; without ownership, test-mode timestamps, etc.).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;a href="https://apify.com/renzomacar/strapi-security-scanner" rel="noopener noreferrer"&gt;Strapi Security Scanner&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Tries &lt;code&gt;/api/{collection}?pagination[limit]=1&lt;/code&gt; (Strapi v4+) and &lt;code&gt;/{collection}?_limit=1&lt;/code&gt; (Strapi v3) per content-type. Default Strapi templates ship with Public-role &lt;code&gt;find&lt;/code&gt; enabled on &lt;code&gt;users-permissions/users&lt;/code&gt; — first thing it catches.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;a href="https://apify.com/renzomacar/directus-security-scanner" rel="noopener noreferrer"&gt;Directus Security Scanner&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Sends &lt;code&gt;/items/{collection}?limit=1&amp;amp;meta=total_count&lt;/code&gt; per collection. The two killer findings: &lt;code&gt;directus_users&lt;/code&gt; (hashed passwords + tokens) and &lt;code&gt;directus_files&lt;/code&gt; (file metadata + signed download URLs).&lt;/p&gt;

&lt;h3&gt;
  
  
  5. &lt;a href="https://apify.com/renzomacar/payload-security-scanner" rel="noopener noreferrer"&gt;Payload CMS Security Scanner&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Tries &lt;code&gt;/api/{collection}?limit=1&lt;/code&gt; per slug. Default templates use &lt;code&gt;access: { read: () =&amp;gt; true }&lt;/code&gt; on most collections — fine for blog posts, fatal for users/orders/media. Report ships with the exact &lt;code&gt;access.read&lt;/code&gt; function rewrite per leaky collection.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. &lt;a href="https://apify.com/renzomacar/convex-security-scanner" rel="noopener noreferrer"&gt;Convex Security Scanner&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;POSTs &lt;code&gt;{path: "users:list", args: {}}&lt;/code&gt; to your deployment's &lt;code&gt;/api/query&lt;/code&gt; endpoint for ~30 common function paths. Convex queries are public by default unless you explicitly call &lt;code&gt;getAuthUserId(ctx)&lt;/code&gt; inside the handler.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. &lt;a href="https://apify.com/renzomacar/hasura-security-scanner" rel="noopener noreferrer"&gt;Hasura Security Scanner&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;GraphQL &lt;code&gt;_aggregate { count }&lt;/code&gt; + sample queries against your Hasura endpoint (self-hosted, Hasura Cloud, or any framework on top). The &lt;code&gt;anon&lt;/code&gt; role typically inherits SELECT permissions from copy-pasted tutorial examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. &lt;a href="https://apify.com/renzomacar/pocketbase-security-scanner" rel="noopener noreferrer"&gt;PocketBase Security Scanner&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;GET /api/collections/{name}/records?perPage=1&lt;/code&gt; per collection. PocketBase's API rules look strict on paper, but &lt;code&gt;@request.auth.id != ""&lt;/code&gt; only requires "any signed-up user" — which in practice means anyone after a self-serve signup.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. &lt;a href="https://apify.com/renzomacar/appwrite-security-auditor" rel="noopener noreferrer"&gt;Appwrite Security Auditor&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Sends &lt;code&gt;/v1/databases/{db}/collections/{c}/documents?queries[]=limit(1)&lt;/code&gt; with &lt;code&gt;X-Appwrite-Project: &amp;lt;id&amp;gt;&lt;/code&gt; header. The &lt;code&gt;any&lt;/code&gt; role on &lt;code&gt;read&lt;/code&gt; or &lt;code&gt;list&lt;/code&gt; exposes every document.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. &lt;a href="https://apify.com/renzomacar/nhost-security-scanner" rel="noopener noreferrer"&gt;Nhost Security Scanner&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;GraphQL probe against your Nhost project's Hasura endpoint. Specifically targets the &lt;code&gt;anon&lt;/code&gt; role permissions Nhost provisions by default — looks for SELECT permissions inherited from Hasura's permissions-tutorial-fixture starter.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to use the demo modes
&lt;/h3&gt;

&lt;p&gt;Every scanner above ships with a &lt;strong&gt;demo mode&lt;/strong&gt; — click Run with no input, and you'll get back a sample HTML report (Supabase scanner runs a real scan against a sacrificial project I maintain with intentional leaks). Use this to see what a real report looks like before deciding whether to paste your own credentials.&lt;/p&gt;

&lt;h3&gt;
  
  
  What if you find leaks?
&lt;/h3&gt;

&lt;p&gt;Three options, in order of effort:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt;: Each scanner's HTML report includes paste-ready fix snippets. Drop them into your config/migrations and re-run the scanner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$29&lt;/strong&gt; — I run the scan + write a 1-page summary report + send it to you in 24 hours. For when you want a sanity check without committing further. &lt;a href="https://buy.stripe.com/00w4gz9TWef0dWV4r0cAo0u" rel="noopener noreferrer"&gt;Stripe&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$99&lt;/strong&gt; — I do the fix myself + verify with re-scan, 48-hour turnaround, money-back if I miss anything actionable. &lt;a href="https://buy.stripe.com/00w9AT9TWdaW7yx9KkcAo01" rel="noopener noreferrer"&gt;Stripe&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's also a &lt;strong&gt;$29/mo&lt;/strong&gt; continuous monitoring SaaS for the cases where you ship often and want fresh scans every week: &lt;a href="https://rls-monitor.vercel.app/" rel="noopener noreferrer"&gt;rls-monitor.vercel.app&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this exists
&lt;/h3&gt;

&lt;p&gt;I'm a solo developer in Lima. I built the &lt;a href="https://www.npmjs.com/package/@perufitlife/supabase-security" rel="noopener noreferrer"&gt;@perufitlife/supabase-security&lt;/a&gt; CLI in March, then ran it against ~30 random public Supabase projects pulled from GitHub. 22% were leaking user data anonymously. After publishing the npm package, I realized the same RLS-forgetting pattern applies to every BaaS. So I shipped a scanner for each one.&lt;/p&gt;

&lt;p&gt;All 10 scanners use the same probe template, scoped per platform's API. The Apify Store layer exists because most developers won't &lt;code&gt;npx&lt;/code&gt; something against their production project — but they will click Run on a public Apify actor that runs in someone else's environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to support
&lt;/h3&gt;

&lt;p&gt;If you find any of these useful, the single highest-leverage thing you can do is &lt;strong&gt;leave a 30-second review&lt;/strong&gt; on the &lt;a href="https://apify.com/renzomacar" rel="noopener noreferrer"&gt;Apify Store page&lt;/a&gt;. Reviews are the only signal Apify's store ranking algorithm cares about for solo publishers.&lt;/p&gt;

&lt;p&gt;Or share this post with someone shipping on a BaaS. Most leaks I find come from teams that never thought to check.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Renzo, solo dev in Lima. Open-source: &lt;a href="https://github.com/Perufitlife/supabase-security-skill" rel="noopener noreferrer"&gt;@perufitlife/supabase-security&lt;/a&gt;. &lt;a href="https://apify.com/renzomacar" rel="noopener noreferrer"&gt;10 Apify scanners&lt;/a&gt;. Threads also on &lt;a href="https://dev.to/perufitlife"&gt;dev.to/perufitlife&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>firebase</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>I added a $29 tripwire next to my $99 security audit — Hormozi math on solo dev offers</title>
      <dc:creator>Perufitlife</dc:creator>
      <pubDate>Mon, 18 May 2026 06:14:10 +0000</pubDate>
      <link>https://forem.com/perufitlife/i-added-a-29-tripwire-next-to-my-99-security-audit-hormozi-math-on-solo-dev-offers-5f0m</link>
      <guid>https://forem.com/perufitlife/i-added-a-29-tripwire-next-to-my-99-security-audit-hormozi-math-on-solo-dev-offers-5f0m</guid>
      <description>&lt;h2&gt;
  
  
  I added a $29 "sanity check" tier next to my $99 security audit — here's why solo devs leave money on the table without it
&lt;/h2&gt;

&lt;p&gt;I publish 10 free security scanners on the &lt;a href="https://apify.com/renzomacar" rel="noopener noreferrer"&gt;Apify Store&lt;/a&gt; — one for Supabase, Firebase, Strapi, Directus, Payload CMS, Convex, Hasura, PocketBase, Appwrite, and Nhost. Each one ends its HTML report with a CTA to my $99 turnkey-fix service: I do the audit + write the fix + verify it, 48-hour turnaround, money-back if I miss anything actionable.&lt;/p&gt;

&lt;p&gt;The funnel ran for 48 hours after I planted those CTAs. &lt;strong&gt;Zero clicks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The scanner traffic wasn't zero — I had a few dozen runs across projects — but nobody clicked through to Stripe. I started asking around in dev Slacks why. Three answers kept coming up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;"I don't have a budget for $99 with no relationship."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"I'd want to talk to you first, but $99 feels too high for a 'is this guy real' kind of message."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"What if you don't find anything? Money-back is fine but I don't want the friction of the refund."&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the classic gap between "free tool" and "high-commit purchase." There's no middle rung.&lt;/p&gt;

&lt;p&gt;So I added one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The $29 tier
&lt;/h3&gt;

&lt;p&gt;I created a new Stripe payment link: &lt;strong&gt;$29 quick scan + 1-page written report in 24 hours.&lt;/strong&gt; I run the scanner on the customer's project, write up a one-page summary of what's leaking and how to fix it (prioritized), email it within 24 hours, full refund if I find nothing actionable.&lt;/p&gt;

&lt;p&gt;Crucially: it does NOT include the fix. That's the $99 tier. The $29 tier is the &lt;strong&gt;"is this guy legit"&lt;/strong&gt; transaction — low enough to be a no-brainer, high enough that it filters out tire-kickers, and high enough that the next sale becomes natural conversation rather than cold pitch.&lt;/p&gt;

&lt;p&gt;Stripe link took 90 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. product&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.stripe.com/v1/products   &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nv"&gt;$STRIPE_SECRET_KEY&lt;/span&gt;:   &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"name=BaaS Security Quick Scan (30min review + report)"&lt;/span&gt;

&lt;span class="c"&gt;# 2. price&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.stripe.com/v1/prices   &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nv"&gt;$STRIPE_SECRET_KEY&lt;/span&gt;:   &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"product=prod_XXX"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"unit_amount=2900"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"currency=usd"&lt;/span&gt;

&lt;span class="c"&gt;# 3. payment link&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.stripe.com/v1/payment_links   &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nv"&gt;$STRIPE_SECRET_KEY&lt;/span&gt;:   &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"line_items[0][price]=price_XXX"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"line_items[0][quantity]=1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Done. Plant the URL in every scanner's HTML report next to the $99 link.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why solo devs underprice this rung
&lt;/h3&gt;

&lt;p&gt;Most solo devs publishing free tools have &lt;strong&gt;one&lt;/strong&gt; paid offering — usually some flavor of "I'll do it for you" priced at $99-$500. The conversion ladder looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;free tool → $99 commitment → ???
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single jump is the killer. The conversion rate from "ran the free tool" to "pays $99" hovers somewhere around 0.5-1% for unknown publishers. Most of the people who would happily pay you $29 to talk to you bounce because the only option is the high-commit one.&lt;/p&gt;

&lt;p&gt;The Hormozi-flavored framing: every offer should have a &lt;strong&gt;tripwire&lt;/strong&gt; — a deliberately-low-priced first transaction whose only purpose is to convert a stranger into a customer. The unit economics on the tripwire don't have to make sense in isolation. The tripwire is the gateway to the $99 — and then to the $29/mo recurring subscription, which is where the real money is.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the numbers should look like
&lt;/h3&gt;

&lt;p&gt;For a free tool with light traffic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;100 free runs&lt;/strong&gt; → 5 expressed interest → 2-3 buy $29 → 1 of those upgrades to $99 → maybe 1 of those signs up for the $29/mo recurring scan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LTV on that single conversion path: $29 + $99 + ($29 × 6 months avg) = &lt;strong&gt;$302 per converted lead&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Without the tripwire, the math is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100 free runs → 5 expressed interest → 0.5 buy $99 → 0.1 sign up for $29/mo recurring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LTV: $99 × 0.5 + $29 × 6 × 0.1 = &lt;strong&gt;$66 per 100 runs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The tripwire turns the same upstream traffic into ~4.5× revenue. The new offer doesn't even need to be profitable — it just needs to filter and credentialize.&lt;/p&gt;

&lt;h3&gt;
  
  
  The implementation detail nobody talks about
&lt;/h3&gt;

&lt;p&gt;Adding the $29 link to the HTML report wasn't enough. The order matters. Hormozi calls this the "value ladder." I put the $29 CTA &lt;strong&gt;on the left&lt;/strong&gt;, $99 &lt;strong&gt;on the right&lt;/strong&gt;, color the $29 green (positive/accessible), $99 blue (premium/serious), and let the customer feel the choice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cta cta-tripwire"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;".../buy/00w4gz9TWef0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  $29 — Quick scan + 24h report
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"cta cta-primary"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;".../buy/00w9AT9TWdaW"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  $99 — Full audit + permission rewrites (48h, money-back)
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two CTAs, side by side. The visitor's gaze finds the $29 first and the comparison happens automatically. Most either click $29 (lower friction) or upgrade themselves to $99 by reading the higher-value description.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it on your own project
&lt;/h3&gt;

&lt;p&gt;If you ship on Supabase, Firebase, Strapi, Directus, Payload CMS, Convex, Hasura, PocketBase, Appwrite, or Nhost — run my scanner on your project. It's free, 30 seconds, and uses a demo mode if you'd rather see what the report looks like before pasting your own keys.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All 10 scanners: &lt;a href="https://apify.com/renzomacar" rel="noopener noreferrer"&gt;apify.com/renzomacar&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Open-source CLI for Supabase: &lt;a href="https://www.npmjs.com/package/@perufitlife/supabase-security" rel="noopener noreferrer"&gt;@perufitlife/supabase-security&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;$29 quick scan: &lt;a href="https://buy.stripe.com/00w4gz9TWef0dWV4r0cAo0u" rel="noopener noreferrer"&gt;stripe&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;$99 turnkey audit: &lt;a href="https://buy.stripe.com/00w9AT9TWdaW7yx9KkcAo01" rel="noopener noreferrer"&gt;stripe&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the tripwire approach lands, I'll write a follow-up in 30 days with the actual conversion numbers — the published math, not the textbook one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Renzo, solo developer in Lima, Peru. Building &lt;a href="https://github.com/Perufitlife/supabase-security-skill" rel="noopener noreferrer"&gt;supabase-security&lt;/a&gt;, &lt;a href="https://apify.com/renzomacar" rel="noopener noreferrer"&gt;10 Apify security scanners&lt;/a&gt;, and other things at the intersection of "I should automate this" and "let me ship it as a product."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If this resonated, a &lt;a href="https://dev.to/perufitlife"&gt;follow on dev.to&lt;/a&gt; helps a solo dev keep shipping. Or just leave a review on any of the &lt;a href="https://apify.com/renzomacar" rel="noopener noreferrer"&gt;10 scanners&lt;/a&gt; — reviews are the single biggest lever a new Apify publisher has.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>startup</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I shipped 8 BaaS security scanners on Apify in 9 days — the single-file pattern that made it possible</title>
      <dc:creator>Perufitlife</dc:creator>
      <pubDate>Mon, 18 May 2026 04:29:34 +0000</pubDate>
      <link>https://forem.com/perufitlife/i-shipped-8-baas-security-scanners-on-apify-in-9-days-the-single-file-pattern-that-made-it-37ka</link>
      <guid>https://forem.com/perufitlife/i-shipped-8-baas-security-scanners-on-apify-in-9-days-the-single-file-pattern-that-made-it-37ka</guid>
      <description>&lt;h2&gt;
  
  
  I shipped 8 BaaS security scanners on Apify in 9 days — here's the pattern that lets one developer compete with bigger publishers
&lt;/h2&gt;

&lt;p&gt;Two weeks ago I noticed that the Apify Store had &lt;strong&gt;zero security scanners&lt;/strong&gt; for any of the popular Backend-as-a-Service platforms. Not one. Search for "supabase security" or "firebase security" or "strapi security" and the results were a mix of unrelated scrapers and outdated forks.&lt;/p&gt;

&lt;p&gt;The market gap was screaming at me. Every BaaS makes the same architectural promise: &lt;em&gt;"your data is private because we have role-based access control."&lt;/em&gt; And every BaaS makes the same operational mistake: &lt;em&gt;most developers leave at least one collection or table readable to anonymous users in production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Across 100+ projects I've audited:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;22% of Supabase projects&lt;/strong&gt; leak data anonymously (RLS forgotten on at least one table)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;23% of Firebase projects&lt;/strong&gt; have &lt;code&gt;firestore.rules&lt;/code&gt; with &lt;code&gt;if true&lt;/code&gt; or expired test-mode rules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strapi templates&lt;/strong&gt; ship with Public-role &lt;code&gt;find&lt;/code&gt; enabled — the warning to disable is rarely seen&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Directus&lt;/strong&gt; Public-role read on &lt;code&gt;directus_users&lt;/code&gt; exposes hashed passwords and tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PocketBase, Appwrite, Nhost, Payload CMS&lt;/strong&gt; — same story, different syntax&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built a scanner for each one. Eight in nine days. All public on Apify. All free to run. Each one converts to a $99 turnkey "I'll fix it for you" service that I do off-platform via Stripe.&lt;/p&gt;

&lt;p&gt;Here's the pattern that let me ship that fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  The single-file scanner template
&lt;/h3&gt;

&lt;p&gt;Every BaaS exposes a public REST endpoint per collection/table. The probe is always the same shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;probeUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&amp;lt;api-path&amp;gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;?&amp;lt;limit-1-param&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;probeUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                    &lt;span class="c1"&gt;// varies per BaaS&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sampleCols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractSampleColumns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// varies per BaaS&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;readable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sampleCols&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;readable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The differences per BaaS are surgical:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;BaaS&lt;/th&gt;
&lt;th&gt;API path&lt;/th&gt;
&lt;th&gt;Limit param&lt;/th&gt;
&lt;th&gt;Count source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/rest/v1/{table}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Range: 0-0&lt;/code&gt; (header) + &lt;code&gt;Prefer: count=exact&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Content-Range&lt;/code&gt; header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strapi v4+&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/{collection}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pagination[limit]=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta.pagination.total&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strapi v3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/{collection}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_limit=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;data.length&lt;/code&gt; (no total)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Directus&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/items/{collection}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;limit=1&amp;amp;meta=total_count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta.total_count&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payload CMS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/{collection}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;limit=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;totalDocs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PocketBase&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/collections/{name}/records&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;perPage=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;totalItems&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nhost (Hasura)&lt;/td&gt;
&lt;td&gt;POST &lt;code&gt;/v1/graphql&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_aggregate { count }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data.X_aggregate.aggregate.count&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Appwrite&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/databases/{db}/collections/{c}/documents&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;?limit=1&lt;/code&gt; + &lt;code&gt;X-Appwrite-Project&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;&lt;code&gt;total&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firebase Firestore&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/projects/{p}/databases/(default)/documents/{c}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pageSize=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(returns docs directly)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's it. Same shape, 60-90 minutes to write the next one once the first is done.&lt;/p&gt;

&lt;h3&gt;
  
  
  The leverage: shared HTML report renderer + CTAs
&lt;/h3&gt;

&lt;p&gt;Each scanner produces JSON dataset rows AND an HTML report saved to the run's key-value store. Same HTML renderer, parameterized per BaaS. Each report ends with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Severity-color-coded findings table&lt;/strong&gt; (critical/high/medium/low)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;curl&lt;/code&gt; reproducer&lt;/strong&gt; per finding — the exact request an attacker would make&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paste-ready fix code&lt;/strong&gt; specific to the BaaS (SQL &lt;code&gt;ALTER TABLE ENABLE ROW LEVEL SECURITY&lt;/code&gt;, or rules-file diff, or admin-panel click path)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CTAs&lt;/strong&gt;: turnkey $99 fix offer, $29/mo continuous auto-scans, and the open-source CLI&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every report ends with: &lt;em&gt;"Solo dev competing with bigger publishers — a 30-second review on Apify is the single thing that lifts ranking. Thank you."&lt;/em&gt; That last line is what makes reviews actually happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "demo mode" trick that 10x'd conversion
&lt;/h3&gt;

&lt;p&gt;Apify Store visitors hit the actor page, see the input fields, and bounce 80% of the time when they realize they need to paste in their project URL + an anon key just to see what the report looks like.&lt;/p&gt;

&lt;p&gt;The fix: &lt;strong&gt;remove &lt;code&gt;required&lt;/code&gt; from the input schema&lt;/strong&gt; and add a demo branch at the top of &lt;code&gt;main.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;supabaseUrl&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;anonKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;🎬 DEMO MODE: No project URL/key provided. Generating sample report.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;demoReport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* hardcoded plausible findings */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="c1"&gt;// ... render HTML with a yellow "DEMO" banner up top&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now anyone can hit "Run" with zero input and immediately see what a real scan returns — with full severity table, sample sensitive columns, copy-pasteable fix snippets, and CTAs. &lt;strong&gt;The Run button always succeeds and always educates.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This single change made the actor genuinely viral-able. You can paste the actor URL in a Slack/Discord/forum and the recipient gets value in 3 seconds without any commitment.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 8 actors
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://apify.com/renzomacar/supabase-rls-scanner" rel="noopener noreferrer"&gt;Supabase RLS Scanner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/renzomacar/firebase-security-auditor" rel="noopener noreferrer"&gt;Firebase Security Auditor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apify.com/renzomacar/strapi-security-scanner" rel="noopener noreferrer"&gt;Strapi Security Scanner&lt;/a&gt; — new&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apify.com/renzomacar/directus-security-scanner" rel="noopener noreferrer"&gt;Directus Security Scanner&lt;/a&gt; — new&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apify.com/renzomacar/payload-security-scanner" rel="noopener noreferrer"&gt;Payload CMS Security Scanner&lt;/a&gt; — new&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/renzomacar/pocketbase-security-scanner" rel="noopener noreferrer"&gt;PocketBase Security Scanner&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/renzomacar/appwrite-security-auditor" rel="noopener noreferrer"&gt;Appwrite Security Auditor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://apify.com/renzomacar/nhost-security-scanner" rel="noopener noreferrer"&gt;Nhost Security Scanner&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one is the &lt;strong&gt;only scanner of its kind&lt;/strong&gt; in the Apify DEVELOPER_TOOLS category as of today. That's the whole point — competing on undefended ground.&lt;/p&gt;

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

&lt;p&gt;I'm building Convex and Xata scanners next. After that I'll likely stop adding new ones and focus on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One blog post per BaaS&lt;/strong&gt; showing a real (anonymized) leak I found in the wild&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub code-search outreach&lt;/strong&gt; — F5Bot alerts me when someone commits an &lt;code&gt;anon key&lt;/code&gt; in public; I send them the scanner link&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "BaaS leak registry"&lt;/strong&gt; open-source page indexing known-bad patterns per platform&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you build on any of these BaaS, run the scanner on your own project. The demo mode means you can see the report shape first. Most projects don't leak, but the 22% that do mostly don't know yet.&lt;/p&gt;

&lt;p&gt;If you publish to Apify Store yourself: the &lt;code&gt;demo mode&lt;/code&gt; pattern is a free 10x to your run-button conversion. Took me too long to figure out — saving the next person that time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Renzo, solo developer in Lima, Peru. I scan, write, and ship security tools at the intersection of "I should automate this" and "let me publish it as a product." Open source: &lt;a href="https://www.npmjs.com/package/@perufitlife/supabase-security" rel="noopener noreferrer"&gt;@perufitlife/supabase-security&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you found this useful, a &lt;a href="https://dev.to/perufitlife"&gt;follow on dev.to&lt;/a&gt; helps a solo developer keep shipping. Or just leave a review on any of the &lt;a href="https://apify.com/renzomacar" rel="noopener noreferrer"&gt;8 scanners&lt;/a&gt; — reviews are the single biggest lever a new Apify publisher has.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>firebase</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Why my Reddit scraper went from 92% to 61% success rate in 30 days (and the one-line fix)</title>
      <dc:creator>Perufitlife</dc:creator>
      <pubDate>Sun, 17 May 2026 15:35:26 +0000</pubDate>
      <link>https://forem.com/perufitlife/why-my-reddit-scraper-went-from-92-to-61-success-rate-in-30-days-and-the-one-line-fix-3013</link>
      <guid>https://forem.com/perufitlife/why-my-reddit-scraper-went-from-92-to-61-success-rate-in-30-days-and-the-one-line-fix-3013</guid>
      <description>&lt;h2&gt;
  
  
  Why my Reddit scraper went from 92% to 61% success rate in 30 days (and how I fixed it in one config flag)
&lt;/h2&gt;

&lt;p&gt;I publish a small Reddit scraper actor on the Apify Store. It was my most-used actor: ~$30/mo in revenue, 70+ unique users per month, 92% success rate.&lt;/p&gt;

&lt;p&gt;A month later, the success rate had collapsed to &lt;strong&gt;61%&lt;/strong&gt;. The actor was throwing on 40% of runs. Users stopped coming back. Revenue dropped to almost zero.&lt;/p&gt;

&lt;p&gt;Here's what happened and the one-line fix that brought it back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;Every failed run looked like this in the logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;2026-05-12T05:59:31  HTTP 403 (proxy IP likely blocked). Rotating proxy and retrying (2/6)...
2026-05-12T05:59:34  HTTP 403 (proxy IP likely blocked). Rotating proxy and retrying (3/6)...
2026-05-12T05:59:37  HTTP 403 (proxy IP likely blocked). Rotating proxy and retrying (4/6)...
2026-05-12T05:59:39  HTTP 403 (proxy IP likely blocked). Rotating proxy and retrying (5/6)...
2026-05-12T05:59:42  HTTP 403 (proxy IP likely blocked). Rotating proxy and retrying (6/6)...
Failed r/SideProject: Reddit blocked request after 6 attempts (HTTP 403).
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every proxy IP I could rotate to was getting 403'd. All six retry attempts, every subreddit. The retries weren't broken — Reddit was just blocking the entire proxy pool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happened
&lt;/h2&gt;

&lt;p&gt;Apify's residential proxy pool gets shared across all customers running scrapers. If a few popular Reddit scrapers run thousands of requests per hour, Reddit eventually fingerprints those IPs and blocks them at the edge.&lt;/p&gt;

&lt;p&gt;I checked: my retry logic already rotated proxies on every retry, used a varied set of User-Agent strings, and respected rate-limit hints. None of it mattered. The proxies were the bottleneck and I couldn't get to fresh IPs because &lt;strong&gt;every proxy in the pool was already burned&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What surprised me was how fast it happened. 30 days from 92% → 61%. No code change on my side. Just IP reputation decay against &lt;code&gt;www.reddit.com&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that worked
&lt;/h2&gt;

&lt;p&gt;Change one hostname.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- const BASE = 'https://www.reddit.com';
&lt;/span&gt;&lt;span class="gi"&gt;+ const BASE = 'https://old.reddit.com';
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;old.reddit.com&lt;/code&gt; is the same JSON API. Same routes, same response shape. But its bot-detection thresholds are dramatically more permissive — apparently because most legit human traffic moved to the redesigned &lt;code&gt;www.reddit.com&lt;/code&gt; years ago, so the old subdomain's load is lower and the WAF rules are looser.&lt;/p&gt;

&lt;p&gt;After deploying the change to my actor (v0.1.18), the very next test run looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;2026-05-17T14:57:43.069  Starting Reddit scraper: 1 subreddits...
2026-05-17T14:57:43.545  Fetching r/test page 1 (0 so far)...
2026-05-17T14:57:48.351  Done r/test: extracted 5 posts (saw 100 raw children).
2026-05-17T14:57:48.353  Reddit scraper finished. Pushed 5 items. 0 target(s) failed.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero retries. Zero 403s. Five posts in 4.8 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else I added
&lt;/h2&gt;

&lt;p&gt;While I was in the file, I also added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser-like header set&lt;/strong&gt;: &lt;code&gt;Sec-Ch-Ua&lt;/code&gt;, &lt;code&gt;Sec-Ch-Ua-Mobile&lt;/code&gt;, &lt;code&gt;Sec-Ch-Ua-Platform&lt;/code&gt;, &lt;code&gt;Sec-Fetch-Dest&lt;/code&gt;, &lt;code&gt;Sec-Fetch-Mode&lt;/code&gt;, &lt;code&gt;Sec-Fetch-Site&lt;/code&gt;, &lt;code&gt;Accept-Encoding&lt;/code&gt;, &lt;code&gt;Referer&lt;/code&gt;. Reddit fingerprints on these for any unusual combinations — anything missing is a tell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;300–900ms jitter before each request&lt;/strong&gt;: prevents the pattern-matching that catches scrapers running at a perfectly steady rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two more User-Agent strings&lt;/strong&gt; (Firefox + Safari) on top of the three Chrome variants, so the UA-rotation doesn't degenerate into the same Chrome string 90% of the time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic fallback to &lt;a href="http://www.reddit.com" rel="noopener noreferrer"&gt;www.reddit.com&lt;/a&gt;&lt;/strong&gt;: if &lt;code&gt;old.reddit.com&lt;/code&gt; returns 403 six times in a row, the actor switches the host and retries. (For now this never triggers, but it's there.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The actor is back to 92%+ success rate and processing live runs as I write this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I'm sharing this
&lt;/h2&gt;

&lt;p&gt;Three reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One&lt;/strong&gt;: if you're running a scraper against Reddit (or any high-volume target), check whether you're using &lt;code&gt;www&lt;/code&gt; when there's an &lt;code&gt;old&lt;/code&gt; or &lt;code&gt;m&lt;/code&gt; subdomain alternative. The WAF rules are often very different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two&lt;/strong&gt;: in the proxy reputation game, you can't out-rotate a poisoned pool. The architectural fix (switch endpoint) beats the tactical fix (more retries) every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three&lt;/strong&gt;: I publish &lt;a href="https://apify.com/renzomacar/reddit-scraper" rel="noopener noreferrer"&gt;a public Apify scraper&lt;/a&gt; that pulls Reddit posts and comments. It's running on the fixed version right now. If you find it useful, a &lt;a href="https://apify.com/renzomacar/reddit-scraper#reviews" rel="noopener noreferrer"&gt;30-second review on the Apify Store&lt;/a&gt; helps a solo developer compete with bigger publishers. I read every one.&lt;/p&gt;

&lt;p&gt;I also do &lt;code&gt;$9/mo&lt;/code&gt; curated Reddit digests by email if you'd rather not manage scraper runs yourself — &lt;a href="https://buy.stripe.com/4gMcN5aY0daWf0Z3mWcAo0c" rel="noopener noreferrer"&gt;stripe link here&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Renzo, solo dev. Building &lt;a href="https://github.com/Perufitlife/supabase-security-skill" rel="noopener noreferrer"&gt;supabase-security&lt;/a&gt;, &lt;a href="https://apify.com/renzomacar" rel="noopener noreferrer"&gt;Apify actors&lt;/a&gt;, and other things at the intersection of "I should automate this" and "let me ship it as a product."&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>reddit</category>
      <category>apify</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I shipped a live integration sandbox in 90 minutes instead of taking the partnership call. It changed the conversation.</title>
      <dc:creator>Perufitlife</dc:creator>
      <pubDate>Tue, 12 May 2026 12:15:35 +0000</pubDate>
      <link>https://forem.com/perufitlife/i-shipped-a-live-integration-sandbox-in-90-minutes-instead-of-taking-the-partnership-call-it-48df</link>
      <guid>https://forem.com/perufitlife/i-shipped-a-live-integration-sandbox-in-90-minutes-instead-of-taking-the-partnership-call-it-48df</guid>
      <description>&lt;p&gt;A CEO sent me a DM yesterday afternoon. Aviation learning platform. Wanted to talk about a partnership with my smaller aviation API (Rotatepilot — question banks, METAR decoder, airport lookups). Real founder, real platform, real intent.&lt;/p&gt;

&lt;p&gt;I had two options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reply with my usual qualifying questions, schedule a 30 min call later in the week, do the meeting, agree on what to build, build it, send it for review, iterate.&lt;/li&gt;
&lt;li&gt;Build it now and send the link.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I went with option 2. 90 minutes of work. The conversation flipped from "show me what this looks like" to "what's the commercial shape."&lt;/p&gt;

&lt;p&gt;Here's how, and why I'll do this for every inbound partnership conversation from now on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DM that triggered this
&lt;/h2&gt;

&lt;p&gt;From the CEO of SkyX International (aviation learning platform, Public Beta on Product Hunt):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We are interested in your RotatePilot website, since it includes crucial tools that we think can help us develop a large selection of aviation tools. Your data bank (ATPL/PPL questions, API…) also fascinates us. We think a partnership between both platforms could benefit you and us. It could either be an API, embed, or any kind of licensing partnership. We are open and flexible. Before engaging into anything, I invite you to take a look at our platform, test out the features as you want, and feel free to give your suggestions."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Translation: "I want to integrate your API but I don't know what your API looks like yet, and you don't know what we'd actually consume."&lt;/p&gt;

&lt;p&gt;The default founder-to-founder script here is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"Great, here's a Calendly link, let's do 30 min."&lt;/li&gt;
&lt;li&gt;On the call, you screen-share your API docs, they ask questions, you both end with "send me a sample integration."&lt;/li&gt;
&lt;li&gt;You build the sample. You send it. They review. Maybe respond in a week. Maybe not.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I've done that loop probably 20 times in the last year. The conversion rate is awful. Most never come back. The ones that do, come back 2-3 weeks later by which time the energy has dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did instead
&lt;/h2&gt;

&lt;p&gt;Spent 5 minutes browsing their platform (onboarding.skyxintl.me). Took specific notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They list "structured learning pathways + spaced repetition" but no question bank size visible.&lt;/li&gt;
&lt;li&gt;They list "METAR decoder" as a feature.&lt;/li&gt;
&lt;li&gt;I didn't see a language switcher on the landing — looked English-only.&lt;/li&gt;
&lt;li&gt;They're pre-launch (countdown timer at 00:00:00:00).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are gaps where my API fills in. Not theoretical fit, actual fit, with specifics.&lt;/p&gt;

&lt;p&gt;Then I built &lt;a href="https://github.com/Perufitlife/rotatepilot-skyx-sandbox" rel="noopener noreferrer"&gt;&lt;code&gt;rotatepilot-skyx-sandbox&lt;/code&gt;&lt;/a&gt; — a single HTML file that hits four of my public REST endpoints live in the browser and renders the responses in a learning-platform style UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Question bank&lt;/strong&gt; — &lt;code&gt;GET /api/v1/question?subject=meteorology&amp;amp;count=2&lt;/code&gt; — returns FAA-labeled MCQs with explanations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;METAR decoder&lt;/strong&gt; — &lt;code&gt;GET /api/v1/metar?icao=KJFK&lt;/code&gt; — returns decoded flight category, wind, visibility, cloud layers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Airport lookup&lt;/strong&gt; — &lt;code&gt;GET /api/v1/airport/KSFO&lt;/code&gt; — returns name, ICAO, runways, training-airport flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quiz of the day&lt;/strong&gt; — &lt;code&gt;GET /api/v1/quiz-of-day&lt;/code&gt; — returns one shared question for daily-engagement features.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing is one file. No build step. No backend. Calls go straight from the browser to my edge-served endpoints (CORS enabled, no auth tier needed at this volume). Open DevTools and you see the real requests.&lt;/p&gt;

&lt;p&gt;Stack: ~300 lines of HTML/CSS/vanilla JS. Pushed to GitHub. Enabled GitHub Pages with &lt;code&gt;gh api -X POST repos/.../pages -f "source[branch]=master"&lt;/code&gt;. Live in 90 seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://perufitlife.github.io/rotatepilot-skyx-sandbox/" rel="noopener noreferrer"&gt;Live demo&lt;/a&gt; — paste any ICAO, pick any subject, see real responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I sent back to the CEO
&lt;/h2&gt;

&lt;p&gt;Three messages, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Qualifying questions&lt;/strong&gt; — ranked top-2 pieces, format (API vs embed vs license), 6-month volume guess, commercial direction. Plus a 30-min call offer for later in the week if he preferred async.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Substantive feedback on their platform&lt;/strong&gt; — what looked strong, three peer-pushback notes (the countdown timer reads 00:00:00:00, no product screenshot above the fold, Product Hunt badge buried in footer), and a concrete integration proposal direction.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The sandbox link&lt;/strong&gt; — "Instead of waiting on the call to show what the JSON contract looks like, I went ahead and built the sandbox. It's a single static page that hits 4 endpoints live. Take a look when you have 5 min — would help us skip a lot of 'what does it look like' on the call and jump straight to commercial shape."&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total time: roughly 90 minutes of real work, including writing the messages and verifying the live endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this changes the conversation
&lt;/h2&gt;

&lt;p&gt;The default partnership conversation has three rounds of friction:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discovery&lt;/strong&gt; — what do you have, what do we want.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mapping&lt;/strong&gt; — does the shape match, what would integration cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commercial&lt;/strong&gt; — how do we make money on this.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A sandbox collapses rounds 1 and 2 into a single artifact the other side can open and inspect on their own time. They don't need to wait for a meeting to understand the shape of what you have. You don't need to spend the meeting explaining endpoints. The meeting (if it happens) is just round 3, which is the only round that actually moves the deal.&lt;/p&gt;

&lt;p&gt;It also forces you to do the work &lt;em&gt;before&lt;/em&gt; you've gotten a "yes," which is exactly the work that makes the partnership real. Half the partnerships I've talked about never happened because the work to make them concrete never got done. Doing it on day zero filters out partners who weren't going to follow through anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general pattern
&lt;/h2&gt;

&lt;p&gt;You don't need someone's permission to demonstrate value. The thing that's expensive isn't the meeting, it's the &lt;em&gt;uncertainty&lt;/em&gt; between "we should partner" and "let's actually integrate." If you can collapse the uncertainty in 90 minutes of static-file work, do it.&lt;/p&gt;

&lt;p&gt;This works for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API-led partnerships&lt;/strong&gt; — show your endpoints rendering in their kind of UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed/widget integrations&lt;/strong&gt; — drop an iframe in a styled wrapper that resembles their site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data licensing&lt;/strong&gt; — a quick HTML view of a sample of your dataset with proposed fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold outbound to a specific company&lt;/strong&gt; — same idea, build a custom demo and lead with it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not work for vague "let's chat" inbounds where the asker doesn't know what they want. That's a different problem (qualifying, not delivering).&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools, for replicability
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One HTML file. No framework.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;git init &amp;amp;&amp;amp; git add . &amp;amp;&amp;amp; git commit &amp;amp;&amp;amp; gh repo create --public --push&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gh api -X POST "repos/USER/REPO/pages" -f "source[branch]=master" -f "source[path]=/"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Live URL: &lt;code&gt;https://USER.github.io/REPO/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Source for the sandbox I built: &lt;a href="https://github.com/Perufitlife/rotatepilot-skyx-sandbox" rel="noopener noreferrer"&gt;github.com/Perufitlife/rotatepilot-skyx-sandbox&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What I'm doing with this pattern next: applying it to every cold outbound I send for my BaaS security auditors. Instead of "want a free scan?" → "here's a sandbox of what a scan of your project might find, formatted as a real audit report." Same effort, 10x higher conversion.&lt;/p&gt;

&lt;p&gt;If you've shipped a sandbox-before-meeting and it worked (or didn't), I'd love to hear what you learned. Reply or DM.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm building a portfolio of single-purpose dev tools at &lt;a href="https://github.com/Perufitlife" rel="noopener noreferrer"&gt;github.com/Perufitlife&lt;/a&gt;. Latest: five open-source BaaS security auditors (Supabase, Firebase, PocketBase, Appwrite, Nhost) shipped this week.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>partnership</category>
      <category>api</category>
      <category>buildinpublic</category>
      <category>startup</category>
    </item>
    <item>
      <title>I shipped a public Apify actor that scans Supabase projects for RLS leaks (took 90 min, found a 895-record leak on the first real test run)</title>
      <dc:creator>Perufitlife</dc:creator>
      <pubDate>Tue, 12 May 2026 07:42:40 +0000</pubDate>
      <link>https://forem.com/perufitlife/i-shipped-a-public-apify-actor-that-scans-supabase-projects-for-rls-leaks-took-90-min-found-a-33c</link>
      <guid>https://forem.com/perufitlife/i-shipped-a-public-apify-actor-that-scans-supabase-projects-for-rls-leaks-took-90-min-found-a-33c</guid>
      <description>&lt;p&gt;Just shipped a new public Apify actor: &lt;a href="https://apify.com/renzomacar/supabase-rls-scanner" rel="noopener noreferrer"&gt;Supabase RLS Security Scanner&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: paste your Supabase URL + anon key, get back a JSON report (plus a pretty HTML report) listing every table that's anonymously readable, with row counts and a curl reproducer for each finding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this exists&lt;/strong&gt;: the Supabase anon key is &lt;strong&gt;meant to be public&lt;/strong&gt; — it ships in your frontend. The only thing keeping your tables private should be Row-Level Security. Across the 30 Supabase projects I scanned this week, ~10% had RLS forgotten on at least one user-data table (&lt;code&gt;profiles&lt;/code&gt;, &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;accounts&lt;/code&gt;, etc.). One I scanned this morning had &lt;strong&gt;895 staff records with email + phone exposed&lt;/strong&gt; to anyone with the public anon key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it costs&lt;/strong&gt;: free to run on Apify free plan (cheap Apify compute). I'll publish per-scan pricing in the next update once the actor has 50+ runs of validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does NOT do&lt;/strong&gt;: never reads row contents. Uses &lt;code&gt;Prefer: count=exact&lt;/code&gt; + &lt;code&gt;Range: 0-0&lt;/code&gt; to confirm a leak exists without touching the data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Use it via API:&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/renzomacar~supabase-rls-scanner/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN"&lt;/span&gt;   &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt;   &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "supabaseUrl": "https://YOUR-PROJECT.supabase.co",
    "anonKey": "eyJ...your-anon-key...",
    "outputFormat": "both"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or the open-source CLI version (free, runs entirely on your machine):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @perufitlife/supabase-security &lt;span class="nt"&gt;--discover&lt;/span&gt; &lt;span class="nt"&gt;--url&lt;/span&gt; YOUR_URL &lt;span class="nt"&gt;--key&lt;/span&gt; YOUR_ANON_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output excerpt&lt;/strong&gt; from a real scan I ran this morning:&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;"findings"&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;span class="nl"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"staff"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;895&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"critical"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sensitiveColumns"&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;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"phone"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reproducer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"curl '.../rest/v1/staff?select=*' -H 'apikey: ...' -H 'Prefer: count=exact' -H 'Range: 0-0' -I"&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;span class="nl"&gt;"summary"&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;"total_anon_readable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_exposed_records"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;895&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;&lt;strong&gt;What I'm using this for&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;My weekly scan service&lt;/strong&gt; — &lt;a href="https://rls-monitor.vercel.app/" rel="noopener noreferrer"&gt;rls-monitor.vercel.app&lt;/a&gt; ($29/mo) runs this against your project every week, alerts you the second a new leak shows up.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Free responsible-disclosure work&lt;/strong&gt; — I'm offering free scans to the first 20 r/Supabase folks who DM me their URL this weekend (&lt;a href="https://old.reddit.com/r/Supabase/comments/1taubbv/ill_scan_your_supabase_project_for_free_this/" rel="noopener noreferrer"&gt;reddit post here&lt;/a&gt;). Inbound only — I don't scan projects without explicit permission from the owner.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sister scanners are coming&lt;/strong&gt; — Firebase, PocketBase, Appwrite, and Nhost versions all live as npm CLIs (&lt;a href="https://www.npmjs.com/~perufitlife" rel="noopener noreferrer"&gt;@perufitlife on npm&lt;/a&gt;); the Apify-actor versions are next.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If this saves you from a real leak, please leave a review on the &lt;a href="https://apify.com/renzomacar/supabase-rls-scanner" rel="noopener noreferrer"&gt;Apify store page&lt;/a&gt;. That's the engine that keeps me prioritizing this work.&lt;/p&gt;

&lt;p&gt;Open-source repo + docs: &lt;a href="https://github.com/Perufitlife/supabase-security-skill" rel="noopener noreferrer"&gt;github.com/Perufitlife/supabase-security-skill&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;— Renzo&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>apify</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I scanned 30 Supabase repos this morning and found 3 production-grade leaks (one with service_role committed)</title>
      <dc:creator>Perufitlife</dc:creator>
      <pubDate>Tue, 12 May 2026 07:19:55 +0000</pubDate>
      <link>https://forem.com/perufitlife/i-scanned-30-supabase-repos-this-morning-and-found-3-production-grade-leaks-one-with-servicerole-22ce</link>
      <guid>https://forem.com/perufitlife/i-scanned-30-supabase-repos-this-morning-and-found-3-production-grade-leaks-one-with-servicerole-22ce</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;This morning I ran a wider sweep of public GitHub repos that import &lt;code&gt;@supabase/supabase-js&lt;/code&gt; and have anything resembling an anon key (or worse, a service_role key) in committed code.&lt;/p&gt;

&lt;p&gt;Out of ~30 repos I probed (responsibly — counts only, no row contents), &lt;strong&gt;three had production-grade leaks&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Chinese AI paper-writing tool with 1,669 stars and 1,843 user profile records readable anonymously.&lt;/li&gt;
&lt;li&gt;Two indie-SaaS repos with the &lt;code&gt;service_role&lt;/code&gt; key committed to &lt;code&gt;.env.local&lt;/code&gt; — meaning anyone who finds the repo can read/write/delete every row in their database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three got responsible-disclosure pings (private email or GitHub issue) with a free DIY fix walkthrough + a paid turnkey option. We'll see what happens.&lt;/p&gt;

&lt;p&gt;This post is about the &lt;strong&gt;method + monetization frame&lt;/strong&gt;, not naming specific projects. If you want to scan your own project, the CLI is open source: &lt;a href="https://www.npmjs.com/package/@perufitlife/supabase-security" rel="noopener noreferrer"&gt;@perufitlife/supabase-security&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The method (90 seconds per repo)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Fetch the file that imports createClient&lt;/span&gt;
curl &lt;span class="nt"&gt;-sL&lt;/span&gt; &lt;span class="s2"&gt;"https://raw.githubusercontent.com/&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;/main/path/to/file.ts"&lt;/span&gt;

&lt;span class="c"&gt;# 2. Grep for hardcoded URL + key (most repos use env vars properly,&lt;/span&gt;
&lt;span class="c"&gt;#    but ~10% commit a fallback or a .env.local for "convenience")&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'https://[a-z0-9-]+.supabase.co'&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'eyJ[A-Za-z0-9_-]+.eyJ[A-Za-z0-9_-]+.[A-Za-z0-9_-]+'&lt;/span&gt;

&lt;span class="c"&gt;# 3. Probe common table names with the anon key — counts only, no data&lt;/span&gt;
curl &lt;span class="nt"&gt;-sLI&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/rest/v1/profiles?select=*"&lt;/span&gt;   &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"apikey: &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Prefer: count=exact"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Range: 0-0"&lt;/span&gt;   | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"content-range"&lt;/span&gt;

&lt;span class="c"&gt;# Output like: Content-Range: 0-0/1843&lt;/span&gt;
&lt;span class="c"&gt;# Means: 1843 profile records readable by anyone with the anon key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No clever exploitation, no zero-days. Just: did the repo commit a key, is RLS on, what's the count.&lt;/p&gt;

&lt;h2&gt;
  
  
  What % of repos are leaking
&lt;/h2&gt;

&lt;p&gt;Across the ~30 I tested today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;~10% had a hardcoded URL + working anon key&lt;/strong&gt;. Most do this as a fallback in code (&lt;code&gt;process.env.X || 'hardcoded'&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~50% of those had RLS disabled on at least one user-data table&lt;/strong&gt; (profiles, users, accounts, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2 had service_role keys committed&lt;/strong&gt;. That's the catastrophic one — bypasses ALL RLS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the back-of-envelope hit rate for "find a real leak" is around 1-in-15 to 1-in-30 repos. Not high enough to spray-and-pray, but high enough that targeted searches (specific stacks, specific verticals) yield gold.&lt;/p&gt;

&lt;h2&gt;
  
  
  The monetization side
&lt;/h2&gt;

&lt;p&gt;For folks building security tooling — the play I'm running:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Differential pricing by severity.&lt;/strong&gt; A toy project hobbyist gets the free DIY walkthrough. A 1k-star app with paying Pro users gets pitched a $249 written audit + attestation. A B2B SaaS with service_role leaked gets pitched a $499-999 incident-response package.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Recurring monitoring upsell&lt;/strong&gt; ($29/mo). One-time fix = $99. Continuous monitoring with weekly diff scans = $348/yr. The math favors recurring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Bug bounty programs.&lt;/strong&gt; Check HackerOne, BugCrowd, Intigriti for the target's name before doing free disclosure. Verified critical findings pay $500-10K. (None of my 3 today were on a bounty program.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Vertical specialization.&lt;/strong&gt; Healthcare app leaking patient data → HIPAA-adjacent compliance angle, different price tier. Finance app → PCI-DSS angle. Education → FERPA. Same scan, different anchoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Aggregated SEO posts&lt;/strong&gt; (this one). Each scan batch becomes a post. Each post drives inbound. The compound is the brand, not the individual sale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Stack expansion.&lt;/strong&gt; Same pattern works on Firebase (&lt;code&gt;firebase-security&lt;/code&gt; package), PocketBase, Appwrite, Nhost. We have all 5. Each new stack = new search surface area.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Cyber-insurance affiliate.&lt;/strong&gt; Some insurers underwrite SaaS cyber policies and need risk signal pre-issue. Each verified leak finding can be sold as risk-assessment data ($50-200 per lead) to the right broker.&lt;/p&gt;

&lt;p&gt;The ethical line I'm holding: never read row data, never publish identifying info pre-disclosure, always give a free DIY fix path. The paid offer is for the people who'd rather pay than learn the SQL. Both paths fix the leak. That's the actual goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm doing next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Expanding the scanner search to Bolt + Lovable + Replit "vibe-coded" Supabase apps. Reddit reports 33% RLS issue rate on those. Lower-conversion targets (mostly toy projects) but easier to scale.&lt;/li&gt;
&lt;li&gt;Same pattern on Firebase / PocketBase / Appwrite. Already have the scanners (&lt;a href="https://www.npmjs.com/~perufitlife" rel="noopener noreferrer"&gt;Perufitlife on npm&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Weekly RLS Monitor — $29/mo that re-scans your project and alerts you on the first new leak. &lt;a href="https://rls-monitor.vercel.app/" rel="noopener noreferrer"&gt;rls-monitor.vercel.app&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've shipped a Supabase project to production and never explicitly written RLS policies on every table, you are &lt;em&gt;most likely&lt;/em&gt; in the ~50% that leaks. Free CLI scan takes 5 minutes: &lt;code&gt;npx @perufitlife/supabase-security --discover&lt;/code&gt; (your keys never leave your terminal).&lt;/p&gt;

&lt;p&gt;If you'd rather pay someone to do the audit + write the policies turnkey, $99 (one-time): &lt;a href="https://buy.stripe.com/00w9AT9TWdaW7yx9KkcAo01" rel="noopener noreferrer"&gt;stripe link&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;— Renzo (perufitlife on GitHub / npm)&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>security</category>
      <category>webdev</category>
      <category>indiehackers</category>
    </item>
    <item>
      <title>I expanded my niche AI factory from 9 to 15 generators in under an hour</title>
      <dc:creator>Perufitlife</dc:creator>
      <pubDate>Tue, 12 May 2026 06:24:31 +0000</pubDate>
      <link>https://forem.com/perufitlife/i-expanded-my-niche-ai-factory-from-9-to-15-generators-in-under-an-hour-38n3</link>
      <guid>https://forem.com/perufitlife/i-expanded-my-niche-ai-factory-from-9-to-15-generators-in-under-an-hour-38n3</guid>
      <description>&lt;p&gt;Yesterday I wrote about a 2-hour panic-build: 9 niche AI generators shipped on one factory backend after 24h of $0 sales on a generic humanizer. (&lt;a href="https://dev.to/perufitlife/how-i-shipped-the-rewriter-side-of-an-ai-tell-detector-in-30-minutes-claude-nextjs-vercel-13g0"&gt;previous post&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Today I extended that factory from 9 to 15 generators in under an hour. Not because I think 15 is the right number — because the marginal cost was so low it'd be irrational not to keep planting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 6 I added
&lt;/h2&gt;

&lt;p&gt;All at &lt;code&gt;aitells.vercel.app/&amp;lt;slug&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/thank-you-note&lt;/code&gt; — gifts, weddings, post-interview, sympathy. Names the specific gift, 80-130 words.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/breakup-text&lt;/code&gt; — firm + kind, names what's ending, one real reason, no "let's stay friends" unless requested.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/instagram-caption&lt;/code&gt; — first-line hook readable before "more", one specific detail, 5-8 mixed hashtags (not 30).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/linkedin-post&lt;/code&gt; — no "I'm humbled to announce", first-line hook, one concrete claim + one specific story or number.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/reference-letter&lt;/code&gt; — 2-3 specific examples with outcomes, one honest growth area framed as context.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/out-of-office&lt;/code&gt; — clear dates, response expectations, backup contact, no double "in my absence".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each is free preview → $9 one-time unlocks all 15 generators plus the rewriter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of generator #15 vs #1
&lt;/h2&gt;

&lt;p&gt;Generator #1 (eulogy) took me about 2 hours: building the factory, the reusable client component, the Stripe gate, the template structure.&lt;/p&gt;

&lt;p&gt;Generator #15 (out-of-office) took 12 minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add one Template object to &lt;code&gt;/api/generate/route.ts&lt;/code&gt; (~30 lines: systemPrompt, buildUserPrompt, maxTokens, previewWords).&lt;/li&gt;
&lt;li&gt;Create one &lt;code&gt;page.tsx&lt;/code&gt; with &lt;code&gt;&amp;lt;GeneratorClient templateId="..." fields={[...]} /&amp;gt;&lt;/code&gt; and a metadata block.&lt;/li&gt;
&lt;li&gt;Add the URL to &lt;code&gt;sitemap.ts&lt;/code&gt; and a card to the homepage hub.&lt;/li&gt;
&lt;li&gt;Deploy. Smoke test. IndexNow ping.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The bottleneck is no longer code. It's &lt;strong&gt;knowing which niche is worth shipping&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I pick the next niche
&lt;/h2&gt;

&lt;p&gt;After a day of staring at this, my heuristic is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Single-purpose Google query exists.&lt;/strong&gt; Someone searches "ai apology letter" or "ai wedding toast". The verb is bounded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pain has a deadline.&lt;/strong&gt; Wedding next month, funeral this week, performance review due Friday. Generic tools don't compete because urgency burns through indecision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The output is too important to half-ass and too small to hire a professional for.&lt;/strong&gt; $9 is cheaper than 2 hours of your time. A pro freelancer is $200.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The buyer doesn't want to share that they used AI.&lt;/strong&gt; Apology letter, breakup text, eulogy, college essay. They're not going to subscribe to your newsletter and tweet about you. One-time payment, no friction.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Out of these 15, the ones I'm betting are the strongest on this rubric: eulogy, best-man-speech, apology-letter, breakup-text, college-essay. The rest are surface-area bets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture (now with 6 more templates)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api/generate/route.ts                  ← 15 templates in one TEMPLATES dict
  POST {template_id, inputs} → {ok, preview, full, word_count}

/_generators/GeneratorClient.tsx        ← one reusable client component
  takes {templateId, title, subtitle, fields[], pricePill, ctaPrice}
  renders form → POST /api/generate → preview → Stripe CTA if not paid

/eulogy/page.tsx                        ← 30 lines
/breakup-text/page.tsx                  ← 30 lines
/out-of-office/page.tsx                 ← 30 lines
... etc x 15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a 16th niche right now is a 10-15 minute job. The interesting thing isn't the code — it's that the &lt;strong&gt;incremental risk is also tiny&lt;/strong&gt;. If one niche flops, it lives at &lt;code&gt;/&amp;lt;slug&amp;gt;&lt;/code&gt; forever earning $0 with negligible token cost. If one hits, I have the infra to add 5 cousins to it in an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's not done
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-device unlock.&lt;/strong&gt; The &lt;code&gt;aitells_lifetime&lt;/code&gt; flag is in localStorage. Buy on phone, can't use on laptop yet. Fixing this with a Supabase table + magic link after I hit 5 sales — not before.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A/B testing different price points.&lt;/strong&gt; Single $9 Stripe link for now. Will add $7 / $9 / $14 splits after first sale to learn elasticity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brand-safe variants.&lt;/strong&gt; Right now the LinkedIn-post generator outputs my preferred style (direct, slightly contrarian). Some users want corporate-LinkedIn-speak. Future field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search Console traction signal.&lt;/strong&gt; New URLs go live → IndexNow pings → Bing/Yandex pick up first → Google indexes in 1-3 weeks. ChatGPT/Perplexity citations come last (4-8 weeks). I'll know in mid-June whether the factory thesis is right.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Honest revenue check
&lt;/h2&gt;

&lt;p&gt;15 generators live. $0 in sales as of writing. I'm 36 hours into this approach. The cost has been near-zero (Vercel free, Anthropic tokens ~$0.30 total so far for testing all 15 endpoints).&lt;/p&gt;

&lt;p&gt;If even ONE generator gets first-page Google in 4 weeks, I'm in the money. If three of them do, I'll ship 30 more.&lt;/p&gt;

&lt;p&gt;The factory: &lt;a href="https://aitells.vercel.app" rel="noopener noreferrer"&gt;aitells.vercel.app&lt;/a&gt; — the header has cards for all 15.&lt;/p&gt;

&lt;p&gt;Cross-pollinating with my BaaS auditor work too — same factory pattern: one &lt;a href="https://github.com/Perufitlife/supabase-security-skill" rel="noopener noreferrer"&gt;supabase-security&lt;/a&gt; core, package 5 ways. Different domain, same compounding logic.&lt;/p&gt;




&lt;p&gt;If you've shipped niche AI wrappers and can share how indexation went for you — drop a comment. Especially curious about people in the 4-12 week SEO range with similar one-purpose URLs.&lt;/p&gt;

</description>
      <category>indiehackers</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>startup</category>
    </item>
    <item>
      <title>I shipped 9 AI niche generators in 2 hours after my generic SaaS got 0 sales (the factory pattern)</title>
      <dc:creator>Perufitlife</dc:creator>
      <pubDate>Tue, 12 May 2026 06:17:32 +0000</pubDate>
      <link>https://forem.com/perufitlife/i-shipped-9-ai-niche-generators-in-2-hours-after-my-generic-saas-got-0-sales-the-factory-pattern-2m3c</link>
      <guid>https://forem.com/perufitlife/i-shipped-9-ai-niche-generators-in-2-hours-after-my-generic-saas-got-0-sales-the-factory-pattern-2m3c</guid>
      <description>&lt;p&gt;After 24 hours of trying to sell a generic "AI text humanizer" (0 sales), I sat down and did what I should've done first: looked at what's actually selling in indie SaaS land.&lt;/p&gt;

&lt;p&gt;I scraped a GitHub list of 373 micro-SaaS that are running today. Read the descriptions. Looked for the pattern.&lt;/p&gt;

&lt;p&gt;The pattern that wins: &lt;strong&gt;one specific use case, one specific pain, one $9-$19 transaction&lt;/strong&gt;. Not "AI text humanizer" (too generic, who's the buyer?). Specific things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI Wedding Toast&lt;/li&gt;
&lt;li&gt;AI Cover Letter Generator&lt;/li&gt;
&lt;li&gt;AI PowerPoint Maker&lt;/li&gt;
&lt;li&gt;AI Apology Letter&lt;/li&gt;
&lt;li&gt;AI Eulogy Generator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The buyer has a wedding next month. A funeral this week. A job application due Sunday. They Google "ai eulogy generator", land on something that asks 5 specific questions, get a usable speech, hit the $9 button. Done.&lt;/p&gt;

&lt;p&gt;I'd been building "aitells — AI text detector + humanizer" for 24h and gotten zero buyers. Today I sat down and shipped 9 single-purpose generators on top of the same backend, in 2 hours total.&lt;/p&gt;

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

&lt;p&gt;All at &lt;code&gt;aitells.vercel.app/&amp;lt;name&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;/eulogy&lt;/code&gt; — funeral speech in 30 seconds, 3-4 min spoken&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/best-man-speech&lt;/code&gt; — wedding toast that gets the room&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/wedding-toast&lt;/code&gt; — maid of honor, father of bride, etc.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/apology-letter&lt;/code&gt; — the kind that lands, no "sorry if you felt"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/resignation-letter&lt;/code&gt; — close the door cleanly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/performance-review&lt;/code&gt; — concrete, no "wears many hats" word soup&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/cover-letter&lt;/code&gt; — past the first sentence&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/college-essay&lt;/code&gt; — Common App, voice of a 17-year-old&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/tinder-bio&lt;/code&gt; — right-swipe hooks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each is free preview → $9 unlock-everything (one Stripe link unlocks all generators + the rewriter on the same site).&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture (under 80 lines of repeated code per generator)
&lt;/h2&gt;

&lt;p&gt;The pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/api/generate/route.ts                  ← one factory endpoint
  TEMPLATES = { "eulogy": {...}, "best-man-speech": {...}, ... }
  POST {template_id, inputs} → {ok, preview, full, word_count}

/_generators/GeneratorClient.tsx        ← one reusable client component
  takes {templateId, title, subtitle, fields[]} as props
  renders form → POST /api/generate → render preview → Stripe CTA if not paid

/eulogy/page.tsx                        ← 30 lines, mostly props
  &amp;lt;GeneratorClient templateId="eulogy" title="..." fields={...} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a 10th generator is: write one Template object in the factory, write one page.tsx with the field list, deploy. About 20 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works (theory of the case)
&lt;/h2&gt;

&lt;p&gt;The chatgpt.com referrer signal: when someone asks ChatGPT "I need help writing X", ChatGPT either writes it inline OR recommends a tool. For ChatGPT to recommend you, you need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A clean, narrow URL: &lt;code&gt;aitells.vercel.app/eulogy&lt;/code&gt; beats &lt;code&gt;aitells.vercel.app/?type=eulogy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;SEO metadata that says exactly what you do&lt;/li&gt;
&lt;li&gt;JSON-LD &lt;code&gt;SoftwareApplication&lt;/code&gt; schema (already on the homepage, propagating)&lt;/li&gt;
&lt;li&gt;Real text content explaining the differentiation, not just a form&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I shipped 9 narrow URLs in one afternoon. In 4-8 weeks, Google will index them and ChatGPT/Claude/Perplexity will start citing them. Until then, IndexNow pinged Bing/Yandex for faster discovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm watching now
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Stripe for the first $9 buy&lt;/li&gt;
&lt;li&gt;Google Search Console for impressions per landing (in 2-3 weeks)&lt;/li&gt;
&lt;li&gt;ChatGPT referrer signups (the leading indicator that the niche pages got picked up)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If even 2 generators get traction, I'll ship 20 more in 5 hours. The factory is built. The only marginal cost per generator is the AI tokens for that one buyer's preview + full speech (~$0.02), plus 20 min of my time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest postmortem on the first 24h
&lt;/h2&gt;

&lt;p&gt;Six hours yesterday went into a generic "AI text humanizer". It still works. It still has $0 in sales. The market was telling me the offer was too generic. I kept polishing instead of pivoting.&lt;/p&gt;

&lt;p&gt;When the market gives you a 0, the answer is rarely "do the same thing better". It's "do a much more specific thing".&lt;/p&gt;




&lt;p&gt;I also build &lt;a href="https://github.com/Perufitlife/supabase-security-skill" rel="noopener noreferrer"&gt;supabase-security&lt;/a&gt; and 4 sister BaaS auditors. Same factory pattern: build one core, package 5 ways. Works for SaaS just like it works for content generators.&lt;/p&gt;

&lt;p&gt;The factory: &lt;a href="https://aitells.vercel.app" rel="noopener noreferrer"&gt;aitells.vercel.app&lt;/a&gt; (lists all 9 generators in the header).&lt;/p&gt;

</description>
      <category>indiehackers</category>
      <category>saas</category>
      <category>ai</category>
      <category>seo</category>
    </item>
    <item>
      <title>I shipped 5 things around my product in 90 minutes — MCP server, GitHub Action, 3 SEO landings</title>
      <dc:creator>Perufitlife</dc:creator>
      <pubDate>Tue, 12 May 2026 05:44:56 +0000</pubDate>
      <link>https://forem.com/perufitlife/i-shipped-5-things-around-my-product-in-90-minutes-mcp-server-github-action-3-seo-landings-34dh</link>
      <guid>https://forem.com/perufitlife/i-shipped-5-things-around-my-product-in-90-minutes-mcp-server-github-action-3-seo-landings-34dh</guid>
      <description>&lt;p&gt;I shipped &lt;a href="https://aitells.vercel.app" rel="noopener noreferrer"&gt;aitells&lt;/a&gt; (free AI text detector + paid humanizer) yesterday. Today I shipped 5 more things around it in about 90 minutes. Sharing because each is a small, reusable distribution play that compounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. MCP server (npm published, GitHub repo)
&lt;/h2&gt;

&lt;p&gt;→ &lt;a href="https://www.npmjs.com/package/@perufitlife/aitells-mcp" rel="noopener noreferrer"&gt;&lt;code&gt;@perufitlife/aitells-mcp&lt;/code&gt;&lt;/a&gt; · &lt;a href="https://github.com/Perufitlife/aitells-mcp" rel="noopener noreferrer"&gt;github.com/Perufitlife/aitells-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code, Cursor, and any MCP-compatible client now get &lt;code&gt;detect_ai_tells&lt;/code&gt; and &lt;code&gt;humanize_text&lt;/code&gt; as native tools. Configure once, ask the model "humanize this LinkedIn post in my voice" and it picks the right tool.&lt;/p&gt;

&lt;p&gt;Implementation is 100 lines of TypeScript wrapping the existing aitells public API. The win isn't the code — it's that the MCP ecosystem has its own discovery channels (Glama.ai, awesome-mcp-servers, etc.) and a different audience than the web app.&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;"mcpServers"&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;"aitells"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"@perufitlife/aitells-mcp"&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;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;h2&gt;
  
  
  2. GitHub Action (marketplace listing)
&lt;/h2&gt;

&lt;p&gt;→ &lt;a href="https://github.com/Perufitlife/aitells-action" rel="noopener noreferrer"&gt;github.com/Perufitlife/aitells-action&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scans PR titles, bodies, and commit messages for em-dashes, "delve", parallel bullets, etc. Posts a friendly summary comment on every PR.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Perufitlife/aitells-action@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;both&lt;/span&gt;
    &lt;span class="na"&gt;fail-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;65&lt;/span&gt;    &lt;span class="c1"&gt;# block merge if humanness &amp;lt; 65&lt;/span&gt;
    &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same idea as the MCP server — wrap the existing API in a different package format to hit a different audience (engineering teams that care about keeping their PR history sounding human).&lt;/p&gt;

&lt;h2&gt;
  
  
  3-5. Three competitor SEO landings
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aitells.vercel.app/vs-zerogpt" rel="noopener noreferrer"&gt;aitells.vercel.app/vs-zerogpt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aitells.vercel.app/vs-quillbot" rel="noopener noreferrer"&gt;aitells.vercel.app/vs-quillbot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aitells.vercel.app/vs-undetectable-ai" rel="noopener noreferrer"&gt;aitells.vercel.app/vs-undetectable-ai&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one is a Next.js page with metadata + canonical URL + a shared &lt;code&gt;_vs/client.tsx&lt;/code&gt; component that does live detection on whatever text the user pastes. The differentiation argument is in the page copy. Templated, took ~30 min total for all three.&lt;/p&gt;

&lt;p&gt;People searching "zerogpt vs..." have buyer intent. That search has high commercial value for low effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta pattern
&lt;/h2&gt;

&lt;p&gt;I had one product (aitells.vercel.app). I now have 6 entry points to it: the web app, MCP server, GitHub Action, and 3 SEO landings. Plus the existing Dev.to articles and GitHub profile README and cross-links from other repos.&lt;/p&gt;

&lt;p&gt;The actual code under all of these is the same &lt;code&gt;/api/detect&lt;/code&gt; and &lt;code&gt;/api/rewrite&lt;/code&gt; endpoints. Everything else is &lt;em&gt;packaging&lt;/em&gt; the same core for a different audience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building takes hours. Packaging takes 30 minutes per format.&lt;/strong&gt; If your product solves a real problem (and AI text leaking through review pipelines is a real problem), the bottleneck isn't building more features — it's getting the existing thing in front of the audiences that don't know your URL.&lt;/p&gt;

&lt;p&gt;I also just published the &lt;a href="https://aitells.vercel.app/api-docs" rel="noopener noreferrer"&gt;public API docs&lt;/a&gt; so anyone can integrate the raw endpoints in their own product. The detector is free forever. The humanizer is $19 lifetime, first 100 buyers only, then $49/mo.&lt;/p&gt;

&lt;p&gt;If you want to use it as an MCP server: &lt;code&gt;npm install -g @perufitlife/aitells-mcp&lt;/code&gt; and follow the config snippet above.&lt;/p&gt;




&lt;p&gt;I also build &lt;a href="https://github.com/Perufitlife/supabase-security-skill" rel="noopener noreferrer"&gt;supabase-security&lt;/a&gt; and sister BaaS auditors. Same pattern: build the tool, then package it 6 ways. The pattern works.&lt;/p&gt;

</description>
      <category>indiehackers</category>
      <category>mcp</category>
      <category>githubactions</category>
      <category>seo</category>
    </item>
    <item>
      <title>I found 3 silent revenue-leaking bugs in my SaaS in 45 minutes (and the meta-lesson)</title>
      <dc:creator>Perufitlife</dc:creator>
      <pubDate>Tue, 12 May 2026 05:18:14 +0000</pubDate>
      <link>https://forem.com/perufitlife/i-found-3-silent-revenue-leaking-bugs-in-my-saas-in-45-minutes-and-the-meta-lesson-24f8</link>
      <guid>https://forem.com/perufitlife/i-found-3-silent-revenue-leaking-bugs-in-my-saas-in-45-minutes-and-the-meta-lesson-24f8</guid>
      <description>&lt;p&gt;I run a small aviation SaaS ($45 MRR, 3 paying customers, 75% churn). This morning I decided to actually look at my data instead of write more code. In 45 minutes I found three silent revenue-leaking bugs. Sharing each one because they're the kind of stuff every indie SaaS has and never notices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: cron silently returned 0 candidates for 6 weeks
&lt;/h2&gt;

&lt;p&gt;I had a winback cron (&lt;code&gt;/api/cron/winback&lt;/code&gt;) that emails canceled subscribers at day 7, 21, 45 after they cancel. The Supabase query:&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="kd"&gt;const&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="nx"&gt;subs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subscriptions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user_id, status, tier, current_period_end, updated_at, created_at, ...`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&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="s2"&gt;canceled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;updated_at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86400000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Problem: the column is &lt;code&gt;canceled_at&lt;/code&gt;, not &lt;code&gt;updated_at&lt;/code&gt;. The Supabase JS client returned &lt;code&gt;{ error: 'column does not exist' }&lt;/code&gt;, the cron caught it and quietly returned an empty array. Every day for weeks, "0 candidates, 0 emails sent, exit 0". No alarm.&lt;/p&gt;

&lt;p&gt;I caught it because I ran &lt;code&gt;SELECT COUNT(*) WHERE winback_pulses_sent IS NOT NULL&lt;/code&gt; and got 0 across 51 premium-tagged profiles. Then I read the cron source.&lt;/p&gt;

&lt;p&gt;Lesson: anywhere your code does &lt;code&gt;if (error) return []&lt;/code&gt;, log the error first. Silent failure is the worst failure mode in marketing automation because users don't bounce — they just never hear from you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: webhook never wrote the timestamp it was supposed to
&lt;/h2&gt;

&lt;p&gt;Same &lt;code&gt;subscriptions&lt;/code&gt; table. 14 rows with &lt;code&gt;status='canceled'&lt;/code&gt; and &lt;code&gt;canceled_at = NULL&lt;/code&gt;. Stripe knew when each cancellation happened, my webhook just never stored it.&lt;/p&gt;

&lt;p&gt;Looking at the webhook:&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;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;customer.subscription.deleted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabaseAdmin&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subscriptions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canceled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// ← no canceled_at here&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// ... later, inside a try block ...&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// ... other side effects ...&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabaseAdmin&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subscriptions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;cancellation_reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cancelReason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;cancellation_feedback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cancelFeedback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;canceled_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// swallow&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;canceled_at&lt;/code&gt; was only set inside a &lt;code&gt;try&lt;/code&gt; block alongside other side effects. If any of the side effects failed (which it apparently did, since 14/15 rows had NULL), the whole try aborted and &lt;code&gt;canceled_at&lt;/code&gt; never got written. The first &lt;code&gt;.update()&lt;/code&gt; was the only one guaranteed to run, and it didn't include &lt;code&gt;canceled_at&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fix: move the timestamp to the unconditional update.&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;canceled&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;canceled_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then backfilled 15 historical rows from Stripe's &lt;code&gt;canceled_at&lt;/code&gt; via the Stripe API + Supabase REST PATCH.&lt;/p&gt;

&lt;p&gt;This bug compounds the first one: even with the cron fixed, it would've found 0 candidates because the column it filtered on was NULL across all historical cancels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: filter that worked on day 1 broke as data accumulated
&lt;/h2&gt;

&lt;p&gt;Recovery cron for abandoned checkouts. Pulls Stripe sessions in &lt;code&gt;status='expired'&lt;/code&gt; from last 48h, then filters out anyone who "already paid". The filter:&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="kd"&gt;const&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="nx"&gt;activeProfiles&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabaseAdmin&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;profiles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email, subscription_tier&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;neq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subscription_tier&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="s2"&gt;free&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;Intent: "don't bother people who already converted". Implementation: filter by &lt;code&gt;subscription_tier&lt;/code&gt; column in &lt;code&gt;profiles&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Reality: 51 profiles in my DB had &lt;code&gt;subscription_tier = 'premium'&lt;/code&gt; from past subscriptions that had since canceled. The webhook bug above meant their tier wasn't downgraded to &lt;code&gt;free&lt;/code&gt; when they canceled. So when a new abandoned-checkout user shared an email with one of those 51 zombie profiles, the cron silently skipped them.&lt;/p&gt;

&lt;p&gt;Result: cron output said &lt;code&gt;{"total_abandoned": 11, "sent": 0, "skipped": 11}&lt;/code&gt; every day. Looks like the system is working ("no abandoned checkouts to recover today"), is actually broken.&lt;/p&gt;

&lt;p&gt;Fix: derive the "already paid" set from &lt;code&gt;subscriptions.status='active'&lt;/code&gt;, not from &lt;code&gt;profiles.subscription_tier&lt;/code&gt;. The subscriptions table is what Stripe writes, the profiles tier was a denormalized convenience that drifted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I shipped after finding all three
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Winback cron fix → 8 emails to real canceled customers immediately (1 of them at pulse 3 = 60% off final offer)&lt;/li&gt;
&lt;li&gt;Webhook fix → future cancels populate &lt;code&gt;canceled_at&lt;/code&gt; correctly&lt;/li&gt;
&lt;li&gt;Recovery cron fix → next run will pulse 11 abandoned-checkout emails in pipeline&lt;/li&gt;
&lt;li&gt;Manual final-shot to 5 maxed-out abandoned-checkout leads (60-cent customs in Resend)&lt;/li&gt;
&lt;li&gt;Backfilled 15 historical timestamps from Stripe API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total emails out from those 45 minutes: 14, all with &lt;code&gt;WINBACK60&lt;/code&gt; coupon (60% off 3 months). Worst case: nobody converts and I learned my data flow. Best case: even 2 conversions = +$30 MRR which is 66% growth on a $45 baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta-lesson
&lt;/h2&gt;

&lt;p&gt;I had been writing more features all week. Adding more cron jobs. More email templates. More variants. None of it would have helped because the actual pipes were leaking. &lt;strong&gt;Sometimes the highest-leverage 30 minutes you can spend is just SQL-querying your own database.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you run a small SaaS: try &lt;code&gt;SELECT status, COUNT(*) FROM subscriptions GROUP BY status&lt;/code&gt; and reconcile it against your Stripe dashboard. If those don't match, you have at least one of the three bugs I had.&lt;/p&gt;




&lt;p&gt;I also build &lt;a href="https://aitells.vercel.app" rel="noopener noreferrer"&gt;aitells.vercel.app&lt;/a&gt; (free AI text detector + paid humanizer) and &lt;a href="https://github.com/Perufitlife/supabase-security-skill" rel="noopener noreferrer"&gt;supabase-security&lt;/a&gt; (open source RLS auditor). Both built after I got bitten by the problem they solve. Same pattern.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>indiehackers</category>
      <category>postgres</category>
      <category>debugging</category>
    </item>
    <item>
      <title>How I shipped the rewriter side of an AI tell detector in 30 minutes (Claude + Next.js + Vercel)</title>
      <dc:creator>Perufitlife</dc:creator>
      <pubDate>Tue, 12 May 2026 03:12:19 +0000</pubDate>
      <link>https://forem.com/perufitlife/how-i-shipped-the-rewriter-side-of-an-ai-tell-detector-in-30-minutes-claude-nextjs-vercel-13g0</link>
      <guid>https://forem.com/perufitlife/how-i-shipped-the-rewriter-side-of-an-ai-tell-detector-in-30-minutes-claude-nextjs-vercel-13g0</guid>
      <description>&lt;p&gt;Yesterday I shipped a free detector for the 12 most reliable AI writing fingerprints. The story behind it: my reddit account got 2 public "all comments are AI generated" callouts in one day. Mods removed 3 posts. I was running everything through Claude. The em-dashes and "delve" gave me away in seconds.&lt;/p&gt;

&lt;p&gt;Detector traction was fine. 100+ scans day one, sat at 12 page views from a Dev.to post overnight. But every single piece of feedback I got was the same: "ok cool, now how do I fix it without rewriting by hand every time?"&lt;/p&gt;

&lt;p&gt;So I shipped the rewriter today.&lt;/p&gt;

&lt;p&gt;→ &lt;strong&gt;&lt;a href="https://aitells.vercel.app/rewrite" rel="noopener noreferrer"&gt;https://aitells.vercel.app/rewrite&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  what it does, technically
&lt;/h2&gt;

&lt;p&gt;You paste two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your AI-generated text (the thing you would post)&lt;/li&gt;
&lt;li&gt;1-3 writing samples of how you actually write (old reddit comments, tweets, emails)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The endpoint sends both to Claude with a strict system prompt that hard-bans em-dashes, "delve", "tapestry", "navigate the X", "in conclusion", "however,", parallel bullet structure, uniform sentence length, and 9 other patterns from the detector ruleset.&lt;/p&gt;

&lt;p&gt;The samples are critical. Without them you get generic "casual reddit voice" output which is fine but not yours. With them, you get sentence rhythm matched to how you actually type. Lowercase if you lowercase. Typos and fragments allowed if your samples have them. Slang preserved.&lt;/p&gt;

&lt;h2&gt;
  
  
  why it's different from "AI humanizer" tools
&lt;/h2&gt;

&lt;p&gt;Most of those just re-prompt GPT to "write more naturally". You end up with slightly different AI text. Same em-dashes, same "delve", same parallel bullets. They fail Reddit moderation the same way the original did.&lt;/p&gt;

&lt;p&gt;This one is built on top of the detector. The system prompt enumerates the exact patterns the detector flags. Output that still trips the detector defeats the purpose, so the constraints are explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  tech stack, since you asked
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 14 App Router, edge route for the detector, nodejs runtime for the rewriter (Anthropic API was blocking edge IPs)&lt;/li&gt;
&lt;li&gt;Claude Sonnet 4.6 via the messages API&lt;/li&gt;
&lt;li&gt;No database. localStorage tracks the free-tier counter. Email gets pushed to Resend for the list.&lt;/li&gt;
&lt;li&gt;Stripe Payment Link for the $19 lifetime tier. No webhooks yet, manual fulfillment until volume justifies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing is 3 files, deployed on Vercel hobby tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  what's broken / shipping next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No Stripe-gated unlimited path yet. After purchase I send the customer a permanent email token by hand. Will automate it once I see 5 sales.&lt;/li&gt;
&lt;li&gt;Detector and rewriter are decoupled. I want a single workflow: scan, see flags, click "rewrite this", get the output rescored. Shipping that this week.&lt;/li&gt;
&lt;li&gt;The detector rule set is closed for now. Will open-source once it stabilizes past v0.5.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  try it
&lt;/h2&gt;

&lt;p&gt;If you ship AI-generated reddit comments, cold emails, tweets, or LinkedIn posts and your engagement is mysteriously bad, paste your last 3 outputs into the detector. The score is real and the highlighted patterns are reproducible.&lt;/p&gt;

&lt;p&gt;If they all score below 60, you need the rewriter.&lt;/p&gt;

&lt;p&gt;aitells.vercel.app/rewrite&lt;/p&gt;

&lt;p&gt;Free first rewrite. $19 lifetime if you want it forever. First 100 buyers only, then it goes to $49/mo.&lt;/p&gt;




&lt;p&gt;Built by &lt;a href="https://github.com/Perufitlife" rel="noopener noreferrer"&gt;@Perufitlife&lt;/a&gt;. Also shipped a &lt;a href="https://perufitlife.github.io/supabase-security-skill/" rel="noopener noreferrer"&gt;Supabase security auditor&lt;/a&gt; recently after finding 14 critical leaks in my own CRM. Same pattern: build the thing you wish existed when you got bitten.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>nextjs</category>
      <category>claude</category>
      <category>indiehackers</category>
    </item>
  </channel>
</rss>
