Ulsav Docs

Requirements

Requirement kinds, subjects, gates, inheritance, and how eligibility is evaluated at checkout and check-in

A Requirement is a configured condition that must be satisfied before a named gate. Changing any one of its five coordinates makes a different condition.

CoordinateValues
ScopeORGANIZATION, EVENT_GROUP_SERIES, EVENT_GROUP, EVENT_SERIES, EVENT, PASS
KindIDENTITY_COMPLETE, EMAIL_VERIFIED, PHONE_VERIFIED, AFFILIATION, FORM, CONSENT
SubjectBUYER, PARTICIPANT, EVERY_PARTICIPANT, REGISTRATION
GateAT_SELECTION, BEFORE_PAYMENT, BEFORE_ACTIVATION, BEFORE_CHECKIN
EnforcementSOFT or HARD

Definitions are versioned: editing a Requirement archives the prior definition and writes a new one, so an evaluation can name the definition it was decided under.

Kinds

Four kinds evaluate. Two exist in the model but are not implemented.

KindWhat the evaluator checks
IDENTITY_COMPLETEThe subject Account has at least one email identity and one phone identity
EMAIL_VERIFIEDThe subject Account has an email identity, optionally restricted to the domains in kindConfig.domains
PHONE_VERIFIEDThe subject Account has a phone identity
AFFILIATIONA matching, unexpired eligibility with the configured provider id, email domain, or claim
FORMNothing. The evaluator returns UNKNOWN
CONSENTNothing. The evaluator returns UNKNOWN

The identity adapter counts every email and phone identity on the Account, including a provisional identity created by admin issuance. It does not filter on verifiedAt.

AFFILIATION is the general audience gate: a provider assertion is the highest assurance, a verified email domain is the fallback, and a claim is the loosest. The absence of a provider is what makes domain gating a fallback rather than a separate concept.

Subjects

SubjectResolved to
BUYERThe purchasing Account
PARTICIPANTThe Account ids supplied for the participant set
EVERY_PARTICIPANTThe same set, required to satisfy the condition in full
REGISTRATIONNo Account. The evaluator returns UNKNOWN

At checkout, the participant set is the buyer's Account. PARTICIPANT and EVERY_PARTICIPANT are therefore evaluated against the buyer, not against each Participant's own Account. At check-in, the scanned Participant's Account is supplied instead.

Because the buyer is often not the attendee, an audience gate is authored as EVERY_PARTICIPANT by default, but the checkout evaluation does not yet distinguish the buyer from the roster.

Gates

GateWhere it is evaluatedEffect
AT_SELECTIONThe anonymous Pass-lock read onlyRenders a Pass locked; checkout does not check it
BEFORE_PAYMENTCheckout, before the Order or payment intent existsBlocks the Order
BEFORE_ACTIVATIONThe same checkout call, alongside BEFORE_PAYMENTBlocks the Order before payment, not after it
BEFORE_CHECKINEach scan, against the scanned Participant's current AccountBlocks admission; the Pass stays issued

AT_SELECTION is advisory. A client that calls checkout directly does not pass through it, and the server does not re-check it there.

Enforcement

SOFT

  • Renders a nudge
  • Records the unmet state
  • Never blocks

HARD

  • Blocks the command at a gate the server actually evaluates
  • At checkout, returns a 409 problem with the generic message "A required condition is not met for this Pass"
  • At check-in, records requirements_incomplete

The 409 does not carry the Requirement's id, kind, or guidance. The evaluator collects that list internally, but the registration API replaces it with the generic problem.

Ulsav's completeness prompt is a platform-owned IDENTITY_COMPLETE Requirement with SOFT enforcement, appended to every evaluation chain. An Organization may not make it HARD; it adds its own Requirement instead.

Inheritance

Requirements apply additively down the scope hierarchy, mirroring the stored RoleAssignment scopes:

OrganizationEventGroupSeriesEventGroupEventEventSeriesPass
Diagram source
flowchart TD
O[Organization] --> GS[EventGroupSeries]
GS --> G[EventGroup]
G --> E[Event]
O --> RS[EventSeries]
RS --> E
O --> E
E --> P[Pass]

A Pass is evaluated against the union of the Organization, ancestor group or series, Event, and Pass Requirements. There is no negation: access is strengthened by adding a Requirement, never by subtracting one.

Moving an Event changes its inherited set immediately. The move is an ordinary Event update with no confirmation step and no audit record.

Evaluation

One server-authoritative evaluator answers three questions for a subject at a gate:

  • whether each applicable Requirement is SATISFIED, UNSATISFIED, or UNKNOWN,
  • which artifact satisfied it,
  • the aggregated hard blockers.

The same evaluator backs the anonymous Pass-lock read, checkout, and check-in. Registration creation, payment confirmation, and activation do not run it separately. Results are recomputed on every call; there is no evaluation cache.

Snapshots and freshness

Checkout writes each Registration's Requirement evaluation snapshot, including the definitions evaluated and their states. Later edits do not retroactively invalidate a confirmed Registration.

BEFORE_CHECKIN is the exception by design: it is re-evaluated at scan time against current eligibility. An affiliation that expires between purchase and the door blocks admission, and the Pass remains issued.

Interfaces

A Requirement can be created, read, edited, and archived at any of the six scope types. The create path does not verify that the scope id exists in the Organization, so a wrong or foreign id produces a definition that never matches an evaluation chain.

The API checks requirements.read and requirements.manage at Organization level, not at the Requirement's scope, and RoleAssignments have no Pass scope.

The effective lock set for a Pass is anonymously readable through GET /api/v1/events/{event_id}/pass-requirement-locks, so the register surface locks before sign-in. Checkout returns the generic 409 described above; the unmet list is not included.

Invariants

  • A HARD Requirement blocks the gates the server evaluates. It does not block AT_SELECTION at checkout.
  • A Pass is not payable while a BEFORE_PAYMENT or BEFORE_ACTIVATION Requirement is unmet, whatever the client sends.
  • An unnamed Participant cannot satisfy BEFORE_CHECKIN, because the scan has no Account to evaluate.
  • Requirement versions are immutable; edits archive and replace.
  • Evaluation is tenant-scoped, and a Requirement from another Organization never contributes.

On this page