Flow
Order of work inside each action
11. Resolve access
Load the access context for the current session.
22. Require tenant
Refuse if there is no organization scope; take the organization id from here.
33. Check role
Compare the role to the allowed set; refuse before any other work.
44. Validate input
Non-empty names, present identifiers, member role from a fixed set.
55. Call data layer
Pass the session's organization id explicitly with the client's identifiers.
66. Revalidate and return
Refresh the page data and return a success object.
This week the teams screen of an admin app lost two route files. What took their place is mostly one permission check, pasted into half a dozen functions. That check had to move by hand. The framework doesn't remind you.
Before, the screen fetched its list of teams from a route. Every change, like creating a team or adding a person, was a request to a route handler. The handler checked who was calling and what their role was, then sent back a status code.
Now the page loads the list on the server. Each change is a server action, a function the button calls that runs on the server. When it works, the page re-renders with fresh data. There's less code and no cache to manage. An agent likely did most of the move, judging by the commit.
Hidden is not blocked
A route handler looks like a door, so people ask who gets in. A server action looks like a normal function. It isn't just that. The framework gives it an address, and any signed-in browser can call it with any input, even if the button is hidden.
The page has a flag that hides the controls from people who can't manage teams. That flag only changes what's drawn. It can't block anything.
So each action checks again, on the server.
Before doing anything, it reads who is calling and refuses anyone whose role isn't allowed to edit.
It also refuses if there's no organization, checks the input, and takes the organization from the session. It never trusts one sent by the browser.
One list, three copies
The list of roles that may manage teams now lives in every action, in the page flag, and in the help text on the empty screen. If those copies drift apart, you get buttons that never work, or worse, actions that work for people who can't see a button. Nobody reports a button they never saw.
A no that looks like a yes
The old routes said no with a forbidden status code. The actions return an object with an error message, and the screen shows it as a pop-up. To the server's logs, a refused request now looks like a normal, successful call. If you want to know how often people get turned away, you have to log it yourself, and I haven't yet.