Summary
A conventional REST API means every data-fetching endpoint is, by construction, something a browser can call directly — anyone with the URL and a valid session can hit it, regardless of what the UI does around it. The usual response is to harden that surface: auth-check every route, validate every payload. The more durable fix is to not give the surface a URL in the first place.
Problem
As a CRM grows the number of roles and permission boundaries it supports, a client-facing REST layer becomes harder to reason about. Every new endpoint is a new place a role check can be missing, inconsistent, or quietly bypassed by a request crafted outside the UI. Auditing that surface means auditing every route, every time a role changes — the correctness of the whole system depends on nobody forgetting a check at any single call site.
Discussion
The alternative isn't a better auth check — it's removing the endpoint that needed one. Replacing client-facing REST routes with Next.js Server Actions means there is no longer a data-returning URL a browser can call outside the application's own server-rendered flow. Role checks move into the same Server Action that fetches or mutates the data, so the boundary that returns data is the boundary that enforces who's allowed to see it — there's no separate route to forget to guard.
This isn't free. Server Actions couple data access more tightly to the framework itself, which makes it harder to expose the same data layer to some future client that isn't a Next.js server-rendered page. That's a real constraint to accept, not a footnote. But it's a narrower problem than the one it replaces: needing a different client someday is a known, schedulable cost. A missed auth check on a route nobody remembered to review is not.
Key Takeaways
- An endpoint that doesn't exist can't be called incorrectly — removing a client-exposed surface is a stronger property than auth-checking everything on it.
- Put the access check at the same boundary that fetches the data, so there's no separate route where the two can drift apart.
- Coupling data access to a framework primitive is a real tradeoff — accept it deliberately, don't discover it later as a surprise.