Skip to content

Specialties Feature

Medical specialty categories (Cardiology, Physical Therapy, Dermatology) to organize your team.

NOT BUILT — reconciled 2026-08-02

There is no specialties table, no Go domain, no routes, and zero specialties.* permission rows in any migration. This page is a design spec, not a description of running code. Every "Implementation Files" path below is aspirational.

Settled 2026-08-02 — specialties are PER-ORG. specialties.organization_id UUID NOT NULL. The open "per-org or global?" question that used to sit in features.md F1.1 is closed: per-org matches leo, data-model.md Area 2, and this page. Global would have enabled cross-org anonymised analytics and a platform-seeded starter catalog; that is not worth diverging from the live system the data migrates out of.

Ships in migration 000040 — the first migration of the F1–F6 port. See platform-completion.md and leo-port-map.md §3 → F1.

What this enables

Team organization: Categorize your specialists by medical discipline—helps you manage who can do what.

Patient filtering: Patients can browse by specialty ("I want a Cardiologist") to find the right specialist.

Multi-specialty clinics: Run a clinic with Cardiology, PT, and Dermatology all in the same platform, each with their own team.

How it works

  1. Admin creates specialties: "Cardiology", "Physical Therapy", "Dermatology"
  2. Admin assigns specialists: Dr. Smith → Cardiology, Dr. Jones → Physical Therapy
  3. Patients filter by specialty: "Show me available Cardiologists"
  4. System filters: Shows only specialists tagged with Cardiology

Technical Reference

Overview

Specialties are medical specialty categories (e.g., Cardiology, Dermatology, Orthopedics) used to organize specialists and appointment templates within an organization.

Key Concepts

  • Organization-scoped: Each organization maintains its own specialty catalog. organization_id NOT NULL + RLS, like every tenant table.
  • Specialist association: Specialists can belong to multiple specialties (M:M relationship via specialist_specialties junction table — which carries its own denormalised organization_id NOT NULL and its own RLS policies)
  • Offering filtering: An Offering references a single specialty via offerings.specialty_id (ON DELETE RESTRICT). "Appointment template" is retired leo vocabulary — see the naming note below.
  • Simple CRUD: Basic create/read/update/delete operations with admin-only mutations

Database Schema

See architecture/data-model.md → specialties for the canonical table definition (UUID PKs, RLS policies, indexes).

Key fields:

  • id UUID — primary key
  • organization_id UUID NOT NULL — organization foreign key (settled per-org 2026-08-02)
  • title TEXT — display name (e.g., "Cardiology")
  • slug (text) - URL-safe identifier (e.g., "cardiology")
  • created_at, updated_at - Standard timestamps

Constraints and indexes the migration must carry:

  • UNIQUE (slug, organization_id) — slugs are unique within a clinic, not globally
  • GIN (immutable_unaccent(title) gin_trgm_ops) — Romanian typeahead without unaccent breaks UX ("Stefan" must match "Ștefan"). immutable_unaccent + pg_trgm already ship in 000001.
  • RLS SELECT for staff via organization_memberships, plus a patient-side SELECT policy via current_human_patient_profile_ids() joined to patients.organization_id — without it the portal's specialty filter returns zero rows for patient sessions.

Deletion

Specialties are configuration, not medical records, so delete is a hard delete — but it is pre-checked and returns 409 with in-use counts when specialists or offerings still reference the row, and it emits an audit entry either way. (Contrast specialists, which is soft-delete-only.)

Permissions

specialties.manage for mutations; membership-read for the list. No specialty permission row exists in any migration today — this feature seeds its own, along with the system role-template grants, in the same PR as the schema.

Naming

speciality (leo's spelling), appointment_template and franchise are retired vocabulary. The platform spells it specialty, the catalog entity is an Offering, and the tenant is an organization. See glossary.md, which wins every naming dispute.

API Reference

See api.md for complete endpoint documentation.

Endpoints:

  • GET /v1/specialties - List all specialties in current organization
  • POST /v1/specialties - Create specialty (admin only)
  • GET /v1/specialties/{id} - Get specialty with related specialists
  • PUT /v1/specialties/{id} - Update specialty (admin only)
  • DELETE /v1/specialties/{id} - Delete specialty (admin only)
  • Specialists (../specialists/) - Specialists link to specialties via specialist_specialties
  • Offerings (../services/) - An offering references a single specialty via offerings.specialty_id

Implementation Files (planned — none of this exists yet)

Backend implementation (Go), following the platform's domain layout (/new-domain scaffolds it):

  • services/api/internal/core/domain/specialties/{model,repository,service,handler,errors}.go — all SQL stays in repository.go, reached through ConnFromContext(ctx) so RLS applies
  • Route registration in services/api/internal/core/server/routes.go, gated by RequirePermission and RequireURLOrgMatchesScope("id") (P47)

Database migration:

  • 000040specialties + specialists + specialist_specialties in one migration, with table DDL, indexes, RLS policies, permission seeding, system role-template grants, and data-classification.md entries for every new column. make check fails the build if the classification rows are missing.

The earlier claim on this page that specialties shipped across migrations 000001000003 was wrong in every particular. Those are 000001_init, 000002_tenancy_rbac and 000003_org_settings; the string "specialt" does not appear in any migration in the repository.