Pinned Tweet
Replying to @TTrimoreau
none of them honestly. i'd rather keep shipping for five clients than bet a year on one idea i didn't pick myself.
an iPhone user uploaded a profile photo and my upload route threw a 500
PNG, JPEG, WebP, even AVIF went through fine
the file was HEIC, which iPhones save by default
the route resizes with sharp. its prebuilt binaries ship libheif with the AV1 codec only, so AVIF works
HEIC uses HEVC, which is patent-encumbered. decoding it needs a custom libvips built with libde265
fixed it with 30 lines that convert to JPEG in the browser before upload
the most common phone camera format was the one format my server couldn't open
I moved an API to a new server on a tuesday at 4pm
new IP, updated the A record, green checks everywhere
by 6pm the inbox said the site was down
it wasn't. people were still hitting the old box, which I had already switched off
the A record had a TTL of 86400
that's 24 hours of resolvers everywhere holding on to an address I'd just killed
no fancy multi-region setup saves you from that
the fix is boring. drop the TTL to 300 at least a day before the move, then move
locked myself out of my own server at 11:40pm
a firewall rule I pushed blocked port 22
fine, I'll get in through the VPN. the VPN runs on that server
fine, the provider's web console. it wants my 2FA code
the 2FA backup codes live in a password manager I self-host. on that server
all the ways back in went through the one machine I had just locked
one support ticket and 3 hours later I got a rescue boot
now any firewall change I push undoes itself in 5 minutes unless I confirm it
my scraper ran clean for 3 weeks, then started getting 403s at exactly 2:00am each night
I spent a weekend on fingerprints. new user agents, a stealth plugin, different TLS settings
still 403 at 2:00am
the cron fired at 2:00 sharp and hit the same 40 pages in the same order, 1.2 seconds apart, night after night
no human browses like a metronome
I added random gaps of 4 to 30 seconds, shuffled the page order and spread the run across an hour
zero 403s since
the fingerprint was fine. the rhythm gave it away
my script connected to my own browser over CDP and then just sat there
no error. 30 seconds later, a timeout
same code against a fresh browser connected instantly
the difference was my real browser had 28 open targets. tabs, iframes, a few service workers from Gmail
connectOverCDP attaches to all of them before it hands you anything
raw CDP lets you attach to the one tab you opened and ignore the rest
swapped the library call for about 60 lines of WebSocket code. it connects in under a second now
the library wasn't slow. it was being thorough about tabs I never asked it to touch
my Postgres trigger called a webhook on every new order and never once threw an error
turns out it can't
pg_net doesn't send anything when you call it. net.http_post hands back a request id and the actual request only starts after your transaction commits
so the insert succeeds whether the other side is up or down
the response lands in net._http_response. for 6 hours. then it's gone
I found 212 failed webhooks in that table on a tuesday. the oldest was 5 hours 50 minutes old
an async call that can't fail is a call nobody is checking
your GitHub, LinkedIn and X profiles are not backlinks
open the HTML on any GitHub profile. the website link says rel="nofollow me"
even the links inside your profile README get rel="nofollow"
LinkedIn marks outside links nofollow too. X wraps yours in a t.co redirect, also nofollow
so that's 3 "backlinks" passing zero ranking signal to your site
they still send real people, which matters more for a small site anyway
a profile link is a door, not a vote
"Supabase is just Postgres, you can leave whenever you want"
true for the data. pg_dump works fine
now open your RLS policies
using (auth.uid() = user_id)
auth.uid() is not Postgres. it's a function in Supabase's auth schema that pulls the user id out of the JWT claims on each request
your profiles table probably has a foreign key into auth.users too
restore onto plain Postgres and the rows come across in 10 minutes. then 40 policies fail because they call a function that isn't there
the lock-in was never the database. it was the login
my side project's AWS bill, month by month
march: $0.42
april: $0.38
may: $1.10
june: $34.71
same 9 users in june as in may. same traffic
the difference was one private subnet and the NAT gateway I added so the app could reach the internet
a NAT gateway in us-east-1 bills $0.045 an hour just for existing, plus $0.045 for every GB it forwards
about $33 a month so my app could call one weather API
july: $0.97. moved it to a public subnet and all 9 users noticed nothing
a Notion sync I wrote kept insisting the database had exactly 100 rows
the database had 2,340
no error. no warning. the API sent back a clean 200, 100 results and a small field called has_more: true
I never read that field. I read status codes
Notion pages everything at 100 per request. you pass next_cursor back as start_cursor and keep asking until has_more is false
an hour of debugging to discover I needed a while loop
the bugs that scare me now are the ones that return 200
the Chrome remote debugging port has no password
not "off by default". the protocol has no concept of auth at all
anything that can reach port 9222 can read every cookie, open any tab and act as you on every site you're logged into
so when someone forwards it from a VPS to "quickly test something", they've handed out their whole browser to whoever scans that IP first
the safe version is an SSH tunnel to localhost. it's 1 line and takes about 10 seconds
went to a small Postgres meetup last night, maybe 30 people
during the pizza break I start ranting to the guy next to me about one ORM. the migrations, the way it hides what SQL it runs, the 400 line stack traces
he nods the whole time. asks good follow-up questions. agrees with most of it
then the host calls his name for the next talk
his first slide is that ORM's logo. he has been maintaining it for 6 years
he thanked me for the feedback. I'm sending a PR this weekend out of pure guilt
your side project turned into a real product the day
> someone emailed asking for an invoice with their VAT number on it
> you got scared to rename one database column
> the free tier bill hit $14 and you opened the pricing page twice
> a user found a bug at 3am their time and you fixed it at 8am yours
> you stopped pushing straight to main
> you wrote a migration with a down step and meant it
and still zero paying users, but 2 people have already asked for an API
postgres gave you JSONB, so you didn't need a document store
it gave you tsvector, so search could live next to the data
it gave you LISTEN/NOTIFY for the small realtime bits
SKIP LOCKED turned a plain table into a job queue
row level security handled the multi-tenant part
pg_cron ran the 3am cleanup without a separate scheduler
and still most teams use about 5% of it, then pay for the other 95% twice in separate services
serverless was not invented in 2014
CGI scripts in 1993 already spun up a fresh process for each request and died when it was done
shared PHP hosting in the 2000s gave you a folder, a $5 bill and zero servers to think about
App Engine in 2008 scaled your app with the traffic and never showed you a server
what changed was the billing. you stopped paying for a box and started paying per request, then per millisecond
the idea is 30 years old. the invoice format is new
the costliest habit on small teams: confusing a fast code review with a good one
the fast review: approved in 4 minutes, one comment about a variable name, a thumbs up
the good review: asks what happens when the payment webhook arrives twice
the fast one feels great. the PR merges before lunch
3 weeks later a customer gets charged twice and someone loses a weekend to refund scripts
the 4 minute approval never comes up in the postmortem
interviewer: design a system that handles 1 million requests per second
me, 45 minutes later: sharding, read replicas, a queue in front of the writes, consistent hashing on the cache
got the job
first ticket: the "export to CSV" button times out for one customer with 80,000 rows
the app peaks at 12 requests per second
fix was streaming the rows out instead of building the whole file in memory first
the million requests never showed up. the CSV customer emails us every monday
spent 40 minutes on friday chasing one error: prepared statement "s1" does not exist
worked on localhost. broke the moment traffic went through pgbouncer
transaction mode hands each query to whichever server connection is free. the statement you prepared a second ago lives on a different one
pgbouncer can track them since 1.21, but max_prepared_statements defaulted to 0 until 1.24
one line in the config fixed it
the 40 minutes were me blaming the ORM
got my scooter serviced this morning
the mechanic runs his whole booking system out of one whatsapp group. 340 customers in it
he said 3 different agencies pitched him an app this year. one quoted ₹2 lakh
asked him why he never said yes
"my customers already have whatsapp. what will the app do, send them a message?"
couldn't argue with that
half the apps pitched to small shops are just a slower version of the group chat they already run on
how a temporary fix becomes infrastructure
1. friday, 6:40pm. a sync job keeps failing, so you add a cron that just reruns it every 10 minutes
2. it works. you leave a TODO and go home
3. three months later someone builds the finance dashboard on the data it keeps patching
4. a year later the onboarding doc says "do not touch the 10 minute job"
5. you change teams
6. the box gets retired in a cleanup. revenue looks 18% down for 9 days before anyone works out why
the TODO is still in there btw
