The honest answer is that a group is not a row. Every question set and every response is anchored to exactly one owner, and a database constraint enforces that it is exactly one. For a collection of independent organizations, that is not a limitation to apologise for — it is the property that lets members share a contract without sharing a database. Each card below names the constraint or symbol that enforces it, so the claim can be checked rather than believed.
Exactly one owner per row, enforced by the database
Enforced by the database today
A question set or a response belongs to exactly one owner: one studio organization, or one school. Not both, and not neither. A database CHECK enforces it, so a row that tried to belong to two members of a group would be rejected by the database before any application code had an opinion about it.
Evidenceck_qt_*_exactly_one_owner (migration 1140): (owner_org_id IS NOT NULL) <> (owner_school_id IS NOT NULL).
The owner is derived from the session, never sent by the caller
Enforced by the database today
A member's owner anchor is resolved server-side from the authenticated session. There is no field in any request body or query string that names an owner, so one member of a group cannot address another member's data by guessing or editing an identifier.
EvidenceanchorOf(scope) returns disjoint anchors ({ownerOrgId, null} or {null, ownerSchoolId}); pinned in question-technology-authoring.gate.test.ts.
The database wall sits UNDER the route gate, not instead of it
Enforced by the database today
Per-owner row-level security is the backstop, and the route still scopes every write to the caller's own anchor rather than leaning on it. Two independent walls have to fail together for one member of a group to read another's responses.
EvidenceRLS: owner_org_id = current_studio_id() OR owner_school_id = ANY(current_school_ids()); the route owner-scopes every write regardless.
Five answer shapes, identical for every member
Enforced by the database today
Every member composes from the same closed set of answer shapes the database itself enforces: open text, a 1-to-5 scale, single choice, ranking, and a forced-choice trade-off. Choice, ranking and conjoint each need at least two distinct options; text and scale carry none. No member gets a private dialect.
EvidenceQT_QUESTION_TYPES = text|scale|choice|ranking|conjoint (ck_qt_question_type); ck_qt_question_choice_has_options widened in migration 1164.
The type vocabulary is closed, and that is a group feature
Enforced by the database today
There is no per-owner type-registry table, and the route that would create one answers 501. That is deliberate here: a closed vocabulary is why one member's export is readable by another member's analyst without a translation step or a private schema.
EvidencePOST /question-types -> 501; the vocabulary is the built-in TYPE_CATALOG, not an insertable per-owner row.
Nothing is hard-deleted out from under a group
Enforced by the database today
There is no delete route. A question or a set is retired by moving it to an archived status, so a response already attached to it never dangles and a member leaving a group cannot silently strip history another member relied on.
EvidenceNo DELETE route by design (the 1085 forms-builder rule); retire via PUT status archived.
These properties do not weaken as a group grows. The constraint that separates two members is the same one that separates a member from an unrelated organization on the engine.