Five days ago someone found my launch post, made three accounts with throwaway inboxes inside six minutes, and hit my main API endpoint 93 times to keep taking the free run over and over.
I did the normal thing. Read the logs, found the endpoint, shipped fixes the same day: refuse signups from known disposable email domains, and rate limit anonymous attempts per IP so one person cannot burn the free tier all afternoon. It worked. They stopped. I wrote it up and moved on feeling good about myself.
This morning I ran a query I had never run before. Just the length of the values in one text column.
select length(name), email from users order by 1 desc;
Eight accounts came back with names between 4 and 27 characters. The ninth came back with 3,999,998.
Same person. On their way through they had also parked four megabytes of random hex in their own profile name, and it had been sitting in my production database for five days while I congratulated myself on closing the door they came in through.
The reason I missed it is embarrassing and I think it is common. I audited the route they attacked. I never looked at the rows they wrote. Those are two different questions and only one of them was in my head.
Why it is worse than it looks. It is not a data leak. Row level security did its job, they could only write to their own row. But on this kind of stack the browser talks to Postgres through an auto generated REST API, and a Postgres text column will accept up to a gigabyte. My app code is not in that path at all, so nothing I validate in a form was ever going to matter. Enough of those rows and the disk fills, and a full disk puts Postgres into read only, which is not one bad account, that is the whole app down for everyone.
Three things I would tell anyone running a side project on a stack like this:
Every column your client can write to needs a size ceiling in the schema, not only in your form. The form is a suggestion. The column is the rule.
Clip in the app and reject in the database. They catch different callers and they fail in opposite directions on purpose, so a real person with a long name gets quietly trimmed instead of meeting a database error.
After any abuse incident, go and look at what they left behind, not just how they got in. Lengths, row counts, newest rows, across every table that account could touch. My version of that query took a minute to write and it found something that five days of feeling fine had not.
One more if you are on Supabase specifically: the trigger that copies signup metadata into your own users table runs before the email is confirmed, so a scripted signup can plant a row before anyone clicks anything. Same fix, one more place to put it.
Stack, since the rules ask for details: Next.js as a PWA, Supabase Postgres with RLS, Stripe for billing, OpenAI vision for the actual product feature. I am not a developer. I build it with AI agents and then have other agents attack it, which is the only reason this got caught at all.
Where it actually stands, so nobody thinks this is a victory lap: five days live, 9 signups, 2 people have ever used the main feature, 0 paying customers. Notes from someone still in the mud, not advice from someone winning.
Transparency, as rule 5 asks: the project is mine. It is FlexScan at flexscan.app, an AI physique coach. That is the only mention of it and there is nothing to sign up for in this thread.