People
Packages: packages/domain/people, packages/application/people
Purpose
The People context is the authoritative source of identity for everyone associated with the organisation. All other contexts that reference a person (Scheduling, Training, Billing, Compliance) do so via the identifiers defined here.
Entities
Person (aggregate root)
The central entity. A Person represents an individual formally associated with the organisation. Every other Type (Student, Instructor, Staff, Admin) is a set of attributes overlaid on a Person — not a separate entity type.
Key attributes:
id— unique identifiername— full nameemail— contact email (unique within the organisation)phone— optional contact numbertypes— set of active Types (student,instructor,staff,admin)status—active|inactive
Value objects
- PersonId — typed identifier wrapping a UUID; prevents mixing up IDs across contexts.
- Email — validated email address; enforces format at construction.
- PersonType — enum of
student | instructor | staff | admin. Defined inpackages/domain/people; mirrored (not imported — see package-boundaries.md) bypackages/shared/permissionsfor permission computation.
Domain rules
- A Person must have at least one contact method (email required).
- Email must be unique within the organisation.
- Types are additive — a Person can hold multiple Types simultaneously.
- An inactive Person cannot be assigned to new Bookings.
- A Person cannot be hard-deleted if they have associated Bookings, Lessons, or Charge Items; deactivation is used instead.
- The
adminType should only be granted by a caller who already holds it — enforced as a policy check inregisterPersonandassignType(application layer), not in the domain functions. Exempt during bootstrap (the very first Person registered, before any admin exists — seesetup.astro).
Note: Type here is organisational involvement, not a decoupled Role/permissions system. See the Type vs Role distinction in ubiquitous-language.md.
Key use cases
registerPerson— create a new Person with initial type assignment.updatePerson— update profile details.assignType— add a type to an existing Person.removeType— remove a type (subject to constraint checks).deactivatePerson— mark as inactive; prevents future bookings.
Persistence
people— one row per Person (id, name, email, phone, status,auth_user_idFK →auth_users, timestamps). Noroles/typescolumn here.people_type— reference table listing the valid type codes (student,instructor,staff,admin) with a human-readable label.person_type_assignments— junction table (person_idFK →people,people_type_idFK →people_type, composite primary key) recording which Types a Person currently holds. This is what backs thetypesarray on the domain entity — a Person can have any number of rows here.
Person onboarding flow
New people are created by an admin in the console (/people/new). There is no self-registration. The flow:
- Admin fills in name, email, and types (no password).
- Server creates the Person record and an auth user with a random placeholder password.
- A password-reset token is issued immediately with
mode=invitein the redirect URL. - Better Auth’s
sendResetPasswordhook detects the invite mode and sends an invitation email to the person. - The person clicks the link, sets their own password, and is redirected to
/login.
The placeholder password is never revealed. The person always sets their own password on first access. See email architecture for the full flow diagram.
Cross-context relationships
| Context | Usage |
|---|---|
| Scheduling | References PersonId for booking owner and instructor assignment |
| Training | References PersonId for student and instructor on a Lesson |
| Billing | References PersonId as the invoice recipient |
| Compliance | References PersonId to track currency requirements |
| Notifications | Uses Person contact details (email, phone) for delivery |
The People context does not import from any other domain context. It owns identity; other contexts reference it.