Stage 7 · Master
Authentication & Authorization
Permission Modeling with RBAC
Make permissions readable enough for humans to review before you make them flexible enough for every future edge case.
Why Roles Alone Stop Being Readable
At the start of a product, role-based access control is deceptively easy. You have owner, member, and maybe viewer. Then a few routes land that do not fit cleanly: project admins can archive projects but not invite workspace members; billing admins can manage tenant settings but cannot delete tasks; a support operator can impersonate a read-only view for debugging. Suddenly route code is full of checks against role names that no longer communicate the underlying permission.
I did not want Fieldwork to grow into that kind of role soup. The system already spans tenants, workspaces, projects, and tasks, and we know from the course roadmap that more services are coming. A route saying owner or admin tells me almost nothing about what is being protected. A route saying tasks:task:delete tells me exactly what is at stake.
That is why Fieldwork's authorization vocabulary is permission-first, with roles as named bundles of permissions. The alternative was to encode logic directly in role hierarchies or boolean columns on memberships. The cost of that alternative is not theoretical elegance; it is review pain. Every new feature turns into archaeology: which role names imply which hidden powers in which service?
| Model | What looks easy initially | Why it breaks down |
|---|---|---|
| Role checks in handlers | Quick to ship route by route | Permission meaning is buried in ad hoc code |
| Boolean capability columns | Simple database queries | Schema grows sideways with every new action |
| Permission strings grouped into roles | One shared vocabulary across services | Requires discipline in naming and ownership |
Choosing a service:resource:action Shape
Fieldwork settled on a service:resource:action string format such as tasks:task:delete or workspaces:membership:update. The first segment answers who owns the permission namespace. The second answers which domain entity is affected. The third answers the allowed operation. That structure is specific enough to stay meaningful in logs and route declarations, but simple enough to fit in a token claim or database column without custom parsing rules.
I explicitly rejected wildcard-heavy models like tasks:*:* for normal application code. They look flexible, but they hide too much. Once wildcards are everywhere, a reviewer cannot tell whether a role really needs broad power or just inherited it because the permission catalog was too lazy to stay explicit. Fieldwork may still use a special super-admin escape hatch internally, but normal tenant-facing roles stay concrete.
Mapping Fieldwork Roles to Permissions
Roles still matter because humans do not want to administer twenty individual permissions for every workspace member. But in Fieldwork, roles are now a distribution mechanism, not the primitive itself. A workspace_owner role expands to a reviewed set of permissions. A project_contributor role expands to another set. The route middleware does not care which role granted the power; it cares only whether the permission exists.
This also keeps tenant-specific customizations possible later. If a large customer eventually needs a role that can update tasks but not delete them, we can create a new role mapping without redesigning handler code. If the system had hard-coded owner and member checks everywhere, that change would turn into a route-by-route hunt.
That split sounds small, but it changes the whole system. Assignment becomes an admin concern. Enforcement becomes a consistent gateway concern. The two no longer need to share the same representation.
Keeping the Model Legible as Services Grow
The service prefix is what keeps the namespace from collapsing once more internal services appear. Tasks and projects may both have delete operations, but tasks:task:delete and projects:project:delete carry distinct ownership and review surfaces. That makes cross-team changes safer too. The tasks service can propose new task permissions without sneaking behavior into the workspace namespace.
I also prefer explicit action names over CRUD purism when the domain needs it. archive, assign, export, and reopen are all better permission names than forcing everything into update. A permission vocabulary should reflect what humans actually debate in code review. If a product manager asks whether a role can archive a project, no one should need to translate that into a generic write bucket first.
- Name permissions so a reviewer can understand them without opening the handler implementation.
- Keep role definitions centralized and versioned like any other policy surface.
- Add new permissions before writing middleware that depends on them, so the vocabulary stays intentional instead of accidental.
The RBAC Decision We Locked In
Fieldwork models authorization with explicit service:resource:action permission strings and uses roles only as assignment bundles. That choice costs a bit more up front because we have to maintain a permission catalog and role mappings deliberately. But it buys us the thing we will need more as the backend grows: legibility. A route can declare what it needs. A token can state what it grants. A reviewer can spot scope creep without reverse-engineering role lore.
The rejected alternative was role-centric logic scattered through handlers and membership lookups. That would have been faster for the first handful of endpoints and worse for every endpoint after that. Fieldwork is already beyond the size where hidden authorization semantics are cute. We picked the model that remains readable when services, roles, and tenants all become more complicated at the same time.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.