
Stria
Catch scope creep before it becomes free work.
382 migrations. Most of the rules that matter aren't in my app code at all — they're triggers, constraints, RLS policies and SQL functions, tested with pgTAP.
The reasoning is uncomfortable but simple: anything enforced in the client is a suggestion. My product sells tamper-evident approval records. If a sign-off's immutability lives in a React component, it isn't immutable, it's polite.
So the update and delete triggers on a completed sign-off refuse for everyone, table owner included. Deposit gating withholds deliverable files at the point where the signed download URL is minted, not in the UI, and it fails closed on error. Seat limits are a trigger on the members table, not a check in a hook.
That pushes testing down with it. Vitest covers 531 files' worth of app logic. The rules live in pgTAP suites that run against a real Postgres in CI, because a mocked trigger proves nothing.
Two things I learned the hard way.
A test suite can be dark. I used mnemonic UUIDs in fixtures for readability. A UUID is hex, and letters like n, y, i and p aren't. Postgres rejected the insert — but the insert was in the fixtures, so the suite aborted before its first assertion and reported no failure and no result. 24 suites were in that state. Not randomly distributed: milestone billing, currency consolidation, the dunning ladder, money webhook events. The features people adopt the product for were the ones with no executing coverage. 125 literals repaired, and the gate now also asserts every suite declares a plan and calls finish(), so a suite can't ship unable to fail.
Global coverage numbers lie in a specific direction. My repo-wide floor is 22% lines, which is honest for a codebase dominated by page components and useless for lateFee.ts. An average lets a 100%-covered tax module and a 0%-covered page swap places without moving the number. So the arithmetic that lands on an invoice has per-module floors — invoice composition, tax, late fees, deposits, retainers, receivables — and the check also fails when a new money-shaped module appears in src/lib with no floor, because a hand-maintained list of important files rots by addition.
React 18 + Vite 7 + TypeScript + Tailwind, Supabase for Postgres and auth, Edge Functions on Deno. That's the whole thing.
What's deliberately absent, and what replaced it:
No component library. Tailwind plus a small set of local primitives. The tradeoff is real: I hand-rolled focus management and I've paid for it. What I got back is that a contrast gate can prove the four ink steps are correct, and a second check bans opacity-modified text colours like
text-charcoal/40— which rendered at 2.40:1, under even the 3:1 large-text floor. Alpha applied at the call site is invisible to a token-level check by construction. You can't write that guard against someone else's theme system.No state manager. TanStack Query is the cache; server state is the only state that matters here. Nothing has needed Redux or Zustand.
No ORM. Postgres functions called as RPCs. The cost is that Supabase takes an RPC name as a free string, so a typo compiles and fails at runtime as a 404. So there's a coverage ratchet on it: of the 246 RPC names the browser reaches, 46 had neither generated types nor a contract. The gate caps that list and fails when a 47th appears.
No form library, no date library.
Two honest concessions. pdfjs-dist and pptxgenjs are heavy, and they're there because clients get PDFs and decks. And there are about 40 devDependencies, so this is a claim about what ships to a browser, not about my node_modules.
The npm audit gate is the one place I'd push back on conventional advice. A bare audit gate is unusable the first time an advisory ships with no patched version: it goes permanently red, and npm audit fix --force "resolves" it by downgrading you into older CVEs. Mine fails on any unreviewed moderate-or-higher advisory, and also fails when a written exception expires or outlives its advisory.
Lean deps aren't a virtue on their own. What they buy is the ability to write checks about your own code, because you own the layer the check would inspect.
1 Like
Comment
Not linting. Not types. Twenty bespoke check-*.mjs scripts, each written the day after something broke in a way no normal test could have caught. The pattern is always the same: a decision recorded in prose, and a call site that reads fine.
A sample, all real:
A cron schedule inside a block comment. The retainer billing job was scheduled in SQL wrapped in /* */. It never ran. Meanwhile the in-app automation health panel advertised the job as active, with the impact line "Retainers will not bill automatically." So a freelancer set up a monthly retainer and it silently never invoiced. The part that makes this a guard rather than a fix: grep cron.schedule finds the commented-out version and looks reassuring. A machine has to tell executed SQL from documented SQL.
A Postgres COMMENT nobody could read. A migration retired a column and documented it perfectly: "read by nothing. Do not add a reader." Two readers were added anyway, months apart. Both call sites look completely ordinary — client.deal_value is a real field on a real type — while the knowledge that it's dead lives 300 lines into a SQL file nobody opens. One of the two silently forecast zero pipeline in the 90-day cash-flow panel on every workspace. So now no file under src/ may read a column whose COMMENT says DEAD.
An HTML entity inside a JS string. A page shipped 'You’re confirmed' inside a ternary. React escapes it, so the screen a buyer sees immediately after agreeing to pay a deposit read the markup out loud. In JSX text those same nine characters are correct. That's what makes it a defect class instead of a typo: the distinction is invisible while typing and invisible in review.
An endpoint documented nowhere. A token-authenticated ingest function shipped live and appeared in no OpenAPI spec. That's OWASP API9:2023, not a docs nit — an undocumented endpoint gets no contract test, no review, and can't be deprecated on purpose.
The meta-lesson I keep relearning: a comment addressed to a future human is decoration. If a decision matters, something has to read it.
1 Like
Comment
I see a lot of posts on Reddit and other forums about solo founders making all these cool products, which is great, but I feel like there is not enough coverage of the inverse of those scenarios. The late nights cramming in another feature, or writing up listing packages for Product Hunt and every other directory on the face of the Earth. I got into this business to solve problems and I started building Stria to solve problems that I have faced in the past and that I know there are lots of people currently facing them as well. I never realized how lonely the journey can be sometimes and how you can be your own harshest critic.
If Stria becomes what I truly believe it can be, then at that point it will all have been worth it, but until then I guess I'll just take it one day at a time. I know I'm not the only one feeling this way so maybe that's an app idea in of itself....a social network for lonely solo founders...just kidding!
1 Like
Comment
I have spent a lot of time building focused Windows utilities and selling them through the Microsoft Store under Automata Labs.
That experience taught me how to find narrow problems, ship a reliable solution, make a listing understandable, support users, and keep improving an app after launch. It has generated revenue.
But building a SaaS is a different challenge.
With a small Windows utility, the job is often: identify a concrete annoyance someone is already searching for, fix it cleanly, show the result in screenshots, and earn enough trust for a one-time purchase.
With SaaS, I need to earn recurring trust. I need a user to believe the product belongs in their workflow every week, not merely that it solves a one-off problem.
My current attempt is Stria, a client-work platform for freelancers, independent consultants, and small studios.
The entry point is deliberately narrow: scope creep often arrives as a client email—“Could you just add…”, “While you are in there…”, or “Can we squeeze in one more revision?” The freelancer has to decide, often quickly, whether the request belongs in the original agreement or needs a revised price, timeline, and approval.
I am building an AI-assisted workflow where a freelancer forwards that request, compares it to the approved scope, and gets an evidence-based starting point: likely in scope, likely out of scope, or unclear and requiring review. The broader product also handles the surrounding workflow: proposals, approvals, projects, invoices, and payments.
I recently launched and the honest starting numbers are not glamorous: I have put the product on the major directories, written initial content, and posted in relevant communities—but I am still trying to earn the first activated users without buying ads.
That is what I am going to document here:
Which free distribution experiments create qualified visits rather than vanity traffic
Whether freelancers trust an AI-assisted scope check enough to use it before replying to clients
Whether “scope creep prevention” is a strong enough wedge to introduce a broader freelancer operating system
Which features matter once someone has actually signed up
My working assumption is that the product does not need to be everything to everyone on day one. It needs to solve one expensive, repeated moment so clearly that a freelancer thinks: “I wish I had checked this before I said yes.”
Question for other founders: if you have moved from paid one-time software to SaaS, what most surprised you about acquiring and retaining the first users?
I’ll share the experiments—especially the ones that fail—as I run them. Stria is at https://getstria.com.
1 Like
Comment
A few years of freelance/contract work taught me something annoying: the actual work was fine, but the business layer was chaos. Scope lived in email threads. Approvals were "did they ever reply to that?" Invoices were a separate tool I'd update from memory at month-end. I lost money twice to scope creep I couldn't prove.
So I built Stria — one system that runs a client engagement from scope to payment: clients, projects, deliverables, client feedback/approvals, invoicing, and payments via Stripe Connect. The client gets a feedback loop built in, so approvals are timestamped instead of buried in email.
About
I got burned a few times by scope creep and got tired of absorbing costs instead of locking in the project scope and requiring change orders.


3 Comments
Yes, and you've named the distinction I'd defend: coverage is a percentage, executability is a per-suite fact, and the second can regress while the first improves.
Two layers now. The 22% is a deliberately dumb global ratchet dominated by page components and generated types, and its only job is to stop the whole repo sliding. I don't treat it as information about any individual module. The real gates are per-module floors on the money path, generated from a single manifest that both the Vitest config and a separate post-hoc check read, so the runner and the auditor can't disagree about what's gated or at what level. That second check also fails when a new money-shaped module shows up with no floor at all, which is the case a threshold can't express.
Executability is enforced separately and earlier. Before the database even starts, CI verifies a named allowlist of 13 pgTAP suites is present: sign-off flow, Stripe webhooks, org_id immutability, dunning ladder, invoice idempotency, version-hash canonicalisation. A missing file fails the build on its own error. The suite is auto-discovered rather than registered, which kills the commonest dark vector: there's no list anyone can forget to add to. 281 SQL suites, and zero skipped or
.onlytests in the TS suite.The edge I haven't closed: presence isn't assertion. A hollowed-out suite with a dropped
plan()count would still pass that check. Pinning expected assertion counts per gating suite is the obvious next move.