---
id: w6/m12/t004
title: "Plan-gate roles on invite/change-role + downgrade role guard"
worker: worker6
status: todo
estimate: 45m
depends_on: [w6/m12/t002]
---

## Objective

Enforce Render's plan-gated role sets: on a Pro workspace only ADMIN/DEVELOPER are assignable (CONTRIBUTOR/VIEWER/BILLING are Scale+), invite and change-role reject out-of-plan roles, and `ChangePlan` downgrades are additionally refused while members hold roles the target plan lacks.

## Context

- `RESEARCH-workspaces.md` finding 5: Pro = Admin, Developer; Scale/Enterprise add Contributor, Viewer, Billing; Hobby has no roles (single member).
- Members service: `lego/backend/internal/members/service.go` (invite/change-role already consult `store.LimitsFor(plan).MaxMembers` — extend the same plan-awareness to the role set). Roles are Render's UPPERCASE enum (docs/ADR024-members.md).
- Natural home for the role-set catalog: `lego/backend/internal/store/plans.go` (`RolesFor(plan)` beside `LimitsFor`).

## Steps

1. Add the per-plan assignable-role set to `internal/store/plans.go`.
2. Enforce it in members invite + change-role with a `core.ErrBadRequest` naming the plan and allowed roles.
3. In `ChangePlan`, refuse a downgrade while any member holds a role outside the target plan's set (error names the offending members/roles).
4. Update `docs/ADR024-members.md`'s role-matrix note.

## Files

- `lego/backend/internal/store/plans.go`
- `lego/backend/internal/members/service.go`
- `lego/backend/internal/workspaces/service.go` (downgrade guard)
- `docs/ADR024-members.md`

## Acceptance criteria

- [ ] Inviting a VIEWER on Pro is rejected; DEVELOPER succeeds; both succeed on Scale
- [ ] scale→pro downgrade with a VIEWER member is refused, naming the blocker
- [ ] Existing members tests still green

## Out of scope

- Migrating pre-existing out-of-plan role assignments (none can exist yet — roles shipped after plans, all workspaces are effectively ungated today; flag if reality disagrees)
