DEV Community

Cover image for My SaaS runs on static HTML. The build was the easy part.
PingClass
PingClass

Posted on

My SaaS runs on static HTML. The build was the easy part.

Honestly,
When I tell people PingClass has no frontend framework — no React, no build step, just .html files — the first question is always about the stack. "Doesn't that get unmanageable?"

Honestly? The rendering layer has never been the hard part. I launched on static HTML plus Supabase, and then real tuition centers started using it. Everything that got hard was underneath: money, automation, and the database boundary.

This is what shipping a no-framework SaaS actually looked like after launch.

The stack that never changed

Still static HTML + vanilla JS on Vercel, still one Supabase project (Postgres + Auth), still a handful of serverless functions for the dynamic bits. In months of adding payments, reminders, notifications and scheduled jobs, I have not once thought "this needs a framework." Every page is a real .html file, so Googlebot, ClaudeBot and PerplexityBot read it natively — that part was solved on day one and stayed solved.

The interesting work was the plumbing.

Rendering: solved, don't touch it

Briefly, because it's the least interesting now: marketing pages are static, dashboards are static pages that talk to the database directly, and dynamic content like approved reviews is also rendered statically on the homepage so crawlers see it. Scripting then skips what's already in the HTML. Rating data is baked in as JSON-LD, llms.txt tells AI crawlers what's what, and IndexNow pings Bing on every deploy.

None of that has needed maintenance. It just works, which is the point.

Money is a state machine, not a button

The moment I added payments, the naive version stopped being okay. A "Pay" button is easy. What's hard is that money-as-a-state can be wrong in ways a normal bug can't:

  • Orders are created server-side, never on the client, so the amount can't be tampered with.
  • The success callback is verified server-side against a signature — the browser claiming "paid" means nothing.
  • Payment is idempotent: the same event arriving twice must not create two subscriptions.
  • Every state change is recorded so it can be reconciled later.

I stopped thinking of "subscribe" as an endpoint and started treating it as a state machine with a verified transition. That mindset shift mattered more than any library.

The automation is what makes it feel like a product

A dashboard alone is just a nicer spreadsheet. What actually earned the subscription was the stuff owners never have to remember:

  • Dues auto-generate per student × batch for the current period, with a duplicate guard and a nightly scheduled job.
  • Payment reminders, escalation, and receipts go out by email and push.
  • Attendance produces absence alerts and a daily reminder, with dedupe so nobody gets pinged twice.
  • Plan-expiry notices, invites, and announcements all fire themselves.

All of it is scheduled functions and managed Postgres jobs. No queue, no worker fleet, no scheduler SaaS.

Error reporting without a vendor

I didn't want to add a third-party error tracker as yet another dependency, so I built the small version I needed. Every page loads a tiny script that prints a light developer greeting and captures uncaught errors and unhandled rejections. Those reports go to my own endpoint, which merges duplicate crashes so I see distinct problems instead of a flood.

Little, dependency-free, and good enough — which is the recurring theme.

Security lives in the live database, not your migrations

The scariest issue I've dealt with wasn't in my application code. It was in database permissions.

Here's the part I wish someone had told me earlier: the effective privileges in a system live in the live database, not in your migration files. Your history can look perfectly locked-down while the actual, running database has drifted away from it. A revoke you rely on can be quietly undone by some later, unrelated change — and staring at git log will never reveal it.

So I stopped reasoning from the files and started asking the database itself. Now, after any change that touches permissions, I verify against the live system directly: which objects are reachable by which roles, and whether that matches intent. If the live answer disagrees with the files, the live answer wins.

That one principle has done more for my sleep than any framework ever did, and it costs nothing but discipline.

Backups, or you don't have a business

Once institutes keep attendance and fee records in your system, "did the backup run?" becomes existential. So snapshots run automatically, and something alerts me if one didn't. There's a retention policy and self-serve account deletion to go with it.

Backups you have to remember to check aren't backups. They're a liability with extra steps.

What I deliberately did not add

  • A frontend framework. The static pages have had no build-step problems because there's no build.
  • A queue / worker system. Scheduled functions and managed Postgres jobs cover every background task I have.
  • A third-party error tracker. The small in-house version does exactly what I need.
  • A separate "app." It's a PWA with a service worker and push notifications. Close enough for now.

The meta-lesson

Spend your complexity budget on the database boundary — permissions, payments, backups — and keep the rendering layer boring. Static HTML plus a managed Postgres plus a CDN has carried this further than I expected, at near-zero cost, and it made the "will people find it?" problem trivial because every page is HTML that search engines and LLMs read natively.

The framework was never going to be what made this work. The unglamorous stuff did.

PingClass is live — free up to 20 students, then ₹249/mo and ₹599/mo, also on Product Hunt and G2. Happy to go deep on the money state machine, the static-HTML approach, or the tuition-center niche.

Top comments (3)

Collapse
 
pingclass_off profile image
PingClass •

Hi, I'm Godwin, the maker. I didn't start from a plan — I started from watching tuition centers nearby run on paper registers and WhatsApp groups. Happy to go deep on the static-HTML approach, the payments state machine, or the live-database security lesson. Thanks for reading!

Collapse
 
posting-dude profile image
Posting Dude •

The point that effective privileges live in the live database and not in the migration files is easy to forget. How do you check the live state now, a scheduled query against the catalog or something you run after each migration. Curious whether that held up once more than one person was touching the database.

Collapse
 
pingclass_off profile image
PingClass •

Great question. Right now it's a run-on-demand post-migration script, not a scheduled job — it queries the live DB (has_function_privilege, prosecdef scan, table-privilege allow-list), never the migration files, because a later broad grant can silently undo an earlier revoke. It holds up with multiple people since it trusts the catalog, not the author; more contributors would just move it into CI as a merge gate.

How do you handle it — cron diff, or a pipeline assertion?