Data model — for review

Dance studio platform · Phase 0 discovery · what we have decided, inferred, and parked
Draft v0.2
24 October 2026
Tempo 🩰 · for Craig
Decided Proposed Deferred Not modelled yet

The prototype is throwaway. This is the artifact that survives it: the shapes we think the real system needs, and the rules that must hold regardless of how we implement them.

1 · Identity, membership, roles

The load-bearing decision: personas are views, not account types. A human is one row. What they can see comes from relationships and roles, never from a "type".

person one row per human id legal_name · preferred_name date_of_birth email? · phone? · photo no billing fields, ever membership person ↔ studio person_id studio_id status · started_on · ended_on UNIQUE(person, studio) a human may have several studio tenant id · name · branding every row is studio-scoped login exactly one per person person_id passkey / email under-13: PIN, no email membership_role a set, not a column membership_id + role granted_at · granted_by parent + student + teacher role catalog data, not an enum owner · admin · teacher choreographer · student accompanist — add by insert guardian_of person → person (self-relation) guardian_person_id · student_person_id authority · relationship · verified (per studio) 1 : n n : 1 1 : 1 1 : n n : 1 guardian of
The triple-role adult — a parent who takes an adult class and also teaches — is one person, one login, one membership, and three role rows. No duplicate accounts, no "which one am I" at sign-up.

2 · Events and the calendar

A class is not the only thing on a studio calendar, and a calendar is not a table of rows someone maintains by hand. Events are typed, may run several days, may belong to several dancers at once, and may contain their own sub-schedule.

schedule_rule the weekly pattern class / level · day_of_week start · end · room · teacher valid from → to authored once event (instance) a dated happening title · type_id · room · teacher date … until (multi-day = 1 row) start · end members[] ← several dancers overrides[] ← moved / cancelled generated, never hand-entered entry competition / recital number event_id · date · time · dur routine_name members[] ← cast, incl. non-family stage · division one event, many numbers, own rosters person referenced by members[] a competition can include dancers with no profile here exception / override the thing that notifies people event_id · new time / room / status authored_by · reason event_type (reference) data, not an enum class · practice · rehearsal · competition meeting · recital · closure — add by insert generates 1 : n members changes type
Why generated, not stored: if a studio must re-enter the calendar each week, it drifts, and every feature built on it inherits the drift.
Why entries exist: "which number, at what time, who's in it" is the actual question a parent asks. One block per competition cannot answer it.

3 · How permission is derived

who person + membership at this studio + roles (a set) + guardian edges can(person, action, subject) one function, every route union of grants ∩ scope subject is always a person outcome allow / deny inside one studio only scoped to own children, own classes, own data

Holding teacher + guardian does not let a teacher-parent read another family's billing, nor a guardian read notes about other students. Grants union; scopes intersect.

4 · Entity dictionary

personDecided
One row per human, studio-agnostic.
  • id · names · date_of_birthcore
  • email, phone, photonullable
  • billing / payer fieldsforbidden
  • persona / type columnforbidden
Age band is computed from DOB, never stored — birthdays self-heal.
loginDecided
Credentials, one per person across all studios.
  • person_id1:1
  • passkey / passwordadult
  • PIN, guardian-issuedunder 13
  • emailnot required for kids
Under-13 accounts must never depend on a child's inbox.
membershipDecided
A person's relationship to one studio.
  • person_id + studio_idunique
  • statusinvited → active → alumni
  • started_on / ended_onhistory kept
membership_roleDecided
Roles are rows. A person may hold several.
  • membership_id + rolePK
  • owner · admin · teachercatalog
  • choreographer · student · accompanistcatalog
  • guardianderived, not granted
guardian_ofDecided
Person → person. Global relationship, per-studio verification.
  • guardian_person_id→ person
  • student_person_id→ person
  • authorityfull · view-only · emergency
  • verified_at / verified_byper studio
Studio A verifying a guardian must not grant access at studio B.
eventProposed
A dated happening, generated from patterns.
  • title · type_id · room · teachercore
  • date … untilmulti-day = 1 row
  • start · endlocal time
  • members[]several dancers
Never hand-maintained: pattern + exceptions generate instances.
schedule_ruleProposed
The weekly pattern a studio authors once.
  • class / levelreference
  • day_of_week · start · endpattern
  • room · teacherdefaults
  • valid_from → valid_toseasonal
exceptionProposed
A dated change to a generated instance.
  • event_id→ event
  • new_time / new_room / cancelleddelta
  • authored_by · reasonaudit
The exception — not the event — is what fires a notification.
entryProposed
A number inside a competition or recital.
  • event_id · date · time · durown schedule
  • routine_namedisplay
  • members[]cast, may be outside family
  • stage · divisionlogistics
Same shape serves recital order-of-show.
event_typeDecided
Reference table. Studios invent types.
  • class · practice · rehearsalseed
  • competition · recital · meetingseed
  • closuresuppresses attendance
Type drives behaviour (attendance, travel, permissions), not just colour.
billing entityDeferred
Not being built. One rule is being kept alive.
  • money attaches to a billing entitynot to person
  • may hold several peoplesplit tuition
  • per studiotwo schools, two relationships
No payer / balance / discount columns on person or membership, ever.
attendanceNot modelled
Held back deliberately, but it gates a lot.
  • event_instance + studentshape tbd
  • present · absent · late · makeupstates tbd
If recording it is slow, every claim about a dancer's week becomes guesswork.

5 · Invariants worth defending

  1. No persona columns.Behaviour comes from relationships and roles. The moment a "type" exists, the adult student, the teacher-parent, and the grandmother-payer all become exceptions.
  2. Roles are a set; grants union, scopes intersect.Holding two roles never widens what data you may see beyond the scope of each role.
  3. One human, one login, many studios.Identity is global; authority is per studio.
  4. The calendar is derived.Patterns generate instances; exceptions are the only edits. A hand-maintained calendar is a calendar that lies.
  5. Events belong to rosters, not to a dancer.A competition with two siblings and three guests is one event with a cast, not five events.
  6. Type is data, and type drives behaviour.Closure means "don't mark absent". A meeting has no attendance. A competition has travel.
  7. Money never lives on a person.The only billing decision made so far is where billing is allowed to attach later.

6 · Open, and honestly unresolved

instances vs definitionsOpen
Until instances are first-class, "this rehearsal moved" cannot be expressed cleanly or notified reliably.
attendanceOpen
Target: under 15 seconds per class, or the data rots and everything downstream inherits it.
permission policyOpen
Hooks exist; no policy rows written. Teacher-parent conflict rules land here, later.
guardian verificationOpen
In-person at the desk, email invite, or both — decides the onboarding screen count.
closuresOpen
Thanksgiving currently looks like empty days, which reads as a scheduling mistake rather than "we're closed".
duplicate peopleOpen
Studios already hold duplicates. Merge tool in v1, or accept them until it hurts?