DEV Community

Cover image for I asked ChatGPT for free status page tools. It didn't mention mine. Here's the Next.js + Supabase build behind that zero.
Hao
Hao

Posted on • Originally published at statuspagebuddy.com

I asked ChatGPT for free status page tools. It didn't mention mine. Here's the Next.js + Supabase build behind that zero.

Twenty-four hours after I swapped Supabase Auth's default SMTP for Resend, my first real user signed up. I'll call him K. I emailed him three questions. He replied in 49 minutes: "make a better ui of admin so we can use." That sentence is now my Q2 roadmap. Three weeks into launch I have 3 users, 2 Google clicks, and zero mentions when you ask ChatGPT or Gemini for "free status page alternatives." This is what I shipped, and where I'm still wrong.

Why I built another status page (when there are already 15)

I launched EurekaWrite last year. Every time I deployed, users DM'd me the same question: "is it down?" I'd check, reply, repeat. After the tenth round I wanted a public URL I could send instead of typing the same reply. So I looked at what existed.

Statuspage.io is $29/month for a page that says "All Systems Operational." That's $348 a year for one indie project that earns nothing yet. Skip.

Upptime is free and open source. I tried it. Three things to learn before I get a public URL: a YAML monitor schema, a GitHub Actions workflow file, and the repo-as-config mental model where every status update is a commit. For a side project I want to ship in an evening, that is two evenings of yak-shaving I do not want.

So I built StatusPageBuddy. The pitch on the landing page is one line: "Skip the YAML, the GitHub Actions, and the $29/month bill." Type a name. Get a link. Share it. 60 seconds, free forever.

One scope decision worth naming: SPB is the public-facing page, not the prober. It is not a monitoring tool. That is deliberate. The trade-off buys 60-second onboarding: no built-in HTTP checks, no alerting rules engine, no integrations tab. Those are the three things a monitoring product would ship on day one and the three things SPB deliberately does not.

StatusPageBuddy landing hero — 60 seconds, free forever

The Supabase default that cost me a month of signups

The stack that gets to 60 seconds is small on purpose.

The stack:

  • Next.js 16.2.1 + React 19 on Vercel
  • Supabase for Auth, Postgres, and RLS
  • Resend for transactional email

The part that cost me a month was a default I never thought to question.

Supabase Auth ships with a built-in SMTP sender for confirmation emails. It is rate-limited to 3-4 messages per hour on the free tier, and the From address is on a shared Supabase domain. That combination did two things. It throttled my signup flow during the only window people were trying it, and it routed the rest to spam. I watched signups start the flow and never come back, and I assumed the product was the problem.

I caught it by accident. I created a fresh test account from a Gmail address I never use, watched the email never arrive, checked spam, and there it was. I sent another. Same path. Then I read the Supabase docs page on production SMTP and found the line I had skimmed past during setup: the built-in sender is for development, not production.

The fix took 20 minutes:

  1. Create a Resend account
  2. Verify a sending domain (one DNS record for SPF, one for DKIM)
  3. Paste the Resend SMTP credentials into Supabase Auth → SMTP Settings
  4. Flip the toggle from default to custom
  5. Send a test signup, watch it land in inbox not spam

Twenty-four hours later, K. signed up. I had been live for almost a month.

If you are on Supabase and you have not swapped the default SMTP, swap it first. It is not a scaling concern. It is a "your funnel is closed and you do not know it" concern.

Supabase Auth SMTP settings — custom SMTP enabled via Resend

Now the architecture piece. The SMTP fix reopened the funnel; RLS is what lets me sleep at night with multi-tenant data in one Postgres database.

SPB is multi-tenant: a user can belong to multiple orgs via an org_members join table, and every status_pages row is scoped to an org_id. The entire authorization layer for that table is one policy:

-- status_pages: a user sees only the pages of orgs they belong to
create policy "org_isolation" on status_pages
  for all using (
    org_id in (
      select org_id from org_members where user_id = auth.uid()
    )
  );
Enter fullscreen mode Exit fullscreen mode

Every read, write, update, delete from any client goes through that filter. The route still exists (Next.js RSC fetch, API handler, whatever), but the route can't leak rows: the query just returns zero. The counterfactual is the if (user.org_id !== page.org_id) throw 403 guard you'd otherwise sprinkle through every API handler and forget in exactly one of them. That forgotten one is the breach. RLS deletes the category.

Three weeks of cold outreach, by the numbers

Architecture closes the breach. It does not bring users. For that I had a spreadsheet and three weeks.

Week 1: 17 cold touches, 0 signups. Comments on indie-dev threads, replies in Supabase Discord, two PRs to awesome-* lists, one Twitter post from a 40-follower account. The Twitter post drew 2 impressions. That is the expected ceiling for a cold account with no graph, and it confirmed what I already suspected: Twitter is not a channel for me yet, it is a vanity surface I should stop touching until I have a reason to be there.

Week 2: 8 cold touches, 3 signups. Fewer touches, tighter targeting — I dropped the broad "any indie thread" comments and only posted in places where someone had already named the problem ("what status page should I use," "alternatives to Statuspage.io"). The signups landed as K. on 4/25, R. on 4/28, M. on 5/1.

The read on attribution: it is asset accumulation, not single-touch. The Supabase Discord #showcase post went up 4/22. K. signed up 4/25. Three days, not three minutes. The awesome-status-pages PR (#194) merged 4/19, before W1 even started counting, and it almost certainly fed R. and M. later through SEO. Neither of them came from anything I did the day they signed up. They came from work that had been sitting on someone else's page for a week or two.

The pattern: ecosystem PRs (awesome-status-pages #194, plus a second PR to awesome-selfhosted queued for review) and GitHub Issues where someone had already typed the buying question. Twitter stays off the list until I have a reason to be there.

Outreach tracker — Week 1 (17 touches, 0 signups) vs Week 2 (8 touches, 3 signups)

The first-user moment — why feedback in under 50 minutes changed the roadmap

I gave K. 24 hours to poke around, then sent three questions over email. No marketing, no "we'd love your thoughts," signed "Hao."

He replied in 49 minutes:

"make a better ui of admin so we can use"

That is the verbatim reply. One sentence, no punctuation, no hedge.

I had been planning a 12-question survey. I scrapped it. One sentence from a real user beats a survey because it tells you which bottleneck you are actually staring at. I had been optimizing acquisition — outreach lists, PR titles, Discord threads. K. told me the wall was activation, and inside activation it was the admin UI specifically. He had signed up. He had gotten in. He could not get out the other side with a working page.

If you want more posts like this — same numbers, smaller doses, sent Sundays — subscribe at statuspagebuddy.com/subscribe. One email a week.

The follow-up I sent the next day, 4/28, did one thing: it forced a choice. "Which screen is the worst right now: components, incidents, or settings? Send a screenshot of the one that frustrated you most." Three options, one ask, one attachment. That is the shape that turned a free-form sentence into shippable scope. The trap with "tell me more" is that the user has to do the structuring work for you, and they will not. Give them the menu.

The reusable shape of the email, in case it helps:

  1. Three questions, max.
  2. One of them forces a choice between named options you already know exist.
  3. One asks for an artifact (screenshot, link, snippet) so the reply has weight.
  4. Sign with your first name, not the company.

The funnel gap — when your spec is the bug

K. told me activation was broken. R. showed me the data side of the same wall.

R. signed up 4/28. He created an org. He never built a page. The status_pages table for his org_id stayed empty.

I had a cron job that was supposed to catch exactly this case. It runs nightly, finds users who signed up more than 48 hours ago and have not "activated," and queues a reminder email. It never fired for R.

The reason is embarrassing in the way good bugs always are. The cron's predicate for "activated" was has_org. R. had an org. The query, correctly, returned zero rows. The code did exactly what it was told. The instruction was wrong.

This is not a bug. The compiler did not lie. The database did not corrupt. The spec was wrong, and the spec was wrong because at the time I wrote it I had no users, and "create an org" felt like the meaningful step. Of course it did. It was the last step in the onboarding flow I had built. From the inside it looks like the finish line. From the user's side, the finish line is a published page someone else can load in a browser.

Fix: redefine activation as has_at_least_one_published_page. Re-fire the reminder cron with the new predicate. The two clauses, side by side:

-- old: activation = "user has at least one org"
where not exists (select 1 from org_members where user_id = u.id)

-- new: activation = "user has at least one published page"
where not exists (
  select 1 from status_pages sp
  join org_members om on om.org_id = sp.org_id
  where om.user_id = u.id
)
Enter fullscreen mode Exit fullscreen mode

R. now sits in the queue for tonight's run.

The takeaway I want to carry into the next product I build:

Your activation predicate is a product decision disguised as a SQL clause.

The schema makes one predicate cheap to query. The user behavior you actually care about is almost always a different one. Anywhere those two diverge, your dashboards look fine and your funnel stays empty. Mine was off by one join.

What ChatGPT and Gemini say about my product (nothing)

Schema fixes are inside-the-box work. The next beat is whether anyone outside the box can find SPB at all.

On 5/2 I ran the check I had been avoiding. ChatGPT first: "free statuspage alternatives for indie developers." Then the same prompt into Gemini.

Neither mentioned StatusPageBuddy. Baseline = 0.

Both named the obvious incumbents — Statuspage, Instatus, Better Stack, Upptime, a couple of self-hosted Docker projects. I tried a second prompt ("cheap status page for a side project, not Statuspage.io"). Same result.

This is the data point most build-in-public posts skip, because a zero is embarrassing in a way a low number is not. A low number reads like progress. A zero reads like absence. But LLMs are now a real discovery surface. When an indie dev asks for a tool today, a meaningful share of them ask a model first and Google second. A zero baseline is where the work begins.

What I'm doing about it, in three lanes:

  1. Ecosystem PRs to awesome-* lists for permanent backlinks that crawlers and training pipelines both eventually find. Next on the queue: awesome-selfhosted and awesome-sre.
  2. One weekly long-form post (this is the first) on dev.to and IndieHackers, canonical on the SPB blog.
  3. One Beehiiv issue per week, short and numbers-forward. First issue title: "Week 3: 3 users, 2 Google clicks, 0 AI mentions."

One PR, one post, one issue per week. I picked twelve weeks because that's one full GSC reporting window plus a buffer — long enough that "no movement" means the strategy is wrong, short enough that I'll still care. The lane I expect to fail first is the awesome-list PRs: most of those repos are unmaintained, and a merged PR is not a guaranteed crawl.

ChatGPT and Gemini answers to

SEO baseline — the search numbers nobody publishes

The older surface is search. Vercel Analytics, week of 4/25–5/2: 28 unique visitors, down 45% week-over-week. 120 page views. 68% bounce rate.

External referrers across the whole week, all sources combined: Google 2, Bing 1, GitHub 1. Four real external clicks in seven days. Everything else was direct or me.

Search Console, three-month rolling total: 2 clicks, 13 impressions, 15.4% CTR, average position 2.6.

Read that last line again. Average position 2.6. When SPB shows up at all, it ranks third on the page. The CTR is fine. The ranking is fine. The problem is that SPB shows up on thirteen queries in three months, total. Nobody is typing the queries I'd rank for, because the queries I'd rank for are queries nobody types.

The takeaway is uncomfortable: SEO ranking is not the bottleneck for a product like this in week three. Indexed surface area is. There are not enough pages on the open web that mention SPB for Google to surface, and not enough pages that mention SPB inside any LLM's training cut for it to recall the name. That is the same problem the AI baseline points at, from the other side. Both fixes look like the same fix.

This is roughly what week three looks like for any indie product without an existing audience. The interesting question is not whether the numbers are small. The interesting question is what the curve does between week three and week twelve.

Search Console 3-month summary — 2 clicks, 13 impressions, 15.4% CTR, position 2.6

What I'm shipping next

Three things that move the curve, in order.

First, the admin UX overhaul K. asked for. Broken into three sub-tasks I can ship one per week: components screen, incidents screen, settings screen. The components screen is first because that is the one he sent the screenshot of.

Second, the activation predicate fix from the funnel-gap section. Ship the new predicate, re-fire the reminder cron with R. in the queue, then leave it running so any future "org-only" user gets caught the same night.

Third, a one-click "create demo page" on first org-create. The funnel gap was a missing reminder, but the deeper gap was that creating a page felt like a separate decision after creating an org. If the org-create handler also writes a draft page row, the activation predicate flips on the same click. That kills the same-shaped gap at the source instead of catching it on a cron 48 hours later.

What would make the next post worth writing, as a threshold, not a scorecard: a non-zero AI mention, a second activated user, and the first 5 newsletter subs. If none of those move, I'll publish the same skeleton with the same zeros and write about why none of them moved. That is also a post.


The newsletter is the main thing I'm asking for. Subscribe at statuspagebuddy.com/subscribe. This Sunday: the W3 outreach data, whether the admin UX rewrite moved activation, and whether anything I shipped this week showed up in ChatGPT. One email a week. Hit reply.

If you actually need a status page, statuspagebuddy.com is free forever for indie projects. Type a name, share a link. If something breaks, email me — same Hao.

Top comments (0)