Skip to content

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

SystemValuesConfigurable?Governs
System rolenormal, audit, administratorFixed enum (operator-side)Platform-wide operator authority
Project rolereadonly, tester, developer, maintainer, ownerFixed enum (operator-side)Operator resources within a project
Tenant roleProject-defined (seeded member, admin, owner)Configurable per projectTenant-scoped resources (members, invites, tenant2application config)
Application rolereadonly, normalFixed enumPer-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:

PermissionGrants
tenant.members.readView the tenant's members
tenant.members.manageAdd / remove members and assign roles (subject to the subset rule)
tenant.invites.manageCreate / resend / revoke invitations (Wave 4 surface)
tenant.roles.manageDefine and edit the tenant's role set
tenant.apps.readSee which applications are enabled and their config
tenant.apps.configureEdit per-(tenant, application) configuration
tenant.lifecycle.manageManage 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:

RolePermissions
membertenant.apps.read
adminmember + tenant.members.read/manage, tenant.invites.manage, tenant.apps.configure
ownerall 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 on owner) and is audit-logged.

Precedence

The precedence rule is encoded once in access_validate.go:

  1. System administrator outranks project roles for operator resources.
  2. Project role governs operator resources within the project.
  3. 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 CanPerform checks) 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:

HeaderMeaning
X-User-Tenant-RoleThe membership role name in the selected tenant
X-User-Tenant-PermissionsComma-separated permissions from the fixed vocabulary
X-User-PictureThe 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