Search the documentation

Find a page by title or section.

Concepts

Roles and permissions

User types, hierarchical roles, resource:action permissions, expiring assignments, and why tenants cannot see each other.

Updated

Access in Cooriroo is decided in two layers. A user’s type says broadly what kind of person they are and which surface they sign in to. Their roles say what they may actually do. Underneath both sits the boundary that matters most: your tenant’s data is invisible to every other tenant, and that is enforced by the database rather than by the application remembering to ask.

Who can do this

Role
Administrators
Permission
roles:readroles:assign

Viewing roles and assigning them are separate permissions. Someone can audit who holds what without being able to change it.

User types

Super adminAdminDriver
Belongs toThe platform, across tenantsOne tenantOne tenant
Signs in toThe super admin consoleThe web consoleThe mobile app or driver portal
Permission checksBypassed entirelyEnforced by roleEnforced by role
Typical workProvisioning and supportPlanning, dispatch, fleet, settingsRunning trips, capturing proof
The three user types you will meet running a tenant.

The type is deliberately coarse. "Can edit vehicles" is not a user type — it is a permission, and two admins in the same tenant can hold very different ones. A user also carries an account status: pending_activation until they accept an invitation, then active, and suspended or deactivated if you need to stop them signing in without deleting the record and the history attached to it.

Roles carry the detail

A role is a named bundle of permissions. Cooriroo ships four system roles — super admin, admin, user and driver — and you can define your own on top of them. A role may name a parent, in which case it inherits everything the parent grants and adds its own, which is how a "dispatch supervisor" becomes "everything dispatch can do, plus these three things". Inheritance follows the chain as far as it goes, and a role cannot be made its own ancestor: a change that would close the loop is refused.

Permissions are resource and action

Every permission is written resource:action. The resource is the thing being touched and the action is what is being done to it, so the whole permission set stays readable without a lookup table:

  • read — see it, list it, export it
  • write — create it and change it
  • delete — remove it
  • assign — grant something to somebody, as in giving a user a role

So trips:read lets someone open the trip list, vehicles:write lets them add and edit vehicles, and roles:assign lets them hand roles out. Permissions are never granted to a user directly — always through a role — which means revoking a role revokes everything it carried in one move, rather than leaving loose grants behind.

A driver’s reach is narrower than their permissions

Permissions decide what kind of thing a user may touch. For drivers there is a second, tighter rule on top: a driver may act on a task only if it is assigned to them, or if it sits on a trip they are driving. Holding the driver role does not give anyone the run of every task in the tenant.

When that rule refuses, the answer is "not found" rather than "not allowed". The difference matters: a refusal that admits the record exists is a way to map another driver’s round one identifier at a time, and there is no operational reason to let anyone do that.

Assignments can expire

Giving a user a role can carry an expiry date. This is the clean answer to covering an absence, onboarding a contractor for a season, or lending someone elevated access for one week of a migration. An expired assignment simply stops counting the moment it passes — the user’s permissions are resolved from assignments that are still live, so nothing has to run, and nobody has to remember to take the access back.

Tenants cannot see each other

Every record in Cooriroo — every trip, destination, vehicle, task, shift and log line — belongs to exactly one tenant. Isolation is enforced by row-level security in PostgreSQL: the database is told which tenant the current request belongs to, and rows outside that tenant are not returned. They are not filtered out late; they are never visible in the first place.

The practical consequence is that you never pass a tenant identifier anywhere. Not in the console, not in an API call, not in an import file. It comes from the credentials being used, which is what makes a missing filter impossible rather than merely unlikely. Tenant isolation covers what this means when you are calling the API.