Settings → Members
Purpose
Invite clinic members, assign their access role + clinic position, edit DEA license metadata for prescribers, remove members, and transfer clinic ownership. The Members page is the source of truth for everyone authorized to operate against this clinic's controlled-substance records.
Regulatory basis: DEA recordkeeping (each registrant maintains, on a current basis, a complete and accurate record of each substance; records maintained at the registered location include the personnel records of those handling controlled substances).
When to use it
- Onboard a new clinic member: invite them via email; pick the access role (default
staff); they complete their clinic position during onboarding - Promote / demote: change a member's access role (admin / manager / staff / viewer)
- Add prescriber metadata: when a member's clinic position is
prescribing_veterinarian, edit their DEA license number + expiration so the dispense flow's prescriber filter surfaces them as eligible per DEA rules - Offboard: remove a member when they leave the clinic (their personal account stays; their clinic membership is deleted; transactions they performed remain in the audit trail with their auth.user_id)
- Transfer ownership: hand the clinic over to a new owner (OTP-gated; rare)
Walkthrough
Step 1 — Open the members page
Navigate to /home/<slug>/members. The page lists every clinic member + every pending invitation.

Step 2 — Invite a member
Click Invite Members in the top-right (data-test="invite-members-form-trigger" — visible only to owner / admin).
The dialog opens with a single email row + an access-role select (AccessRoleSelect, 4 values: admin / manager / staff / viewer — owners are minted only via the Transfer Ownership flow).
Add additional rows via Add Another Email (max 5 invites per send per the subscription user-quota policy). Submit.
What happens behind the scenes:
- The action validates each email + access role under Pattern A (admin client + explicit
account.invitepermission gate) - An
invitationsrow is inserted withaccess_roleset from the form;clinic_positionstays NULL until the invitee records it at onboarding - An invitation email goes to each address (Mailpit in dev; Resend in production)
- The recipient clicks the link; signs in (or signs up); accepts the invitation; the
accept_invitation(...)RPC creates theaccounts_membershipsrow - The recipient is then redirected to
/onboarding/clinic-positionto capture their clinic position + (if prescriber) DEA license
The clinic position is NOT captured at invite time — it's captured at onboarding so the new member chooses their own position rather than the inviter guessing.
Step 3 — Edit a member's clinic role
Click the row's actions menu → Edit Clinic Role. The dialog (edit-clinic-role-dialog) shows two fields: Clinic Position + DEA License (visible only when position = prescribing_veterinarian).
Update; submit via the Save button (data-test="save-clinic-role-button").
ℹ️ The access role (admin / manager / staff / viewer) is updated separately via the Membership Role Selector on the same row (
data-test="role-selector-trigger").
Step 4 — Edit a member's access role
The role-selector dropdown on the row drives accounts_memberships.access_role directly. Change between admin / manager / staff / viewer. Demoting a member to viewer immediately hides every write affordance from their UI — no logout needed (the SSR loader re-evaluates on next navigation; the page re-renders with <If can=...> gates closed).
Step 5 — Remove a member
Click the row's actions menu → Remove. The confirmation dialog (remove-member-dialog) explains the implications — the member loses access immediately; transactions they performed are preserved in the audit trail. Click Confirm Remove (data-test="confirm-remove-member").
Step 6 — Transfer ownership
Click the row's actions menu (only available on non-owner rows; the existing owner is the source) → Make Owner. An OTP dialog opens; the existing owner enters the 6-digit code emailed to them.
⚠️ Transfer ownership is OTP-gated and irreversible by design (the new owner can re-transfer, but the original owner cannot un-transfer). Use sparingly.
Field reference
Invite form (invite-members.schema.ts)
| Field | Label | Type | Required |
|---|---|---|---|
email |
✓ | ||
role |
Access Role | enum (admin / manager / staff / viewer) |
✓ |
An invite asks only for the email address and the access role. Clinic role, DEA number, and employment start date are captured when the new member first signs in, not here.
Edit Clinic Role dialog
| Field | Label | Type | Required |
|---|---|---|---|
clinic_position |
Clinic Position | enum (practice_manager / prescribing_veterinarian / veterinary_technician / veterinary_assistant / receptionist / other) |
✓ |
dea_license |
DEA License Number | text | required when clinic_position = prescribing_veterinarian |
dea_license_expiration |
DEA License Expiration | date | required when clinic_position = prescribing_veterinarian |
Membership table
accounts_memberships row per (user, account) pair:
user_id— auth.users(id)account_id— team accountaccount_role— MakerKit-internalowner/member; not surfaced to usersaccess_role— CS-Logbook-specificowner/admin/manager/staff/viewer(Phase A schema split shipped 2026-05-07 in32a2660d)clinic_position— clinic role:practice_manageretc.is_prescribing_veterinarian— boolean derived from clinic_position
Edge cases
Members are unlimited; billing is driven by DEA-registrant count
CS Logbook Online is a pure per-seat plan (ADR-026) priced on the count of DEA-registered (prescribing) veterinarians — not on member count. There is no per-plan user cap: you can invite as many staff/members as you like without changing your price. Adding a member only affects billing if that member is a DEA-registered prescribing veterinarian (which adds a seat). Invites still require the clinic to have an active subscription; the action fails with subscriptionMissingPlan if the clinic has no active subscription.
Owner cannot leave the clinic
team-account-danger-zone.tsx shows Delete Clinic (only owners) or Leave Clinic (only members). The owner must transfer ownership first if they want to leave the clinic.
Clinic position is captured at onboarding, not invite
The first time a new member signs in, CS Logbook sends them to a short onboarding step to set their clinic position before they can use the app. Until they finish it, you cannot edit their clinic role from this Members page.
Transferring ownership requires a code from your email
Transfer Ownership asks the current owner for a 6-digit code sent by email, so you need access to that inbox to complete the transfer. Start the transfer only when you can retrieve the code.
Members vs the Staff Log (two separate populations)
There are two records for a person, and removing one does not affect the other:
- Membership (
accounts_memberships) — the app login seat. Memberships are unlimited and not billed (billing is driven by DEA-registered veterinarian count, not member count). Remove a member here to revoke their login. - Staff Log (
clinic_staff, the controlled-substance staff register) — the compliance roster, referenced by every transaction the person recorded. It is never deleted; a departed staff member is setstatus = inactive(with an end date) on the Staff page, preserving historical attribution. Many Staff Log entries have no login at all.
Target behavior: deactivating a person on the Staff page should also remove their membership (revoke login + free the seat); reactivation re-invites them.
Removing a member preserves their audit trail
Controlled-substance transactions, disposals, and corrections reference
clinic_staff(id) (the Staff Log), not accounts_memberships(id). Removing a
membership leaves those references intact — the person's Staff Log entry and every
record they touched are preserved; the change-history rows still render their name
from the (never-deleted) clinic_staff row.
Common mistakes
- Inviting a prescriber as
viewer. Viewer cannot dispense; the prescriber filter on the dispense form requires the prescribing_veterinarian clinic_position which is captured at onboarding regardless of access role, but the clinical workflow expects access_role >=staff. - Skipping clinic position. A member with NULL
clinic_positionis redirected to the onboarding gate on every navigation. They must complete it before they can use the rest of the app. - Re-inviting an already-accepted email. The unique constraint on
(account_id, email)blocks duplicate pending invitations. If a member is already accepted, edit their role; don't invite them again. - Demoting yourself to viewer. You'll lose your own write affordances. The owner cannot demote themselves; admins / managers can demote themselves but should be careful — only the owner can re-promote.
Related
- Onboarding → Clinic Position — the redirect gate captures clinic_position on first sign-in
- ADR-014 — access_role + clinic_position split
- Inventory → Transactions § dispense — the prescriber-filter dropdown surfaces members with
is_prescribing_veterinarian = true - Glossary § Access Role + Clinic Position