Skip to content

I made my post URLs readable without touching the primary key

Community posts had ugly UUID links. I wanted human URLs -- without ripping out the key every other feature leans on.

SD
Shubham Datarkar
· 2 min read

Every post on my community feed lived at a URL like /community/p/8f3a1c2e-... -- a 36-character UUID that no human could read, remember, or trust. It worked. It was also ugly enough that I finally decided to fix it. And the fix is exactly the kind of change that quietly breaks everything if you get greedy.

The obvious move is to swap the UUID for a nice short number and make that the primary key. Cleaner rows, prettier URLs, done. I opened the migration file and stopped. That UUID is not just in the URL. It is the target of every foreign key I have built on top of the feed -- votes, comments, bookmarks, reblogs, poll votes, reports, moderation actions, notifications, analytics, search. Ripping out a primary key on a live table means rewriting all of those at once and praying the backfill lines up. That is the migration you run at 2am and regret by 3am.

Keep the key, add a face

So I did not touch the primary key at all. The UUID stays authoritative -- it remains what every foreign key points at. I added a second column, public_id, which is just a bigint: the year times 1e8, plus a global sequence that never resets. So the first post ever is 202600000001, and a post next year might be 202700012346. A before-insert trigger stamps it, and a backfill ordered by created_at gives older posts lower numbers. That is the only new thing in the schema.

public_id is routing and display only. Because I did not move the key, votes, comments, bookmarks, reblogs, moderation and the rest are untouched by construction. There was no risky rewrite. I did not have to reason about which features might break -- none of them can, because none of them ever knew the URL changed.

The other half is respect for links that already exist out in the world. The route now resolves posts by public_id, and the proxy issues a proper 301 redirect from any legacy /community/p/{uuid} link to the new canonical URL. New all-digit URLs short-circuit before any auth round trip, so the pretty path is also the fast path. Share links, comment links and the OG metadata all point at public_id now.

The honest rough edge: the RPC signatures change, so I could not do a full browser test until the migration was actually applied in Supabase. tsc was clean and 74 of 74 unit tests passed, but the end-to-end smoke had to wait on the manual SQL. I do my migrations by hand, one editor window at a time, so that gap is real.

The prettier URL was never worth a schema I would be scared to touch again.

note to self

The whole thing is reversible -- drop the trigger, the functions, the sequence and the column and you are back to UUIDs. That reversibility is the tell that I picked the right shape. Next: apply it on prod, watch a few old links 301 cleanly, and finally stop wincing every time I copy a post URL.

Reactions

How was this article?

Newsletter

Never miss the next one

Get the latest playbooks, build logs, and the occasional unpublished idea straight to your inbox. One signal a week — no noise.