Logging a user in is the solved part — a good identity provider or a well-worn library handles it. The failures that end up in incident reports are almost always authorization: a user who can read another tenant's invoice by changing an id in the URL, an endpoint that forgot its check, an admin flag that leaked. Getting this right is about where the decision is made and how consistently.
Check on the object, not just the route
The most common access-control bug is insecure direct object reference: the endpoint confirms you are logged in but never confirms this particular record is yours. Every data access has to answer 'can this user act on this specific object', which means filtering queries by the user's tenant or ownership rather than trusting an id from the request. Authorization that lives only in the route guard is authorization with a hole in it.
- Scope every query by the current user's tenant or ownership, server-side.
- Never trust an id from the client to belong to the requester — verify it.
- Deny by default; a missing rule should reject, not allow.
Centralise the policy so it can be audited
When permission checks are copy-pasted across dozens of handlers, no one can answer who can do what, and the day requirements change you will miss a spot. Pull authorization into one policy layer — a set of functions or a policy engine — that every path calls. One place to read means one place to review, test and change when a role's powers shift.
Model roles for how the business actually works
Flat role lists stop fitting quickly: real organisations have members who own their own records, managers scoped to a team, and admins scoped to a tenant. Decide early whether role-based checks are enough or whether you need attribute- and resource-based rules, because retrofitting fine-grained access onto a boolean is-admin flag is painful. Keep an audit log of privileged actions so you can answer, after the fact, exactly who did what.
- Design roles around real responsibilities, not a single admin boolean.
- Add resource-scoped rules when access depends on ownership or team, not just role.
- Log privileged and destructive actions with who, what and when.