General
Your primary key shouldn't be in the URL
Divyakush Punjabi DEV Community 周榜
3 views
Open an order confirmation in a lot of apps and the URL ends in something like /orders/48213. That number tells a stranger more than you'd think: roughly how many orders you've taken, how fast you're growing, and — if a single authorization check is ever missing — exactly which URLs to try next. On Saturdays, a food delivery platform, the IDs that leave the server are never the ones the database uses internally.
What an auto-increment ID leaks
A sequential integer primary key is a great internal identifier: compact, fast to index, naturally ordered. It is a poor external one, for three reasons:
It's a business metric. Place one order on Monday and another on Friday, then subtract. You now know the platform's weekly order volume. Competitors do exactly this.
It's a map. If /orders/48213 is yours, /orders/48212 is someone else's. An attacker doesn't have to guess anything.
It turns one bug into a breach. An endpoint that forgets to check ownership — an IDOR, one of the most common web vulnerabilities there is — goes from "exploitable if you happen to find an ID" to "download every order by counting."
Two identifiers, two jobs
The fix is to stop making one column serve two audiences:
class Order(models.Model):
id = models.BigAutoField(primary_key=True) # internal: joins, indexes
public_id = models.UUIDField(
default=uuid.uuid4, unique=True, editable=False
) # external: URLs, APIs, events
The integer stays inside the database, doing what it's good at. Every URL, API response, event payload and webhook uses public_id. A random UUID says nothing about volume, has no neighbour, and can't be enumerated.
This is not access control
It's worth being blunt about the limit, because this is exactly where teams talk themselves into a mistake: an unguessable ID is not a permission check. URLs get shared, logged, pasted into support tickets and saved in browser history. If knowing an order's UUID is enough to read it, you haven't fixed the vulnerability — you've hidden it.
Every lookup still has to be scoped to the caller: this order belongs to this customer, this restaurant, this rider's assignment. The public ID is defence in depth. When the ownership check is correct, it changes nothing. When one is ever missing, it turns a catastrophic, enumerable leak into a narrow, hard-to-exploit one.
Doing it without regret
A few details make the pattern cheap to live with:
Add it early. Retrofitting a public ID onto a table that outside systems already reference means migrating every consumer. On day one it's a single field.
Index it. unique=True gives you the index, so lookups by public ID stay fast.
Never expose both. An API that returns id and public_id has undone the whole point. Serializers should simply leave the internal key out.
The takeaway
Separate the identifier your database needs from the identifier the world sees. It costs one column and removes a whole class of information leak — as long as you remember it's the second lock on the door, never the only one.
More of the design decisions behind the platform — the data model, the payment path, the POS integration — are in the case study.
👉 Read the case study: Saturdays — Food Delivery Platform
Divyakush Punjabi — Full-Stack & AI Systems Engineer
🌐 https://www.divyakush.com · 💼 LinkedIn · 💻 GitHub
Read original: https://dev.to/divyakush/your-primary-key-shouldnt-be-in-the-url-35io
← Previous
Testing email verification flows in Playwright without a mail server
Next →
Why I Built My Portfolio with Bun + Astro + MDX Instead of a More Complex Stack
Related
TACACS+ Failover Testing: Rejection, Outage, and Recovery Are Different Tests
General
0
DEV Community 周榜
Caesar Cipher Explained: How It Works, Encryption, Decryption, and Examples
General
2
DEV Community 周榜
Why I Built My Portfolio with Bun + Astro + MDX Instead of a More Complex Stack
General
1
DEV Community 周榜
Testing email verification flows in Playwright without a mail server
General
0
DEV Community 周榜
Comments0
No comments yet — be the first