Specialists Feature
Medical professionals who conduct appointments and complete clinical notes.
NOT BUILT — reconciled 2026-08-02
There is no specialists table, no specialist_specialties, no specialist_locations, no Go domain, no routes, and zero specialists.* permission rows in any migration. This page is a design spec.
Ships in migration 000040 — the first migration of the F1–F6 port from the live restartix-leo-* systems. See features.md → F1, platform-completion.md, and leo-port-map.md §3 → F1.
Corrections applied on this page:
- There is no
userstable and nouser_idcolumn. The link to a login account isspecialists.human_id UUID UNIQUE NULL REFERENCES humans(principal_id).human_id NULLis the deliberate calendar-only specialist state. See decisions.md → Why principals as the root identity. - Authorization is per-org permission codes, never a global
role = 'specialist'check. The "specialists see only their own patients" behaviour is an RLS + permission outcome, not a role string. calendar_specialists.override_weekly_hoursJSONB is proposed-not-settled — see the open decision noted in the scheduling section below.- The licensing/credentials section describes a design that has never been built and is not in the F1 port scope. It is retained as a design record; see the note there.
What this enables
Team management: Add doctors, therapists, and nurses to your clinic with their bio, photo, and professional credentials displayed to patients.
Scheduling profiles: Each specialist has their own availability (Mon 9am-5pm, Tue 2pm-8pm), timezone, and can work multiple clinics.
Public profiles: Patients see specialist names, titles, and bios when booking (e.g., "Dr. Smith, MD — Cardiologist").
Role-based access: Specialists with a login account can view their own patients and appointments, complete clinical notes, but see nothing from colleagues.
Licensing verification: Track professional licenses with expiration dates. The platform alerts clinic admins when a license is about to expire and can restrict a specialist's ability to accept bookings if their license lapses.
How it works
- Admin adds a specialist: Name, title, timezone, photo, bio
- Admin enters license details: License number, issuing body, expiration date
- Admin sets availability: "Available Mon-Fri 9-5 in Europe/Bucharest timezone"
- Admin assigns to services: "Dr. Smith can do Cardiology consultations"
- Admin optionally links user: Creates login credentials so specialist can access the app
- Patients book: See Dr. Smith's profile, choose a time from her available slots
- Specialist works: Logs in, completes appointment notes, sees only their own patients
Licensing & Credentials
Design record — NOT in F1 port scope
None of the license_* columns below exist, and none of them appear in the F1 port plan or in data-model.md Area 2. The live leo system does not track licences either, so there is no operational behaviour to port. This section is a design that was written ahead of a need.
If it is pulled into scope later it lands as its own increment: the columns, an expiry cron, and a bookability input alongside scheduling_active. Note that the expiry-alert mechanism described below depends on F7 Automations, which is out of scope — the alert would ride internal/core/notify directly instead.
The platform tracks specialist professional licenses to help clinics meet their legal obligations. Under Romanian law (Legea 95/2006), healthcare practitioners must be registered with their professional body and hold a valid practicing license (aviz de liberă practică).
What the platform tracks
| Field | Purpose |
|---|---|
| License number | The official registration number from the professional body |
| Issuing body | Which professional body issued the license (e.g., OBBCSSR for kinesitherapists, CMR for physicians, OAMGMAMR for nurses) |
| Issue date | When the license was issued |
| Expiration date | When the license expires and must be renewed |
| License document | Uploaded copy of the license (PDF or image, stored in S3) |
| Verification status | pending, verified, expired — managed manually by the clinic admin |
How it works
- Admin enters license details when adding or editing a specialist
- Platform tracks expiration — when a license is approaching expiration (configurable: 30/60/90 days), the system sends alerts to the clinic admin via the automations system
- Expired license warning — when a license expires, the specialist's profile shows a warning badge visible to admins. Optionally, the clinic can configure automatic booking suspension — the specialist stops appearing in booking flows until their license is renewed.
- Audit trail — all license changes (added, updated, renewed) are recorded in the audit log
What the platform does NOT do
- The platform does not verify licenses directly with professional bodies (OBBCSSR, CMR, etc.) — there is no API for this in Romania. Verification is the clinic admin's responsibility.
- The platform does not block clinical actions based on license status — a specialist with an expired license can still access existing patient records (for continuity of care). Only new bookings can be suspended.
- The platform stores licenses as documentation, not as a legal certification. The clinic is ultimately responsible for ensuring their specialists are properly licensed.
Regulatory context
| Country | Requirement | Professional Bodies |
|---|---|---|
| Romania | Legea 95/2006 — healthcare practitioners must hold a valid aviz de liberă practică from their professional body | OBBCSSR (kinesitherapists), CMR (physicians), OAMGMAMR (nurses/midwives), CMDR (dentists) |
| EU (general) | Each member state has its own licensing requirements; the platform's licensing fields are generic enough to support any country's requirements | |
| US (future) | State-level licensing with NPI numbers — same tracking model applies |
Technical Reference
Overview
Specialists represent healthcare providers in the RestartiX Platform system. A specialist is a medical professional who can conduct appointments with patients. Each specialist can have one or more specialties, a scheduling profile for booking appointments, and an associated user account for login access.
What Specialists Are
Specialists are healthcare providers (doctors, therapists, nurses, etc.) who:
- Conduct appointments with patients
- Complete clinical forms (analysis, advice, prescriptions, reports)
- Have their own scheduling profiles (availability, timezone, working hours)
- Can be associated with one or more medical specialties
- May have a linked user account for system access
Relationship to Principals
There is no users table. Every actor in the system is a row in principals; human profiles live in humans (PK principal_id). A specialist is optionally linked to a human via specialists.human_id UUID UNIQUE NULL REFERENCES humans(principal_id):
- With a linked human (
human_idset): the person can sign in. What they can see and do is decided by the per-org permissions granted throughorganization_memberships.role_idplus RLS — not by a global role string. "Specialists see only their own patients" is a policy outcome, not a role check in handler code. - Without a linked human (
human_idis NULL): the deliberate calendar-only specialist state. The specialist exists for scheduling and record-keeping but cannot sign in — useful for external providers, or for a clinic that schedules a practitioner before onboarding them. This state is designed-for, not a degraded case: F4 availability and F5 booking must both work against it.
human_id is UNIQUE (one specialist profile per human per org) and ON DELETE SET NULL — deleting the human leaves the specialist record and its clinical history intact.
human_id is a specialist-account column that semantically requires a human, so it FKs humans(principal_id) rather than principals(id); the FK target enforces the constraint without a CHECK. Actor columns that accept any actor type (created_by_principal_id, audit_log.actor_id) FK principals(id) instead.
Staff onboarding
New team members arrive through the shipped invites domain — POST /v1/organizations/{id}/staff-invitations, a magic-link loop. Do not port leo's add-member form, which sets the new member's initial password directly with confirmed: true and no invite loop.
Scheduling Profile
Every specialist has an optional scheduling profile that enables public booking. The scheduling profile is stored directly on the specialists table:
Scheduling Columns (from scheduling/ feature)
scheduling_timezone(VARCHAR, nullable): The specialist's IANA timezone (e.g.,America/New_York,Europe/Bucharest). All scheduling calculations (weekly hours, overrides) are performed in this timezone. When NULL, the specialist has no scheduling profile and cannot be booked.scheduling_active(BOOLEAN, default TRUE): Whenfalse, the specialist does not appear in booking flows even if they have weekly hours configured.
Related Tables (from scheduling feature)
specialist_weekly_hours: Recurring availability blocks by day of week (e.g., Mon 9:00-17:00, Tue 14:00-20:00). These are the specialist's DEFAULT hours, and they are per-specialist global — a specialist works 9–5 as a person, across every offering.specialist_schedule_overrides: Date-specific exceptions (vacations, extra hours, holidays). Scope is an open decision — see below.calendar_specialists: Assignment to specific calendars, with apriority INTthe assignment engine walks.
Both availability tables carry organization_id NOT NULL and their own RLS policies, and both carry location_id UUID NULL (NULL = remote/telerehab) per the 1B.14 contract. data-model.md Area 4 omits organization_id on both — that is a doc bug against a CLAUDE.md hard rule, not a design choice.
Single-true-availability invariant. A DB-level EXCLUDE USING gist constraint on both tables prevents overlapping availability for one specialist, regardless of location_id. A specialist cannot be in two places at once; locations label availability, they never partition it. (This requires CREATE EXTENSION btree_gist, which is not enabled today.)
Overrides replace, they never merge. If any override exists for a local date, that date's weekly rules are skipped entirely and only the override's intervals apply. An override with zero intervals blocks the whole day — encoded server-side as a single 00:00–23:59 unavailable row. This is how "block Tuesday" is expressed, and it is the behaviour the live system has been running against real bookings.
Open decision — override scoping (blocks migration 000044)
Three platform sources disagree. data-model.md Area 4 has no scope column and sketches an override_weekly_hours JSONB on the junction; scheduling-config.md says appointment_type_id, a table that exists in no architecture doc. The live system settles what clinics actually do: restartix-intakes scopes overrides per-schedule (NOT NULL), and the predecessor per-specialist-global table was explicitly removed in favour of it — a deliberate migration, with production rows behind it.
The proposal on file is specialist_schedule_overrides.calendar_id UUID NULL (NULL = all calendars), which preserves the capability without forcing a read-only-until-a-calendar-is-selected UI, and dropping the override_weekly_hours JSONB on calendar_specialists. Not ratified. Until it is, treat the override_weekly_hours description previously on this page as a proposal, not a contract.
See scheduling-config.md and ../scheduling/index.md for full details on the scheduling system.
Specialty Associations
Specialists can be associated with one or more specialties via the specialist_specialties junction table:
- Specialties define areas of medical practice (e.g., Cardiology, Dermatology, Orthopedics)
- A specialist can have multiple specialties
- A specialty can be associated with multiple specialists
- The junction table includes
organization_idfor direct RLS filtering (denormalized for performance)
This allows filtering specialists by specialty in the UI and helps patients find the right provider.
Organization Scoping
All specialists belong to an organization:
- The
specialiststable includesorganization_idfor direct RLS checks (no sub-queries needed) - Each specialist can only be viewed and managed within their organization's context
- Multi-tenancy is enforced at the database level via Row-Level Security policies
Bookability is derived, never a stored flag
A specialist is bookable when all of the following hold:
scheduling_timezone IS NOT NULL -- P23: no timezone, no scheduling profile
AND scheduling_active -- explicit clinic switch
AND EXISTS (weekly hours for this specialist)This is computed, and the API exposes it as a bookable flag with a machine-readable reason that the roster UI renders. leo instead gated bookability on a lazily-provisioned external ID, so specialists who never got one were silently undroppable from every roster with no way to see why. No silent-failure states.
scheduling_active is not humans.blocked. Deactivating a specialist at Clinic A is one column, one transaction, and must never lock that person out of Clinic B. leo's deactivation is a non-atomic two-write toggle that leaves the scheduler live.
Timezone resolution goes through internal/core/scheduling.ResolveSchedulingTimezone(ctx, locationID, specialistID, orgID) — P23's single fallback chain (location → specialist → org). That helper does not exist yet and ships with F1; F4 and F5 both read it. Timezones are IANA strings, validated on write against tzdata, and served from GET /v1/reference/timezones.
Soft Delete
Specialists use soft delete (deleted_at timestamp) rather than hard delete:
- Reason: medical-record integrity. Historical appointments, signed forms and generated documents keep referencing the specialist. Soft delete is the platform-wide rule for clinical records (CLAUDE.md → Patient Data); GDPR is the day-one driver, HIPAA readiness is a nice-to-have that this happens to satisfy too.
- Enforced structurally: no DELETE RLS policy on the table, and
cmd/check-softdeletein CI. - When a specialist is "deleted", their
deleted_atfield is set to the current timestamp - Historical appointments and clinical records remain intact and continue to reference the specialist
- Active specialists can be filtered with
WHERE deleted_at IS NULL
Key Fields
| Field | Type | Description |
|---|---|---|
id | UUID | Internal primary key |
organization_id | UUID | Organization this specialist belongs to (NOT NULL, indexed) |
human_id | UUID | Linked human principal — UNIQUE NULL REFERENCES humans(principal_id) ON DELETE SET NULL. NULL = calendar-only specialist. (Replaces the user_id this page used to name; there is no users table.) |
name | TEXT | Specialist's full name (required) |
title | TEXT | Professional title (e.g., "Cardiologist", "MD", "Licensed Therapist") |
description | TEXT | Biography or professional description (markdown/HTML) |
slug | TEXT | URL-friendly identifier (required, indexed) |
signature_url | TEXT | S3 key for digital signature image |
avatar_url | TEXT | S3 key for profile photo |
license_number | TEXT | Professional license/registration number |
license_issuing_body | TEXT | Professional body that issued the license (e.g., "OBBCSSR", "CMR") |
license_issued_at | DATE | When the license was issued |
license_expires_at | DATE | When the license expires — triggers expiration alerts |
license_document_url | TEXT | S3 key for uploaded license document (PDF/image) |
license_status | TEXT | pending, verified, expired — managed by clinic admin |
scheduling_timezone | VARCHAR(64) | IANA timezone for scheduling (e.g., "Europe/Bucharest") |
scheduling_active | BOOLEAN | Whether specialist accepts bookings (default true) |
minicrm_name | TEXT | External miniCRM identifier (integration field) |
deleted_at | TIMESTAMPTZ | Soft delete timestamp (nullable) |
created_at | TIMESTAMPTZ | Creation timestamp (auto-set) |
updated_at | TIMESTAMPTZ | Last update timestamp (auto-updated via trigger) |
Assets, limits and search
- Signature images upload to the already-registered
s3.SurfaceSignatures. Avatars need a newSurfaceAvatarsregistration — verified absent frominternal/integration/s3/surfaces.go, which today holds only Signatures, Documents, FormsUpload, Logos, AppointmentFiles and ExerciseAssets. Do not overloadSurfaceLogos; that surface is org branding and is publicly readable. - Both uploads follow the shipped
organization.UploadBrandingAssetsequence verbatim: sniff the content type → content-addressed key → upload → DB write → rollback-delete the new object if the DB write fails → orphan-delete the previous object only after the DB write succeeds. - Seat limit: creation is wrapped in
middleware.EnforceLimit("max_specialists", 1). The limit is already seeded in000004_tiers_subscriptions.up.sql(free 2 / pro 20 / dedicated unlimited) withhard_blocksemantics. - Search:
?q=async typeahead +?ids=resolve from the first commit, backed byGIN (immutable_unaccent(name) gin_trgm_ops). Never load the full roster into a picker — at ~20k migrated users the clinic-side directories are not small. minicrm_nameoverrides the display name in outbound CRM payloads (specialist_name = minicrm_name || name). Same pattern asofferings.minicrm_title.
New shared UI primitives required
Both verified absent from packages/ui and needed by the availability editor:
TimeSelect— 15-minute increments, searchableTimezoneSelect— IANA list, sorted by live UTC offset
Open decisions
humanshas no name column. Verified at000002_tenancy_rbac.up.sql: the table carriesprovider_subject_id, provider_org_id, email, confirmed, blocked, portal_credential_generation, last_activity, preferred_language, timezone— and no display name.specialists.namecovers specialists, but admin and customer-support staff have nowhere to store one, which blocks the team roster UI. Either addhumans.name TEXT NULLwith a classification entry, or accept email-as-display-name. A foundation gap surfaced by the port, not a feature preference.- Route topology —
/team/{principalId}or/team+/specialists/{id}? Calendar-only specialists (human_id NULL) have no principal to route on, which argues for the split. - Does holding a specialty gate roster assignment? Enforce with a 422, or declare specialty membership taxonomic-only in the glossary. leo built the enforcing component and abandoned it unimported.
- Timezone-change policy when weekly hours already exist — block-with-migrate, or allow-with-preview. leo silently shifts real availability, which is the one behaviour that is definitely wrong.
- Cross-org double-booking —
specialists.organization_id NOT NULLmeans a specialist working at two clinics has two independent profiles and two schedules, so the single-true-availability invariant holds only within an org. leo has the same limitation. Accept as documented, or add a cross-org check keyed onhuman_id— which is a cross-tenant read and needs anonymised or break-glass treatment.
External References
- Database schema: See architecture/data-model.md →
specialistsfor canonical table definitions, indexes, and RLS policies - API endpoints: See api.md for all specialist CRUD and query endpoints
- Scheduling configuration: See scheduling-config.md for weekly hours, overrides, and timezone handling
- Scheduling feature: See ../scheduling/ for full details on the availability engine and booking system
- Port plan: leo-port-map.md §3 → F1 — the UI surfaces to port, the foundation conflicts (C1, C11, C12), and the anti-patterns not to carry