Access Model
Productify has one documented access model. This page describes the customer-facing role model shipped in the Wave 3 release train: configurable per-project tenant roles over a fixed permission vocabulary, the precedence rule, and the boundary between what the platform forwards (facts) and what applications own (capability).
What shipped in Wave 3
Phase 1 — the rationalized role model and opt-in role/permission forwarding — ships in Wave 3. Delegated tenant administration, invitations, and end-user self-service are Wave 4 (gated on the security release). This page documents Phase 1.
The four role systems
| System | Values | Configurable? | Governs |
|---|---|---|---|
| System role | normal, audit, administrator | Fixed enum (operator-side) | Platform-wide operator authority |
| Project role | readonly, tester, developer, maintainer, owner | Fixed enum (operator-side) | Operator resources within a project |
| Tenant role | Project-defined (seeded member, admin, owner) | Configurable per project | Tenant-scoped resources (members, invites, tenant2application config) |
| Application role | readonly, normal | Fixed enum | Per-application access |
Only tenant roles are configurable — because customer organizations legitimately need their own role vocabulary. System and project roles stay fixed enums; they are operator-side and not a customer-org concern.
Fixed permission vocabulary
Roles are configurable; permissions are not. The platform owns a fixed, versioned vocabulary of tenant-scoped permissions. Extending it is a platform release, never a per-project change:
| Permission | Grants |
|---|---|
tenant.members.read | View the tenant's members |
tenant.members.manage | Add / remove members and assign roles (subject to the subset rule) |
tenant.invites.manage | Create / resend / revoke invitations (Wave 4 surface) |
tenant.roles.manage | Define and edit the tenant's role set |
tenant.apps.read | See which applications are enabled and their config |
tenant.apps.configure | Edit per-(tenant, application) configuration |
tenant.lifecycle.manage | Manage the tenant profile / lifecycle |
Authorization checks test permissions, not role names (manager/internal/gql/access_validate.go). A role is just a named bundle of these permissions.
Seeded default roles
Every project is seeded with three roles over the vocabulary, so simple installs never touch the machinery:
| Role | Permissions |
|---|---|
member | tenant.apps.read |
admin | member + tenant.members.read/manage, tenant.invites.manage, tenant.apps.configure |
owner | all permissions (including tenant.roles.manage) |
member is the default member role. Projects may rename, add, or reshape roles over the vocabulary — but only owner is seeded with tenant.roles.manage.
Fail-closed rules
Everything here fails closed — an unknown or dangling reference denies, never allows:
- Unknown permission on a role → the check denies.
- Dangling role FK (a membership pointing at a deleted role) → denied.
- Subset rule (no self-escalation): a user may only assign or define roles whose permissions are a subset of their own. You cannot grant a permission you do not hold, nor edit a role to include one.
- Editing the role set itself requires
tenant.roles.manage(seeded only onowner) and is audit-logged.
Precedence
The precedence rule is encoded once in access_validate.go:
- System administrator outranks project roles for operator resources.
- Project role governs operator resources within the project.
- Tenant-role permissions govern only tenant-scoped resources — members, invitations, and tenant2application configuration. They never grant operator authority.
A tenant owner is powerful within their organization and powerless over projects, environments, machine users, or other tenants.
Platform forwards facts; apps own capability
The framework grants access; applications grant capability. This boundary is deliberate and load-bearing:
- The platform forwards the membership fact — the user's role and permissions in the selected tenant — never an application-specific permission.
- In-app RBAC (for example, a consumer's own five roles and its
CanPerformchecks) stays application territory. Wave 3 does not move it into the platform.
Opt-in identity forwarding
Forwarding is opt-in per application (forward_role setting on the tenant2application / app setting, default off). Apps that own their RBAC are unaffected. When enabled, the proxy injects:
| Header | Meaning |
|---|---|
X-User-Tenant-Role | The membership role name in the selected tenant |
X-User-Tenant-Permissions | Comma-separated permissions from the fixed vocabulary |
X-User-Picture | The Pocket ID picture claim, when present |
window.__PRODUCTIFY__ exposes the same as tenantRole, tenantPermissions, and user.picture.
Gate on permissions, not role names
Role names are project-configurable and therefore not a stable contract. An app that needs to gate a feature should check tenant.members.manage (a permission), not the string "admin" (a name one project may rename). The Manager returns the role and picture from its validation endpoints; the proxy strips any client-supplied copies of these headers via its sanitizer.
See Also
- Authentication — how identity reaches the Manager
- Multi-tenancy — tenants, membership, and selection
- User Management — assigning roles in the UI
- Wave 3 Migration Guide — old role enum → seeded roles mapping