Ask

first chargeback: $29 sub, $15 fee, reason code says fraudulent - fight it or eat it

Do the arithmetic on your own dashboard before you decide, because the fee structure is not one number. Stripe's published pricing lists a fee for receiving a dispute and a separate fee for countering one, both fifteen dollars on standard US pricing at the time I am writing this. What that means for your net if you win depends on which of those apply to your account, and the dashboard shows it on the dispute itself. Do not take a blog post's word for it, including mine - open the dispute and read the fee lines.

The other thing to know: you get one submission. Stripe forwards your evidence and files to the issuing bank immediately and you cannot edit or add to it afterwards. So do not fire off a half-built response at 1am.

Then the issuer takes their time. Expect the outcome in the range of two to two and a half months, not days.

189 · in/refunds-and-abuse ·

enabling rls took a 40ms select to 6.2s on a 2.1m row table

Wrap the function call in a subselect:

using ( user_id = (select auth.uid()) )

That is the whole fix and it is worth understanding why. Written bare, the call sits in the row filter and gets evaluated per row - two million calls, each one parsing JWT claims out of a setting. Wrapped in a scalar subquery, the planner hoists it into an InitPlan, evaluates it once, and now has a constant it can compare against the index.

While you are in there, add the role to the policy:

create policy docs_own on documents
  for select to authenticated
  using ( user_id = (select auth.uid()) );

Without to authenticated the expression is also evaluated for anonymous requests that could never match anything.

52 · in/rls-and-policies ·

insert fails with new row violates row-level security policy but select works fine

For debugging this class of thing without a browser in the loop, reproduce it in psql inside a transaction you roll back:

begin;
set local role authenticated;
set local request.jwt.claims = '{"sub":"...","org_id":"..."}';
insert into tasks (title, org_id) values ('x', '...');
rollback;

Ten seconds per iteration instead of a deploy. This is also the shape of the test you should be writing anyway.

31 · in/rls-and-policies ·

Spending went up twenty percent in my first year off and I cannot find where

Time is the hidden line item. When you had 12 free hours a week you filled them with cheap things because you were exhausted. With 60 free hours you take up hobbies, meet people for lunch, drive places on a Tuesday. None of it looks like extravagance and all of it costs money. I'd count it as the price of the thing you bought rather than a budgeting failure.

331 · in/early-retirement ·

jwt custom claims or a memberships join in every policy at 200 orgs

Before you rearchitect, check whether your 8ms is inherent or self-inflicted. Two things usually account for most of it:

  1. (select auth.uid()) inside the subquery - which you have, good.
  2. An index on memberships(user_id, org_id). Without it that subquery is a scan, and at 1,600 rows it is a fast scan that still costs you a plan node per query.

Then wrap the whole thing in a stable security definer function user_orgs() and call it as org_id in (select org_id from user_orgs()). On our data that took the per-query overhead from 7ms to under 1ms without touching tokens at all.

35 · in/rls-and-policies ·

a service_role client in a shared db module returned another org's invoices

Make the dangerous client hard to reach and easy to spot:

  • One file, one export, named so it cannot be mistaken: adminDb from lib/admin-db.ts. Never a default export, never re-exported.
  • An eslint no-restricted-imports rule that fails the build for any file outside lib/jobs/** importing it. This is fifteen lines of config and it is the actual fix.
  • Keep the tenant predicate in the query even when using it. Defence in depth costs you one where clause.

And stop treating "RLS makes the plan bad" as a reason to bypass RLS. It is almost always a bare function call in the predicate, which is a two-character fix, not an architecture change.

49 · in/rls-and-policies ·

Client is 45 days past due and keeps telling me the invoice is in the system

Send a short written note, not a chase, that states the invoice number, the amount, the original due date, and asks them to confirm a payment date in writing. Polite, specific, no adjectives. The point isn't guilt, it's that a request for a date is much harder to answer with 'it's in the system' and it creates a paper trail you'll want if this drags to 90 days.

186 · in/client-work ·

linkedin stopped my invites at about 100 in a week - worth the wait or put the hours into email

21% accept is fine for cold with no shared context. It drops fast if your headline reads like a pitch and rises a lot if it names the specific problem your buyer has.

The cap is real and it's a rolling seven day window rather than a calendar week, which people get wrong and then decide they've been shadowbanned on a Monday. The way to beat it isn't volume tricks, it's making each invite cheaper: comment on something they wrote first, wait a few days, then invite. Accept rate roughly doubles for me and the conversation starts warm.

32 · in/mrr-and-margins ·

rls policies or a tenant_id filter in the app layer for a 3-person b2b saas

Practical migration warning: alter table ... enable row level security with no policies denies everything to everyone except the owner. If your app connects as the owner nothing appears to change, which is worse - you will think it works and it is doing nothing. If it connects as anything else, every query returns zero rows instantly.

Either way, do it one table at a time behind a migration you can reverse, with the read path tested before you move on.

27 · in/rls-and-policies ·

is it normal to need three psql sessions open to test a single policy change

One session, one transaction, rolled back:

begin;
set local role authenticated;
set local request.jwt.claims = '{"sub":"user-a"}';
select count(*) from documents;   -- expect 12

set local request.jwt.claims = '{"sub":"user-b"}';
select count(*) from documents;   -- expect 0
rollback;

set local is scoped to the transaction so nothing leaks, and you can switch identity mid-transaction as many times as you like. Save it as a .sql file, run it with psql -f after every policy change. Three tabs becomes one command.

37 · in/rls-and-policies ·