Story-3386 · functions lane · RFD-MATTER-006 F2
A creator can no longer open a matter they cannot enter
Matter create used to be editor-only, required an editor lead, and could
leave the creator off the roster. Opening create by merely granting members
matters:write would have exposed every existing firm-wide matter
mutation. Story-3386 opens only the create door and makes staffing part of
the same transaction as the matter itself.
active member + matters:create
│
├─ names self ───────────────→ creator = lead
│
└─ names another active user → named user = lead
└─→ creator = member
missing lead / inactive target → whole transaction rolls back
The trap: a global key opens too many doors
matters:write is a firm-wide key used by older mutation routes.
Giving that key to members would solve create by silently granting unrelated
powers. The dedicated matters:create permission is instead issued
to all three active firm roles and gates only POST /app/matters.
One transaction is the staffing contract
| Input | Database result |
|---|---|
| Creator names self | One assignment: creator as lead_counsel. |
| Creator names another active member/editor/admin | Named user is lead; creator is member. |
| Additional active same-firm targets | Deduplicated and stored as members, including admins. |
| Missing lead | DTO rejects the request before service work begins. |
| Inactive, suspended, or cross-firm target | 403; no matter or assignment survives. |
The database seed function locks and validates the creator, lead, and every
requested teammate before its first assignment insert. It then writes the
compulsory lead and creator under the existing one-lead unique index. The existing
matter.created event is now the single combined audit record: its
metadata and secondary subjects identify both the creator staffing and the
initial lead assignment.
The RLS bridge is temporary because content visibility stays unchanged
A plain member cannot read an unassigned client or insert an unassigned matter. That is correct after create, but it forms a bootstrap loop during create. A narrow security-definer validator locks the supplied active client and creator membership through commit. The matter reader recognizes only the exact transaction-local UUID being created, and the seed clears that UUID as soon as it writes the roster. From then on, the ordinary assignment-based entry gate is the only path.
can_read_matter still denies every committed unstaffed matter.
The create branch uses COALESCE(..., false) so an unset PostgreSQL
GUC cannot become a NULL authorization result and slip past a PL/pgSQL
IF NOT guard.
Proof
| Scenario | Evidence |
|---|---|
| S1 | A member creates with self as lead, becomes the sole teammate, and opens the new matter. |
| S2 | Member, editor, and admin lead targets all work; the creator is a member, immediately opens the matter, and one combined audit event is stored. |
| S3 | Omitting the lead returns 422 and writes no matter. |
| S4 | An inactive lead returns 403 and writes no matter; an inactive initial teammate is also atomic. |
| RLS regression | The existing cross-matter roster proof stays green, including an unstaffed member denial. |
| Focused regression | 40 focused Story/team scenarios pass against isolated PostgreSQL. |
| Repository regression | 6,346 tests pass and 3 skip; the sole unrelated order-dependent document-card failure passes on its immediate isolated rerun. |
One-sentence recall
Because create must not become a back door to global write or an inaccessible
matter, matters:create opens exactly one route and the database
atomically seats both compulsory lead and creator before commit.