Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesTrialsTrials — Data Model

Trials — Data Model

One migration: libs/db/drizzle/0184_trials.sql.

staff_notes (new)

Schema: libs/db/src/lib/schema/staff-notes.ts.

ColumnTypeNotes
iduuid PKgen_random_uuid()
organization_iduuid NOT NULL → organizations ON DELETE CASCADE
membership_iduuid → memberships ON DELETE CASCADENote on a member
organization_lead_iduuid → organization_leads ON DELETE CASCADENote on a lead; kept after conversion
bodytext NOT NULL1..2000 chars (API-validated, trimmed)
importantboolean NOT NULL DEFAULT falseShown on the roster and in the coach brief
author_user_iduuid → users ON DELETE SET NULL
created_at, updated_attimestamptz NOT NULL DEFAULT now()
deleted_attimestamptzSoft delete
  • CHECK staff_notes_subject_chk: membership_id IS NOT NULL OR organization_lead_id IS NOT NULL.
  • Indexes: staff_notes_membership_idx (membership_id), staff_notes_organization_lead_idx (organization_lead_id).
  • Carry-over is a read rule, not a copy: member reads include notes whose organization_lead_id is the lead with organization_leads.converted_membership_id = that membership.

lead_trial_appointments (new columns)

ColumnTypeNotes
class_session_iduuid → class_sessions ON DELETE SET NULLThe class the trial sits in; null for a trial without a class
checked_in_attimestamptzCoach check-in evidence; never an outcome
checked_in_by_user_iduuid → users ON DELETE SET NULLWho checked them in
  • Partial index lead_trial_appointments_session_idx (class_session_id) WHERE superseded_at IS NULL.
  • Seat rule: a row holds a seat of class_session_id while outcome IN ('pending', 'attended') AND superseded_at IS NULL (holdsTrialSeat, SEAT_HOLDING_TRIAL_OUTCOMES).
  • outcome = 'converted': the lead converted before the class and the seat became their booking (bookings.trial_appointment_id). Not superseded, holds no seat.
  • A checked-in trial ended by a lead path or a class cancel gets outcome = 'attended' and stays unsuperseded; other ended trials get cancelled/rescheduled and superseded_at.
  • starts_at equals the session’s start for a session trial and follows it when the session moves.
  • Check-in and undo bump version, like every appointment write.

bookings (new columns)

ColumnTypeNotes
trial_marked_by_user_iduuid → users ON DELETE SET NULLA staff member marked the booking a trial; null = not marked
trial_appointment_iduuid → lead_trial_appointments ON DELETE SET NULLThe lead trial seat this booking took over on conversion. Never revoked by the entitlement sweep

plans (new columns)

ColumnTypeNotes
is_trialboolean NOT NULL DEFAULT falseBookings paid with the plan show as trials. Drop-in plans only: CHECK plans_trial_drop_in_chk (NOT is_trial OR type = 'drop_in')
trial_once_per_personboolean NOT NULL DEFAULT trueTrial plans only (meaningless when is_trial is false): a member self-serve purchase is refused (409 trial_already_used) when the person already held any trial plan of the org. Staff enrollment is never refused.

expiresInDays (existing plans.expires_in_days) now also applies to trial drop-ins — the trial’s “use within” window — besides class packs.

class_sessions (new column)

ColumnTypeNotes
trial_brief_sent_fortimestamptzThe starts_at the coach brief went out for. Brief is due when it IS DISTINCT FROM starts_at, so a moved class is re-armed.

Not stored

  • A roster entry’s trial reason — derived on every read from bookings.trial_appointment_id, bookings.trial_marked_by_user_id and the booking’s plan is_trial, in that precedence. API-internal: no response carries it.
  • Health-declaration state — derived from the org’s health_declaration compliance form and its submissions.

Shared schemas

libs/shared/src/lib/schemas/trials.schema.ts: staffNoteSchema, createStaffNoteSchema, updateStaffNoteSchema, ROSTER_ENTRY_KINDS (booking, lead_trial), TRIAL_APPOINTMENT_OUTCOMES (incl. converted), rosterTrialSchema ({ lead }, no reason), rosterEntrySchema (canMarkTrial, canUnmarkTrial), sessionRosterResponseSchema, markBookingTrialSchema, trialsTodaySessionSchema, trialSessionOptionSchema, createMemberTrialBookingSchema, memberTrialBookingSchema. The trial error codes (trial_session_full, trial_session_unavailable, trial_not_pending, trial_not_markable) are API-side.

plan.schema.ts: isTrial on createPlanSchema (default false), updatePlanSchema, planResponseSchema; trialOncePerPerson on the same three (create: optional, server default true; refused on non-trial plans; reset on un-flag); expiresInDays refined to class packs and trial plans (isExpiryAllowedForPlan). Helpers: isTrialAllowedForType, isExpiryAllowedForPlan, consumableValidityEnd (the window stamped on activation), planKind / PlanKind ('trial' for a trial drop-in — how staff surfaces label a plan). Codes: TRIAL_REQUIRES_DROP_IN, TRIAL_PLAN_IN_USE.

payment-error-codes.ts: PaymentErrorCodes.TRIAL_ALREADY_USED = trial_already_used.

sales-workspace.schema.ts: salesResultInputSchema.trial is { startsAt?, classSessionId?, type } with exactly one of startsAt / classSessionId.

notification-prefs.ts: category trials (opt-in by default, like every category).