I moved between three codebases yesterday -- my own site, The Parliament, and WeCos -- and by evening I realised I had spent the whole day fixing the same bug three times. Different stacks, different features, same shape: something checked a permission, decided no, and then said nothing at all.
The counter that only counted for me
On my own community feed, bookmark counts were showing 0 on every post I hadn't written. Bookmark my own note, count goes to 1. Bookmark someone else's, it stays 0. No error anywhere.
I first chased the wrong thing. The count was also flashing -1 on the bookmarks page, and that one I understood immediately -- the DB counters floor at zero, so a post with two bookmarkers can read 0 while you still hold a row, and un-bookmarking subtracts from nothing. I floored every decrement in the optimistic reducer and moved on, pleased with myself. Fixed the symptom. Left the disease.
The disease: the trigger function that recalculates bookmark_count was declared without SECURITY DEFINER, unlike its siblings for votes, replies and reblogs. So it runs as you. And the row-level security policy on posts only lets the owner update a post. You bookmark my note, the bookmark row inserts fine, the trigger fires its UPDATE against my row, RLS matches zero rows, and that is not an error. Zero rows updated is a perfectly legal outcome. The trigger returns happy. Nothing logs.
The tell was that self-bookmarks worked. A counting bug is wrong everywhere. This was wrong in exactly the shape of the permission boundary.
The same silence, twice more
On The Parliament, signup was failing on the deployed site and returning nothing useful. I ended up shipping a temporary commit that surfaced the raw DB error to the browser just to see it, then reverted it the same hour and replaced it with proper server-side logging. Not proud of the debug-leak commit sitting in that history, but I could not fix what I could not see. Half an hour later the same site handed me the purer version of the bug: middleware auth was reading the wrong cookie over HTTPS. On Vercel the session JWT lives in __Secure-authjs.session-token, and the token check was not told to look there, so it read null and bounced logged-in members back to sign-in. Correct code, correct config, wrong cookie name, and no error at any point.
On WeCos, signup was blocked behind a confirmation email that was never going to arrive, and separately, returning users signing in were being bounced back into onboarding as if they were brand new. Both were the same failure mode again: a gate evaluating to no, and the UI showing a plausible screen instead of an error.
Both networks grew a paywall the same day
The odd part is that the bugs were the small half of the day. Underneath them, The Parliament and WeCos were both building the exact same thing at the same time, and I did not plan that.
The Parliament went from a flat free membership to four tiers -- Student as the free default, then Associate, Premium and Life, with Committee and Inactive kept internal so they never show up as something you can buy. Then the gates: posting a job opening needs Associate or above, listing a business or applying as a mentor needs the right tier, and anyone below sees an upgrade prompt pointing at exactly the next rung rather than a generic wall. Around it I built the boring, necessary stuff -- CSV importers for the life-member and pending-member exports, dry-run by default and never downgrading someone who already paid, plus a re-tier pass over the full export. Also a forgot/reset password flow and an admin activation blast, which needed the Vercel function timeout bumped to 60 seconds because it loops through email sends.
WeCos got the same idea built as an explicit layer: a capability-to-minimum-tier matrix, one can() function, and a useCan() hook that every gated surface asks. Then feature after feature hung off it -- venture directory listing, the provider lead inbox, unlocking a lead's contact details for a credit, requesting a call with the founder, and messaging, which moved out of free so a free member DMing a paid one now gets a modal instead of a silent nothing. Razorpay one-time checkout went in the same day, test mode, with the amount read from the tier config on the server and never trusted from the client.
That last detail is the whole lesson repeating. A gate in the UI is a suggestion. The Parliament's job-opening gate is enforced in the server action too, not just by disabling the Post button, because a disabled button is a decoration. And an order amount posted from the browser is a price the customer gets to choose.
The rest of The Parliament's day was structural: profiles moved to a root /username URL with route names reserved so nobody can register a username that shadows a page, photo upload straight to Supabase Storage, and the old directory rebuilt as /community with real data, filters, infinite scroll and a Connect button. WeCos wired its startup directory to real rows instead of hardcoded samples, then found the follow-on bug immediately -- clicking a real startup gave "Startup not found", because the detail page could resolve everything except the table the directory had just started reading from.
What I am taking from it
Silent permission failures are the worst class of bug for a solo builder, because they never page you. No exception, no 500, no red in the logs. Just a number that stays 0 and a user who assumes the feature is broken and leaves. You only find them by using your own product as someone who is not you.
So that is the new habit. Every permission check gets a logged branch on the no path, even when no is the expected answer. And I want a second test account -- not the owner -- for every surface I ship, because every one of these bugs was invisible to the account that built the thing.
Next up on my own site: auditing every remaining trigger in that schema for the same two missing words.

