A claim about access is easier to trust when it names the constraint that enforces it. Everything in this section is a property of the system that runs today, read off the real schema, migrations and routes, and each card names where it is enforced so you can check it rather than take our word. Where a guard is narrower than its headline, the card says so on a line of its own; a card carrying a limit is more trustworthy than one without, not less.
A seat is a row, and it binds at exactly one tier
Enforced in the database or on the route today
An associate does not hold a vague level of access. They hold a membership row, and that row is bound to a studio, or a school, or a district -- exactly one of the three, counted and checked by the database rather than assumed by the application. A row that tried to be both a studio seat and a school seat is rejected as arithmetic, not as a policy someone remembered to write.
Evidenceck_membership_one_scope: CHECK ((partner_org_id IS NOT NULL) + (school_id IS NOT NULL) + (district_id IS NOT NULL) = 1), migration 0012_studio_project.
LimitThe constraint is declared NOT VALID, which guards every new and updated row from that statement forward but does not retroactively prove older rows. A VALIDATE CONSTRAINT pass is the follow-up, and it has not been run.
The role vocabulary is a database enum, not a free string
Enforced in the database or on the route today
The partner-tier roles an associate can hold are values in a type the database knows about, so an unrecognised role is not a row with a typo in it -- it is a row the database will not accept. The partner-tier values include a studio administrator, a commissioned account representative, a support administrator, and the two photographer roles.
Evidenceenum UserRole in schema.prisma carries partner_admin, account_manager, support_admin, photographer_org, photographer_user and alumni among its values.
The commissioned representative holds exactly one partner-tier seat
Enforced in the database or on the route today
The account representative role is deliberately constrained rather than left broad: it holds one partner-tier membership with the studio set and the school and district fields empty, which is the same cardinality rule every other seat obeys. Its reach across schools is pre-expanded from an assignment list rather than granted wholesale.
Evidenceschema.prisma, on account_manager: holds EXACTLY ONE partner-tier membership (partnerOrgId set; school/district NULL), satisfying ck_membership_one_scope; reach is pre-expanded to the assignment list.
A seat's reach is derived by the server, never claimed by the caller
Enforced in the database or on the route today
When an associate authors or reads on the question engine, the account their work belongs to is worked out on the server from who they are. It is not read out of the request. A caller who supplies an owner in the body does not thereby become that owner; the value is ignored, because a caller-supplied owner is a cross-account write primitive rather than a convenience.
EvidencerequireQtCreator (apps/api/src/plugins/context.ts) resolves the caller to a server-derived owner scope, a studio owner_org_id or an individual owner_school_id; the route docblock states the owner is NEVER read from the request body or params.
A wrong-account identifier returns nothing, not someone else's row
Enforced in the database or on the route today
If an associate asks for a question, a set, or a response that belongs to a different account, the answer is a plain not-found. It is not a permission error that confirms the thing exists, and it is certainly not the row. Behind the route check there is a second, independent wall in the database itself, so the route does not rely on either one alone.
EvidenceMigration 1140 per-owner row-level security (is_system() OR owner_org_id = current_studio_id() OR owner_school_id = ANY(current_school_ids())); the route additionally scopes every store call, and a cross-owner id returns a UNIFORM 404.
A seat has an end date, and the dates must be in order
Enforced in the database or on the route today
A membership carries the moment it takes effect and, optionally, the moment it stops. An associate who leaves is a row whose window closed, which is a fact with a date on it rather than a checkbox someone has to remember to untick. The database refuses a window that ends before it starts.
EvidenceMembership.effective_from / effective_until, ordered by ck_membership_effective_order, migration 0005_constraints.
LimitAn end date must still be set by a person. The ordering is enforced; the diligence of writing the date down is not something a constraint can do for you.
Money authority is a separate column from the role
Enforced in the database or on the route today
Whether a seat may touch money is not folded into the role name, where it would ride along invisibly with a promotion. It is its own boolean on the row, alongside a finer staff role, so that separating duties is a thing the data model can express rather than a convention held in someone's head.
EvidenceMembership.is_finance_role (Boolean, default false), distinct from the coarse role (UserRole) and the finer staff_role (StaffRole, nullable).
A minor may not hold a finance seat
Enforced in the database or on the route today
A student, a student ambassador, or a minor playwright cannot be the holder of a seat that carries money authority. This is written as a database check, because a rule about a child and commerce should not be enforceable only by whoever is reviewing the pull request that afternoon.
Evidenceck_membership_minor_no_finance: CHECK (NOT (is_finance_role AND role IN ('student','senior_rep','minor_playwright'))), migration 1327_membership_minor_no_finance_floor.
LimitState this one precisely rather than proudly. The constraint is NOT VALID, and the migration deliberately does NOT run its VALIDATE step; it instructs an operator to count live violating rows first, remediate any that exist, and never to drop the constraint to make the count go away. So it guards new and updated rows from that statement forward. It is a floor being laid, and we will not describe it as a finished audit.
At most one organization can be the platform owner
Enforced in the database or on the route today
The company that runs the platform is also an operating studio, which is a structure that quietly invites a second privileged tenant to appear later. It cannot: the platform-owner flag is guarded by a partial unique index, so at most one organization row can carry it at a time.
EvidencePartnerOrg.is_platform_owner with the partial unique index ux_partner_org_platform_owner enforcing AT MOST ONE TRUE row.
Two of the constraints above are declared NOT VALID in their migrations. That is a real and deliberate engineering choice — it lets a constraint start guarding new and updated rows immediately without a full-table rewrite on deploy — and it is also genuinely weaker than a validated constraint. Both halves of that are stated here because only stating the first half is how a guard gets believed past its actual reach.