Doctor
Responsibility
Doctor owns professional doctor profiles, license numbers, one speciality and multiple specializations, qualification summaries, chambers and doctor onboarding orchestration. Identity remains authoritative for accounts and credentials. Organization owns doctor affiliations, invitations, and join requests. A doctor ordinary-member role alone creates neither a professional profile nor an affiliation.
Current implementation
Professional catalogs and profile selections are implemented: separate qualification, speciality and specialization tables, administrator CRUD, public reference lists, authenticated doctor selections, and starter seeds.
Doctor registration, login, authentication verification and profile retrieval at /api/v1/doctors/profile are implemented. Profile editing uses PATCH /api/v1/doctors/profile. Doctor also exposes a curated professional directory for Admin doctor management, which implements credential review and platform-admin organization assignment. Patient creation, search, detail and partial updates are implemented at /api/v1/doctors/patients; see the patient workflow. The remaining doctor portal operations are documentation-first Huma contracts: valid requests return 501, malformed JSON returns 400, and request-local validation errors return 422. Invitation creation/preview, join requests and doctor self-service verification history remain unimplemented.
Registration
POST /api/v1/doctors/register is public and requires name, email, password, license_number, and a catalog speciality_id. Optional specialization_ids contains up to 30 unique child UUIDs, defaulting to []. All children must belong to the selected speciality. Names and licenses are trimmed to 1–255 characters. Email is normalized to lowercase. Passwords contain 8–128 characters and are hashed by Identity. Registration creates an active, initially unverified account and one profile with catalog relationships in the same transaction.
{
"name": "Doctor Example",
"email": "doctor@example.com",
"password": "ExamplePassword123!",
"license_number": "BMDC-123",
"speciality_id": "20000000-0000-4000-8000-000000000001",
"specialization_ids": []
}Success returns 201 with { "message": "Success", "status_code": 201, "data": { "doctor": { ... } } }; sign in separately using /api/v1/doctors/auth/login. Signup creates no session or ordinary organization membership. Duplicate email (case-insensitive) or license returns 409. Malformed JSON returns 400, invalid details return 422, unavailable required providers return 503, and unexpected storage failures return 500. Responses use Cache-Control: no-store.
Omit invitation_token or send "invitation_token": "" to register without joining an organization. A platform admin can assign the doctor to an organization later through Admin doctor management. A whitespace-only token remains invalid.
An optional non-empty invitation_token adds invited onboarding. Organization validates its stored SHA-256 token hash, pending state, expiry, matching normalized email and active organization, then consumes the invitation and creates an active doctor affiliation. The response also contains data.affiliation. Invalid, expired, revoked, consumed or mismatched invitations return 422. Identity account creation, doctor profile insertion, invitation consumption and affiliation insertion commit in one PostgreSQL transaction; failures leave no partial signup. Database uniqueness and invitation row locks protect concurrent requests.
Apply the Identity and Doctor migrations plus Organization migration 00005_create_doctor_invitations.sql before using invited signup:
go run ./tools/cli database migrate --service identity
go run ./tools/cli database migrate --service doctor
go run ./tools/cli database migrate --service organizationInvitation creation, preview and revocation remain 501 contracts; invitation email delivery is also unimplemented. Redemption supports invitations already stored by Organization; an invitation issuer must store only the lowercase recipient email and SHA-256 hexadecimal token hash. Rolling back Organization migration 00005 deletes invitation records; existing doctor profiles and affiliations remain.
Authentication
POST /api/v1/doctors/auth/login accepts email and password. Identity authenticates the active account, then Doctor requires a matching professional profile before Identity issues tokens or creates a session. An ordinary organization member with role doctor does not qualify without a profile. Email verification, Admin license approval and organization affiliation are not login requirements, allowing doctors to complete onboarding.
Successful login returns access_token, refresh_token, token_type (Bearer), and expires_in (seconds). Invalid credentials, disabled accounts and missing profiles share the same 401 response. Identity's ten-attempt limit per normalized email per 15 minutes is shared across ordinary, admin, organization and doctor login; exceeding it returns 429.
GET /api/v1/doctors/auth/verify takes Authorization: Bearer <access_token> and returns { "identity": { ... }, "doctor": { ... } }. It validates the persistent session and active identity, then loads only that identity's doctor profile. Identity includes id, email, name, status, email_verified, created_at, and updated_at. Doctor includes id, user_id, current Identity name/email, license_number, one speciality object, a specialization array, optional qualification and chambers, created_at, and updated_at. Invalid, expired or revoked credentials return 401; a valid identity without a doctor profile returns 403. Both routes return 503 when required providers are unavailable, and use Cache-Control: no-store for successful and error responses.
Use the existing POST /api/v1/auth/refresh and POST /api/v1/auth/logout routes with the refreshToken JSON field. Sessions, token rotation and revocation remain owned by Identity. Auth verification does not grant prescription eligibility or approve professional credentials.
Current doctor profile
GET /api/v1/doctors/profile requires Authorization: Bearer <access_token> and the same active identity and doctor profile as authentication verification. It returns the admin/organization success envelope: { "message": "Success", "status_code": 200, "data": { ... } }. data contains the current Identity id, email, name, status, email_verified, created_at and updated_at, plus a nested profile containing doctor-owned id, user_id, license_number, one speciality object, a specialization array, optional qualification and chambers, created_at and updated_at. Doctor ID is data.profile.id; doctor name is data.name. No role fields or permissions are returned; doctor authorization is deferred.
speciality is one catalog object. specialization is an ordered array of up to 30 catalog objects; each contains its parent speciality_id, which must match the selected speciality. Empty specializations are returned as []. The old scalar specialization and specialty/subspecialty profile arrays have been removed.
qualification is a display summary, such as "MBBS, FCPS". Legacy stored text supports up to 2,000 characters; after catalog selection, the summary is derived from selected names (up to 7,708 characters). chambers supports up to 20 locations, each with name, address and visiting hours as display text. Qualification and chambers are omitted when unavailable. The registration credential remains license_number; no separate registration-number field is returned. Use qualifications, speciality, and specialization for catalog entries and IDs; see catalogs and selections. Example professional fields inside data.profile, alongside its IDs and timestamps:
{
"license_number": "BMDC-123",
"speciality": {"id": "20000000-0000-4000-8000-000000000001", "name": "Cardiology"},
"specialization": [],
"qualification": "MBBS, FCPS",
"chambers": [
{ "name": "Main Chamber", "address": "Dhanmondi, Dhaka", "hours": "Sunday–Thursday, 5–9 PM" },
{ "name": "Evening Chamber", "address": "Uttara, Dhaka", "hours": "Friday–Saturday, 6–9 PM" }
]
}All lookups use the verified identity rather than caller-supplied user or doctor identifiers. Email verification, license approval and organization affiliation are not required. Invalid, expired or revoked credentials return 401; a valid identity without a doctor profile returns 403; unavailable dependencies return 503. Successful and error responses use Cache-Control: no-store.
Workflows
Organization and doctor workflows specifies independent registration, invited registration, invitation preview, doctor self-profile operations, organization discovery, and creating/canceling join requests. Registration creates an Identity account and Doctor profile, then uses doctor login. Invited onboarding redeems Organization-owned invitation state and creates the active affiliation.
Future join-request acceptance belongs to Organization and must atomically change request state and affiliation. Doctor must not maintain a duplicate organization-membership repository. Verification is a future Admin workflow, not an implemented eligibility gate here.
Management API contracts describes professional profile PATCH and the doctor's verification-history list using Admin-owned public DTOs. Profile PATCH is implemented at /api/v1/doctors/profile; verification history remains a validated 501 stub. The former /api/v1/doctors/me profile route has been removed.
Persistence, events, and tests
Doctor owns doctor_profiles and the doctor migration set (goose_doctor_version). Profiles store one required speciality reference, qualification summary, chamber JSON and timestamps; ordered specialization and qualification selections use relationship tables. Names and emails come from Identity. Apply go run ./tools/cli database migrate --service doctor after rebuilding the CLI. Existing profiles with ambiguous multiple specialities or unmatched legacy text require explicit reconciliation. See the catalog migration and recovery procedure.
go run ./tools/cli database seed --items doctors adds a profile for doctor@doctor.com after applying Doctor and Identity migrations, with General Medicine as the single speciality, an empty specialization array, an MBBS/FCPS summary and two example chambers. The default development password is doctor@doctor.com (or the seed's --password override). Repeated seeds preserve professional details, including edited expertise, qualification and chambers; development clean/reset includes doctor tables. Patient operations require an active doctor affiliation and do not require permission grants; see the patient workflow. Local testing can use public registration or seeded/imported profiles.
Handler tests cover registration validation and errors, authentication contracts, eligibility, identity/profile ownership, safe responses and trusted peer metadata. PostgreSQL tests cover signup and login, duplicate accounts/licenses, invitation state/expiry/email/organization checks, reuse, rollback after a late failure and concurrent registration, plus real token issuance, shared rate limits, refresh/logout, revoked sessions, profile removal, migration rollback/replay and repeatable fixtures. Run go test ./services/doctor/...; use AUDIT_TEST_DATABASE_URL for API integration tests and DOCTOR_TEST_DATABASE_URL for CLI fixture tests, each pointing to a dedicated local test database. Registration sends no onboarding emails or pre-commit events; invitation issuance and provider-owned post-commit notifications remain future work.
Code reference
See public profiles, module, and use-case contracts.
Clinical input catalogs
See the clinical suggestion workflow for authenticated Doctor queries, Admin CRUD permissions, DTOs, and seed setup.