In short
Separate commercial authority from operational authority. The people who take money are not automatically the people who run servers. Then make every workspace boundary absolute: a record from another workspace must be indistinguishable from one that does not exist.
Permissions in a hosting platform have two independent axes, and it is easy to model only one. The first is which business are you acting for — the tenancy boundary. The second is what may you do within it — the role. Getting the first wrong is a data breach; getting the second wrong is an operational accident.
The tenancy boundary is absolute, and it has a specific shape
The rule that matters: a record belonging to another workspace must answer exactly as a record that does not exist. Not "forbidden", not "you do not have access to this invoice" — the same not-found a nonexistent identifier gets.
The reason is that a distinguishable refusal is an oracle. If "forbidden" means it exists and "not found" means it does not, anybody with a login can enumerate your other customers' record identifiers and learn how many invoices a competitor has. Two different answers leak, and the leak is cheap to avoid.
This has to hold on every route, including the ones added last. The usual failure is a new endpoint that checks the record exists before it checks who is asking.
Roles: commercial and operational are different
The billing row is the interesting one. A bookkeeper needs to place a non-payment hold — that is a commercial act — and does not need the ability to suspend a hosting account at the panel, or to open a customer's control panel as that customer. Those are operational, and the people who take money are not automatically the people who run servers.
Reads and writes need different lines
Reading a connection's safe metadata — its name, its module, whether it is set up — is reasonable for any active member. Reading what a provider holds, which is another business's customer list, is not. Nor is minting a sign-in link into a customer's panel. Draw the line by consequence rather than by HTTP verb: a GET that returns somebody else's customer data is a privileged operation.
Credentials are never readable
An administrator can replace a provider credential. Nobody can read one back, including them. Store credentials outside the primary database where you can, record the act of setting one, and make rotation a normal operation rather than an incident. There is more in API credentials: least privilege and rotation.
Membership changes are events
- Adding somebody is recorded: who added them, when, at what role.
- Changing a role is recorded, with the previous role.
- Removing somebody takes effect on their next request, not at their next login.
- An invitation expires. An invitation that never expires is a credential sitting in somebody's email forever.
- Ownership transfer is its own act with its own confirmation, and it is the one thing an administrator cannot do.
Where GENV is
GENV has five roles — owner, administrator, billing, support and read-only — with commercial and operational authority separated as above, and the tenancy boundary answers not-found for another workspace's records on every route. Provider credentials are held by a secrets store rather than in the application database and are never returned by any route. Membership changes and role changes are recorded acts.
Questions
Are five roles enough?
They are a sensible starting point for a small team. Per-permission configuration sounds flexible and can produce role sets nobody can audit. Add a role when you have a person who does not fit, not in advance.
Should support staff see invoices?
Reading them, usually yes — a support conversation is often about a bill. Changing them, no. That is the commercial axis.
How should contractors be handled?
As members with an expiring invitation and the narrowest role that works, reviewed on a schedule. The worst pattern is a shared login, because it makes every recorded act anonymous.