Previously, at Vandelay Industries#
Art Vandelay is building an app that runs Vandelay Industries's potato chip import/export empire. It lets suppliers do business with Vandelay without him having to talk on the phone (which for some reason kills deals).
In Part 1 we built a frontend and a backend, which talk to each other through an API. In Part 2 we gave it a memory, a Postgres database on Railway, now filling up with supplier applications.
But there’s one small problem. Here's Art's "private" dashboard:
I put "private" in quotes because nothing about it is private, because Art forgot to add a login. Anyone with the URL - a competitor, a bored teenager, Newman - can read every application + every shipment.
So now Art needs to lock it down. He wants suppliers to sign in and be able to check only their own application status. Art also needs the dashboard locked to himself. And Newman must never, under any circumstances, see the shipping schedule.
All of that is the mythical "auth" you’ve heard about, which is really two separate jobs, as engineers that work on auth are always pointing out.
Authentication vs authorization#
Authentication is proving you are who you claim to be. Anyone can claim to be a marine biologist, but authentication requires you to prove it, by logging in if we’re talking about a web app.
A few years ago, authentication was actually tedious to set up, but it’s now a solved problem. Providers like Clerk (which Art used for his app) make this super easy.
You will see people on X poo-pooing auth providers and saying they can just "roll their own." And perhaps they can. But you can't. And they probably can't either.
Clerk hands you two API keys, you add them to your app (by setting environment variables on Railway, or in your local .env file), and you have working login + signup pages on your app (including the much loved “Sign in with Google”, bc who needs another password).
One of those keys (the "publishable" one) is safe to expose in your frontend. The other (the secret key) must never leave your environment variables, which we store securely in Railway.
And since it's probably your coding agent doing this wiring, not you: Clerk ships an MCP server and a set of agent Skills, so your agent pulls current, correct patterns straight from the source instead of guessing at Clerk's API from year-old training data. Auth is the last place you want your agent hallucinating.
But then the real fun starts, of controlling what users who log in can actually do.
Authorization is what a verified person is allowed to do. Puddy can log in to the app, but he can’t approve his own supplier application.
Authorization is much more complicated than authentication, because there are basically unlimited permutations of roles (admin, user, superuser, superadmin) and activity levels to account for.
Authorization is the half of auth that vibe coders like Art actually get burned by, in 2 ways:
- Not blocking pages + data that shouldn’t be accessible by a given user
- Blocking too much and providing a poor user experience
#2 is a real risk. Authorization requires really gaming out options before you commit, like ordering off the Cheesecake Factory menu.
The authorization plan#
Gaming it out can be embarrassingly low-tech, literally making a table of roles + features those roles should have access to, like so:
Each column is a role, and each row is a feature. The cells are ownership: whose data a given role can act on for that feature.
Kramer can view applications, but only for Kramerica Industries. Coding agents can figure this stuff out, but you’re really better off being clear about what you need here. And it only gets more important as the feature set in your app grows over time.
Would you order “surprise me” off the Cheesecake Factory menu?
Are you on the list?#
A rule from Part 1 of this series is worth tattooing somewhere your mom can't see: never trust the frontend.
The frontend runs in the user's browser, and the user controls their browser. Anything you "hide" there can be un-hidden with dev tools — or skipped entirely by calling your API directly.
When a user does something in the Vandelay App (like view a supplier application), the request passes through three separate stages: the page route they visit (/applications/1002), the API endpoints that page calls to fetch data (/api/applications/1002), and the database query the backend runs to actually get the relevant rows.
Your backend has two places (that we know of) to enforce authorization along the way:
1. On the page route + API call. Both check the same two things: whether a) the authenticated user’s role should be able to view applications, and b) whether the application in question (id 1002) belongs to the user’s organization (Kramerica Industries).
If you skip that second check, any logged-in user can read any other user's data just by changing the number in the URL. Another supplier pulls up /applications/1002, and now he's reading Kramerica Industries' pitch. And taking notes.
The subtle part is that pages and API endpoints are separate ways to access the same data, and protecting the page does not protect the endpoint underneath it.
This is what middleware is for: it applies authorization checks across every route by default, so a new endpoint call can’t be run unprotected.
2. In the database. Postgres offers row-level security (RLS), which attaches the rule to the table itself. Suppliers can only see application rows matching their org_id, which is passed directly from authentication.
This isn’t hypothetical btw. In 2025, a security researcher scanned 1,645 vibe-coded apps and found 170 with missing or unenforced RLS. Tables of emails, API keys, and payment data, readable by anyone.
It’s possible to have too much of a good thing though - if you over-configure too many RLS rules, no one will be able to see any data.
What breaks when the suppliers show up#
Some auth problems, just like database problems, are invisible while you're testing and guaranteed once real users arrive.
Kicking someone out. A supplier emails Art to say their laptop was stolen, and Art needs that laptop logged out of the app immediately.
Whether he can do that depends on plumbing he's never thought about: when someone logs in, the app keeps them logged in with a session token stored in their browser.
Thankfully auth providers like Clerk offer a "sign out of all devices" button for every user, but if you’re managing auth some other way this could be difficult.
Feature creep. With coding agents, adding new features seems cheap - you type an instruction and it’s done.
But each new feature also expands your authorization requirements, and your logic can become unintelligible pretty quickly.
A month in, Art asks his coding agent for an export button so he can pull applications into a spreadsheet. The agent adds a new endpoint (/api/applications/export) that generates the CSV, but now that endpoint needs an authorization plan. Can only admins export, or can bookkeepers too?
The decision isn’t super clear - it’s not something you want to just leave up to a coding agent.
Requests for more login methods. If the app is really successful with larger Suppliers, they might ask Art to turn on SSO (single sign-on), so that their entire company can log in without having to manually create accounts for yet another app.
An entire software category, including companies like Okta and now WorkOS, exists to solve that problem, and enterprise features like SSO can unlock larger revenue opportunities (but again require effort to implement properly).
Where that leaves Art (and you)#
So auth (both authentication + authorization) is now handled, for today’s feature set anyway.
Clerk tells the app who’s at the door, and Art didn’t really have to build much (other than dropping an API key in Railway).
The app now knows the difference between Art, the bookkeeper, Kramer, and Newman.
Hello, Newman. No, you can't see the manifest.