question.associatesTalk to us

question.associates · the seat-and-standing address of the question.* family · Powered by question.technology

If you give someone a seat, what exactly did you give them?

An associate seat is not a checkbox in a console we do not have. It is a row in a table, with a cardinality check on it, a role drawn from a database enum, an end date, and a reach the server works out rather than takes your word for — and this page names each of those, including where the guard is weaker than it sounds.

There is no invite table, no seat-count column, no per-seat billing, and no permissions console today, and this page names each of those plainly rather than let an absence read as an accident. It also names the two constraints that are weaker than they sound. Money is off here. Nothing here is AI.

Exactly one tiera seat binds to a studio, a school, or a district, counted and checked by the database
Roles are a typethe partner-tier role vocabulary is a database enum, not a string a caller can invent
Reach is derivedthe owning account is worked out on the server, never read from the request body
Seats have end datesan effective range on the row, with the ordering enforced by the database

One engine, several front doors, and this one is about standing

The question.* family is one shared response engine with front doors aimed at different readers, all built by one team, on one security model, one support operation, and one roadmap. The engine is the same underneath each of them; what differs is the question the reader arrived with.

question.technology is the engine: the shared question-authoring and response-collection surface the whole family runs on.

question.software is the classroom front door: where a teacher writes questions, runs them with a class, and reviews the responses.

question.management is the district facet: how an administrator stands the engine up across more than one building, and what is still done by hand.

question.company is the venture behind the family: who builds it, how it operates, and its honest stage.

question.associates — this page — is about standing rather than spread: who may hold a place inside an account, what that place is recorded as, and what it deliberately does not confer. If question.management answers “how far does this go,” this page answers “what did I just hand this person.”

Powered by question.technology. An organization pays for the underlying platform and the capabilities it selects, not repeatedly for the same engine wearing different brands.

Provable now

What is actually enforced about a seat

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.

The row itself

Anatomy of a seat, column by column

Most pages about roles describe a metaphor. This one describes the record, because the record is the thing that decides what happens. If you are being offered a seat, or deciding whether to grant one, these are the fields whose values are the entire answer.

The membership row, column by column. This is what an associate seat is; there is no second, hidden object behind it.
ColumnWhat it decides
user_idThe person. One human account can hold more than one seat, and each seat is its own row rather than a widening of a single one.
partner_org_id / school_id / district_idThe tier the seat binds to. Exactly one of these three is set, checked by the database. An associate seat is the first of the three.
project_idAn optional narrowing on top of a school binding, used to confine an adviser to one project rather than a whole school. It is not part of the exactly-one rule, because it narrows rather than binds.
roleThe coarse role, drawn from the UserRole enum. This is the value that decides which partner-tier seat you hold.
staff_roleAn optional finer capability tier. Nullable, meaning the coarse role applies broadly rather than narrowed to a specific staff title.
is_finance_roleThe separation-of-duties flag. Whether this seat may touch money, kept deliberately apart from the role name so it cannot ride along with a promotion unnoticed.
effective_from / effective_untilWhen the seat starts and, when set, when it stops. The end is nullable, which reads as open-ended; the database enforces that the end is not before the start.

One person can hold more than one seat, and each is its own row rather than a widening of a single one. That is why removing someone from one organization does not quietly change what they hold somewhere else.

Radically legible about our stage

What we are, and what we are not, on seats

A page about seats and roles is the easiest place in this estate to imply an administration console, an invite flow, and a permissions matrix. So here is the ledger, out loud. The left column is what a page like this usually implies. The right column is the honest version for Question.

The left column is what a page about seats and roles usually implies. The right column is the honest version for Question, stated so an absence never reads as an accident.
What this kind of page usually impliesThe honest version for Question
An invite button that emails a new associate a join linkThere is no invite table in the schema, so there is no join link to send. A partner-tier seat is a row we create with you, in a conversation, and we would rather tell you that than mock up a button.

How we knowAbsence-proven at the pin, not assumed: the key returned zero over the schema while an INDEPENDENT positive control -- a different symbol, not a fold or a paraphrase of the key -- returned a hit in the same invocation, and a junk key was refused.

A per-seat price and a seat counter on your accountThere is no seat-count column on a partner organization and no per-seat billing. The word seat does appear elsewhere in our codebase, on volunteer shifts, field-trip rosters and class sections, but none of those is an account seat, and we are not going to blur the two to look further along.

How we knowNarrowed deliberately. The word seat DOES occur in the codebase, on volunteer shifts, field-trip rosters, class-section enrollment and library ebook licences -- all different subjects. The claim is about the column, not about the word.

A permissions matrix screen where you tick capabilities per roleThere is no permissions console and no self-serve role editor. The role vocabulary is enforced in the database, which is stronger than a screen, and it is also less convenient, and both of those are true at once.
An access-review report that certifies who had what, whenThe row carries an effective date range, so the underlying facts exist. There is no report screen that assembles them into a review packet, and calling the raw columns an access review would be a stretch.
A single sign-on connector so seats come from your identity providerNo SSO or roster-sync connector ships for Question. question.management carries that posture in full for a district; this page will not restate a capability it did not measure.
A compliance badge asserting the role model is certifiedThere is no certification to claim, and the constraints described on this page are cited with their real status, including the two that are declared NOT VALID. A guard described as stronger than it is would be worse than no guard, because you would stop checking.

Naming the gaps is how the enforced parts above stay believable. Someone reading this page knows exactly what the model does, exactly what it does not, and exactly which two guards are laid rather than finished.

How a seat is created, today

Five steps, and the fourth is the one people get wrong

There is no invite screen, so this is the real process in the order it actually happens. Step four is the one worth reading slowly: it is the place where a comfortable sentence would be false, and where we have written the exact one instead.

Step 1

Someone asks, in words

A studio owner or a district tells us who they want to add and what that person is meant to do. There is no form on this page and no invite screen in the product; this step is a conversation, and it is the step where the interesting questions get asked.

Step 2

We decide the tier before the role

The first decision is not what the person is called but what they are bound to: a studio, a school, or a district. Exactly one, because the database will refuse anything else. Getting this wrong is the mistake that quietly grants too much, so it is settled first.

Step 3

We write the row

We create the membership with its tier, its role from the enum, its finance flag set deliberately rather than by default, and its start date. If the arrangement has a known end, the end date goes on at the same time rather than being left for later.

Step 4

An account exists, separately, and here is the honest part

Signing in is a different mechanism from holding a seat, and it is worth being exact rather than reassuring. Our magic-link verification does create an account from an address it has proved control of. What it does not do is infer a place inside your organization from that address: the code's own comment is that no school membership is inferred from an email the link only proved control of. An account is created. A seat is not. Anyone telling you nothing gets created would be wrong, and we would rather be the ones to say so.

Step 5

Reach follows the row, not the request

From then on, what that person can read and author is derived on the server from the seat they hold. They cannot widen it by asking differently, because the owner is never taken from the request, and an identifier belonging to another account comes back as a plain not-found.

Which reader are you?

A page about who may hold a seat is read by the person being offered one, the person granting it, a counsel, a security reviewer, a parent, and a reporter, and they do not want the same thing. Here is where each should start.

Who you are, where to start on this page, and why.
You areStart withBecause
Someone being offered an associate seatAnatomy of a seat, then How a seat is createdSee exactly what you would be given, and exactly what it does not include, before you accept it.
A studio owner about to add someoneHow a seat is created, then What we are honest aboutThe tier decision comes before the role, and the absent pieces determine what you still have to do by hand.
A school or district counselWhat is enforced, then Trust & dataRead the constraints with their stated limits, including the two declared NOT VALID, and check them against the migrations named.
A security reviewerWhat is enforced, then Anatomy of a seatThe interesting properties are server-derived scope, the uniform not-found on a cross-account identifier, and the second wall in the database.
A parent or guardianTrust & dataSee how a minor is treated in this model, and where the guard is a floor being laid rather than a finished audit.
A reporter or analystWhat we are honest aboutCheck a claim against what we state plainly, including the sentence on this page that we deliberately refused to write.

Trust & data

What a seat can and cannot reach

The interesting trust question about a seat is not what its holder intends but what the system would permit them to do on a bad day. Rather than an absolute promise, here is the scoped custody register the family states, applied to a seat holder.

A seat scopes what a person can reach

Every response and question set on the engine carries an owning organization column, and the seat a person holds is what resolves them to one. There is no cross-organization read path that a seat can be widened into.

Evidenceowner_org_id NOT NULL on qt_realm, qt_question_set, qt_question, qt_share_link and qt_submission, each a foreign key to partner_org(id), migration 1103_question_technology; enforced in the database by the per-owner policy qt_submission_owner_iso -- USING (is_system() OR owner_org_id = current_studio_id() OR owner_school_id = ANY(current_school_ids())) -- migration 1140_qt_owner_anchor_rls. Withdrawal: revoke or retarget the membership row and the resolution stops.

A response carries no identity column

The row a response is written to has no field for a name, email, IP address, account, fingerprint, or session. An associate who can read responses is reading rows that never held a person's identity in the first place.

Evidenceqt_submission holds exactly four columns -- id, owner_org_id, submitted_at, moderation_state -- migration 1103_question_technology. That column set is asserted against the REAL applied table through information_schema by packages/db/src/migration-1103-zero-pii.test.ts, so adding an identity column turns that test RED rather than passing review.

The response time is coarsened to the hour

A stored response keeps only the hour it arrived, enforced by a database check, so a precise moment cannot be matched against the moment a link went out. This holds regardless of how senior the seat reading it is.

Evidenceck_qt_submission_submitted_at_hour_coarse: CHECK (submitted_at = date_trunc('hour', submitted_at)), alongside DEFAULT date_trunc('hour', now()), migration 1103_question_technology. The DEFAULT coarsens and the CHECK makes precision impossible to store. Unlike the two constraints this page narrows, it is a plain valid CHECK with no NOT VALID caveat.

A minor is walled from commerce authority

A student, student ambassador, or minor playwright cannot hold a seat carrying the finance flag. Stated with its real status: that check is declared NOT VALID and its validation step is deliberately unrun, so it guards new and updated rows rather than certifying every historical one.

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. The VALIDATE statement appears in that migration only as a commented-out instruction, which is why the claim above is narrowed rather than stated proudly.

A minor's record is consent-gated

A minor's name, face, and record are consent-gated and not publicly browsable, and consent can be withdrawn. Holding an associate seat does not change that, and no seat exists that lifts it.

Evidencestudent_consent, unique on (student_id, purpose), with state defaulting to pending -- pending is NOT granted, so the gate fails closed. Withdrawal is a recorded event rather than an erasure: student_consent_ledger, migration 0547_consent_immutable_ledger, carries ck_student_consent_ledger_event_kind CHECK (event_kind IN ('grant','withdraw')), a SHA-256 prev_hash / entry_hash chain, an application grant of SELECT and INSERT only, and the BEFORE UPDATE OR DELETE trigger trg_student_consent_ledger_append_only, which raises.

No certification claim

FERPA does not certify products, so we claim no certification and make no blanket compliance claim. The product is designed to support an organization's obligations, and its data practices are documented for review.

EvidenceThis row asserts a refusal rather than a capability, so its artifact is the guard that keeps the refusal true: question-associates.test.ts denies the certification-claim string class over this rendered body, keyed separately on FERPA, on COPPA, and on a blanket-compliance phrase, and every ban carries both a positive control and a negative-universality arm.

What if an organization needs an export or a deletion? Both exist today, by direct request: an administrator can ask for a full export of that organization's question sets and responses, or for the organization's data to be deleted, and either is handled by a person on the team rather than a self-serve button.

This describes the intended model and the constraints that back it, with their real status; it does not promise certified compliance, zero logging, or automatic deletion, and it claims no FERPA or COPPA certification.

Money, said plainly

Money is off on this page

There is no price on this page and no checkout on it. Nothing here takes a payment, captures a card, or converts anyone to a paid plan. There is also no per-seat figure, and the reason is structural rather than coy: there is no seat-count column on an organization to bill against.

This page offers no price, checkout or payment action. The contact link creates no purchase, subscription or per-seat charge on this page.

Isn't this just a way to avoid naming a number? No. The honest reason there is no per-seat price is that per-seat billing has not been built, and we would rather say so than publish a figure this page has no way to charge on.

FAQ

Questions people actually ask before accepting or granting a seat

What is an associate seat, concretely?

A membership row bound to exactly one tier — a studio, a school, or a district — carrying a role from a database enum, an optional finer staff role, a separate money-authority flag, and a start date with an optional end. There is no second object behind it; the row is the seat.

Can I invite an associate myself?

Not today. There is no invite table in the schema and no invite screen in the product, so a partner-tier seat is created with us rather than by a link you send. We would rather say that than show a button that does not exist.

So nothing creates an account without you? Be exact.

No, and this is the question we want to get exactly right rather than comfortably. Our magic-link verification does auto-provision an account from an address it has proved control of. What it does not do is infer a place inside an organization from that address; the code’s own comment is that no school membership is inferred from an email the link only proved control of. An account is created. A seat is not. Those are different objects and we will not let the second hide behind the first.

How many seats can we have, and what does each one cost?

There is no seat-count column on an organization and no per-seat billing, so there is no number to quote and nothing on this page takes a payment. Money is off here. What an arrangement costs is a direct conversation, because there is no self-serve tier to price against.

Can an associate see another organization's data?

No. The owning account for any read or write is derived on the server from the seat, never read from the request, and a database policy scopes the rows underneath that. An identifier belonging to another account returns a plain not-found rather than a permission error that would confirm it exists.

How do we remove someone?

The membership row carries an effective-until date, so a seat that ended is a fact with a date on it. The ordering of those dates is enforced by the database; setting the date is still a human act, and we are not going to describe an unset end date as an automatic one.

Can a student hold a seat that touches money?

No, and it is written as a database check rather than a review habit: a student, student ambassador, or minor playwright may not carry the money-authority flag. Said precisely, that check is declared NOT VALID and its validation step is deliberately unrun, so it guards new and updated rows from the point it landed. It is a floor being laid, and describing it as a completed audit would be exactly the overstatement this page exists to avoid.

Is there a rule stopping a student holding an adult staff title?

Not as a landed database constraint. One was designed alongside the money-authority check and is deliberately held back, because the finer staff-role vocabulary includes publication masthead titles a student editor legitimately holds. Whether a student may hold a masthead title on the membership row is an open product question, and we would rather show you the open question than a wall that is not there.

Do you support single sign-on so seats come from our identity provider?

No connector ships for Question. question.management carries that posture in full for a district; this page will not restate a capability it did not measure itself.

Is any of this AI?

No. Nothing here is AI. No model decides who gets a seat, scores a response, or grades a student.

How is question.associates different from question.management?

question.management is about rollout across buildings: what a district gets from the org-scoped model and what it still has to ask for by hand. question.associates is about the seat itself — who may hold a place inside an account, how that place is recorded, and what it does not grant. One is about spread; this one is about standing.

Can I verify the claims on this page?

The evidence lines name the exact constraints, columns and migrations so those claims can be checked rather than believed, and the limit lines name where a guard is narrower than its headline. Any other claim here can be raised with us directly.

Contact

Talk to the people who run this

Whether you are weighing a seat you have been offered, deciding whether to grant one, checking the constraints against the migrations named above, or checking a claim on this page, the door is the same one, and a person reads it.

Email [email protected]. There is no form on this page, no card asked for, and no student data requested — please do not send any.

You might worry this address queues into a ticketing system nobody reads. It doesn't: there is no ticketing system and no bot behind it, and a person on the team reads every message sent here.

Powered by question.technology · a Stanley Studios venture · money is off · nothing here is AI.

What this page does not claim

Question's response engine runs in production today, a seat is a real row bound at exactly one tier, and the reach that row confers is derived on the server rather than taken from the caller. Those are the strongest true things here.

There is no invite table, no seat-count column, no per-seat billing, no permissions console, no self-serve role editor, and no access-review report. A partner-tier seat is created in a conversation with us.

Two constraints named above are declared NOT VALID, including the one that keeps a minor away from money authority; its validation step is deliberately unrun and the migration instructs an operator to count live violators first. A companion rule about students holding adult staff titles was designed and is deliberately held rather than landed. We would rather show you the open question than a wall that is not there.

And the sentence we refused to write: it would have been comfortable to say nothing here creates an account for a stranger. That is false. Magic-link verification does auto-provision an account from a verified address. What it does not create is a membership, and a seat is a membership. An account is not a seat, and we will not let one hide behind the other.

Money is off: no price and no checkout. Nothing here is AI. A minor’s record is consent-gated and not publicly browsable, and we claim no FERPA or COPPA certification.