Roles in the app
After this you can answer: How does the API decide that only an admin may create an event?
Picture it
Section titled “Picture it”flowchart TB R["events.create"] --> A{"session?"} A -->|"no"| E401["401 UNAUTHORIZED"] A -->|"yes"| B{"role lookup in Postgres"} B -->|"account missing"| E401 B -->|"user"| E403["403 FORBIDDEN"] B -->|"admin"| OK["create the event"] B -->|"lookup failed"| E500["500 INTERNAL_SERVER_ERROR"]How it works
Section titled “How it works”- Three kinds of procedure.
publicProcedurelets anyone in.protectedProcedureadds the session check.adminProcedureadds the role check on top. - 401 first.
isAuthedthrows UNAUTHORIZED when there is no session: “we do not know who you are”. - Then the role.
isAdminlooks up the role by user id. A missing user gives 401. Auserrole gives 403 FORBIDDEN: “we know who you are, and you may not do this”. - Fail closed. If the role lookup fails (Postgres down), the request fails. We never let someone through when we cannot tell.
In MiniTickets
Section titled “In MiniTickets”The middleware lives in apps/api/src/trpc/init.ts → adminProcedure.
events.create is an admin route; browsing events is public; booking is protected.
The role column has a default of user and a database CHECK that allows only user or admin, so
even a typo from a script is refused (apps/api/src/db/schema/users.ts).
There is no screen for roles, on purpose. The make-admin script changes the role in Postgres and
revokes the user’s sessions, so they must log in again
(apps/api/src/scripts/make-admin.ts).
Hiding the admin link in the web app is only for looks; the API check is the real one.
See the API app on the map →See it in AWS
Section titled “See it in AWS”Roles live in Postgres, not in AWS IAM. The API log shows 401 and 403 answers with their correlation ids.
aws logs tail /minitickets/dev/api --since 10m --filter-pattern FORBIDDEN
Break it
Section titled “Break it”| Symptom | Cause | Where to look |
|---|---|---|
| A new admin still gets 403 after logging in again | The role change went to the wrong email | The users row |
| An admin page loads, but saving gives 403 | The UI shows the page; the API checks the role | Working as designed |
| Admin routes return 500 while the cached events list works | Postgres is down; the role check fails closed | /ready, the API log |
Say it in an interview
Section titled “Say it in an interview”Follow-ups
Section titled “Follow-ups”Why not store the role in the session?
Without revocation or another refresh mechanism, a copy could stay old until the session expires after 7 days. Reading Postgres avoids that stale role. The trade-off is one primary-key lookup per admin check and a dependency on database availability.
Why not look up the role in createContext for every request?
Then every route, even the public events list, would depend on Postgres for something only admin routes need. With Postgres down, a logged-in visitor could not browse cached events.
What is the difference between 401 and 403?
401: not authenticated, logging in may help. 403: authenticated but not allowed, logging in again will not help.
Advanced: Why make-admin revokes sessions
The service revokes sessions before updating Postgres and again afterward. The second revocation removes sessions created by concurrent logins before that point. This is not one atomic change across Postgres and Valkey: a login can save a session after the second revocation, and a request already authorized can finish. Reading the role on each new check still prevents a stale session role from preserving admin access.
Checkpoint
Section titled “Checkpoint”Answer these without scrolling up. If one feels hard, reread that section.
- Which three procedure kinds exist, and how do they build on each other?
- When does the API answer 401, and when 403?
- Why is the role read from Postgres on each admin request?
- What protects the role column from a typo?
- Why is hiding the admin link not security?
Redraw it from memory: the decision: session? then role = admin? with the 401, 403 and OK outcomes
Try this: on the local stack, call events.create logged out and then as a normal user, and compare the two error codes
Read this book online: https://book.theversehub.in/