Three weeks ago I got an email that made my stomach drop: a user asking why they could see another customer's data in the admin panel of a side project I'd just launched.
Turned out the admin route was checking if you were logged in, not if you were actually an admin. Any authenticated user could hit the URL directly and see everything. Nobody exploited it maliciously, the user who found it was just confused and honest enough to email me instead of screenshotting it. I got lucky.
This was my fourth SaaS side project. Every single one had hit some version of the same wall: auth, billing, admin, teams, all rebuilt slightly differently each time, all slightly buggy in new and exciting ways because I was moving fast and reinventing basics I'd already gotten right (and wrong) three times before.
After that email, I stopped. Spent real time hardening the boring stuff properly instead of racing to the feature that actually made this product different: role-based access done right, webhook signature verification, rate limiting, the works. Then packaged it so I never have to rebuild that foundation from zero again — that became ShipKit (shipkit.full-optimization.com), a Next.js boilerplate with the plumbing already stress-tested.
Not going to pretend this is some huge launch, it's a small one-time-purchase thing, $99 to start. But I'm curious: for the other builders here who've shipped multiple products, was there a specific moment (a bug, an email, a 2am panic) that made you stop winging the infrastructure layer? Do you rebuild it fresh every time, or have you built your own reusable base?
Mine was an export endpoint - the page was auth-gated, the CSV URL wasn't. What fixed it for me was moving the ownership check down into the data layer, so every query is tenant-scoped by default; per-route checks are where the one you forgot hides. I still rebuild the base, but now I start from the model layer.
That's basically our admin panel story with a different URL. per-route checks scale badly exactly because you have to remember to add them everywhere, forever, on every new route. moving it into the data layer means the default is safe and you have to opt out instead of opt in. that's the fix we're planning too, this thread's basically talked me into doing it sooner rather than later.
That incident is a painful but useful forcing function. For a small SaaS, I’d make tenant ownership a default invariant rather than a checklist: every data-access path accepts tenant/user context, authorization tests cover cross-tenant reads and writes, and staging has two tenants so a missing filter fails loudly. A short pre-launch “is this user allowed to access this object?” pass across admin, exports, billing, and background jobs catches the scary edge cases better than only reviewing the UI. The one-time, opinionated scope also makes sense—rebuilding auth is expensive, but adopting a narrow, trusted foundation is cheaper than a breach.
The two-tenant staging idea is the part I'm stealing. testing with one tenant, you never actually exercise the isolation logic, everything just happens to work because there's nothing else in the database to leak. with two you'd catch a missing filter on day one instead of after a customer emails you about it.
My moment was less dramatic but just as scary. Before launching a multi-tenant app I ran a structured security review and it found two things I'd never have caught by clicking around:
The lesson I kept: put tenant isolation in the database (Postgres row-level security on every table) so a sloppy route can't leak another customer's rows, and don't forget background jobs, which quietly see nothing under RLS unless you set the context for them.
Appreciate the writeup, especially the allowlist point, that's a mass assignment bug and it's an easy one to miss since everything "looks" scoped correctly in the UI.
honest answer: we don't have RLS on the tables right now, isolation is enforced at the query layer (every read/write scoped by org membership in the app code). it works, but it's exactly the kind of thing where one sloppy route slips through and suddenly doesn't. moving critical tables to RLS is on my list now, less because of a specific incident and more because "the app layer always gets it right" is a bet I'd rather not keep making.
the background jobs point is new to me though, didn't know RLS just goes silent under a different execution context instead of erroring. that seems like the kind of failure you don't notice until someone asks why a report is empty.
The security incident makes the pain unusually concrete. Have other repeat SaaS builders actually chosen ShipKit instead of reusing their own boilerplate, and what made the $99 purchase worth switching?
honest answer, not yet — I only launched this week so there's no track record of people switching over from an already-established base. the pitch is really aimed at someone closer to where I was a few months back: two or three products in, still rebuilding auth/billing/webhooks slightly differently each time, hasn't gotten around to packaging their own base into something reusable.
if you already have a battle-tested base of your own that you're happy with, honestly the $99 probably isn't worth it for you, you're past the problem this solves. it's more for the version of me before I stopped and did this properly.
That’s a clear ICP boundary, especially the point where rebuilding stops being worth it. If you’re open to it, what’s the best email to reach you on?
thanks for asking — support@full-optimization.com works great, I actually read and reply to that one myself. looking forward to hearing what's on your mind
Thanks! I’ve just sent it over.
Looking forward to hearing your thoughts whenever you have a chance.
got it, thanks — will take a proper look and get back to you soon.