Skip to content

RBAC Permission Matrix

Roles

RoleCodeScopeDescription
PatientpatientOwn data within current orgEnd user receiving healthcare. Sees only their own appointments, forms, and profile.
SpecialistspecialistOwn appointments + assigned patients within current orgHealthcare provider. Sees appointments where they are the assigned specialist, plus linked patient data.
Customer Supportcustomer_supportAll data (read) within current org, limited writeSupport staff. Can view all org data to assist patients/specialists. Cannot manage templates, integrations, or segments.
AdminadminFull management within current orgOrganization manager. Full CRUD on all org resources. Cannot access other organizations.
SuperadminsuperadminCross-organization, full system accessPlatform operator (RestartiX team only). Creates and manages organizations, assigns initial admins. Uses AdminPool (owner role) which bypasses all RLS policies. Not available to clinic staff.

Enforcement Layers

Access control is enforced at three layers. All must agree for an operation to succeed.

Request


┌──────────────────────────────────────┐
│  Layer 0: Connection Pool Routing    │
│  ──────────────────────────────────  │
│  • Superadmins → AdminPool (owner)  │
│  • Everyone else → AppPool (RLS)    │
│  • Public endpoints → AppPool       │
└──────────┬───────────────────────────┘


┌─────────────────────────────────┐
│  Layer 1: Middleware (Go)       │
│  ─────────────────────────────  │
│  • ClerkAuth: verify token      │
│  • OrgContext: route to pool    │
│  • RequireRole: check role      │
│  • RequireOwner: check entity   │
│    ownership (where needed)     │
└──────────┬──────────────────────┘


┌─────────────────────────────────┐
│  Layer 2: PostgreSQL RLS        │
│  ─────────────────────────────  │
│  • organization_id check        │
│  • role-based SELECT/INSERT/    │
│    UPDATE/DELETE policies        │
│  • Ownership checks (user_id,  │
│    specialist_id sub-queries)   │
│  • Cannot be bypassed from app  │
│  • Superadmins bypass via pool  │
└─────────────────────────────────┘

Pool routing handles: superadmins get the owner pool (bypasses RLS), all other users and public endpoints get the restricted pool (RLS enforced).

Middleware handles: route-level role gating, field-level response filtering, business rule validation (e.g., "cannot cancel a done appointment").

RLS handles: row-level data isolation. Even if middleware has a bug, a non-superadmin request on the AppPool cannot access data outside its org scope.

For schema details, see the schema.sql file in each feature directory under ../features/{feature}/schema.sql.


Permission Matrix

Legend:

  • C = Create | R = Read | U = Update | D = Delete
  • ✅ = Allowed | ❌ = Denied | 🔒 = Own only | 📋 = Published/public only

Organizations

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List organizations🔒 own orgs🔒 own orgs🔒 own orgs🔒 own orgs✅ all
View organization🔒 current org🔒 current org🔒 current org🔒 current org✅ any
View by slug (public)
Update organization✅ current org✅ any
Create organization
Delete organization
View API keys✅ current org✅ any
Manage custom domains✅ current org✅ any
Connect user to org✅ current org✅ any
Switch organization🔒 own orgs🔒 own orgs🔒 own orgs🔒 own orgs✅ any

Enforcement: RequireRole("superadmin") on create/delete org. RequireRole("admin") on update, domains, connect-user, API keys. RLS: id = current_app_org_id() (superadmins bypass RLS via AdminPool).

Schema: See ../features/organizations/schema.sql


Appointments

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List appointments🔒 own🔒 own (as specialist)✅ org✅ org✅ all
View appointment🔒 own🔒 own (as specialist)✅ org✅ org✅ any
View by UID🔒 own🔒 own (as specialist)✅ org✅ org✅ any
Create (basic)
Create from template
Create from intake
Attach forms🔒 own✅ org✅ org✅ any
Reschedule🔒 own✅ org✅ org✅ any
Cancel🔒 own✅ org✅ org✅ any
Update🔒 own✅ org✅ org✅ any
Delete (soft)✅ org✅ any
Calendar view🔒 own🔒 own (as specialist)✅ org✅ org✅ all

Specialist ownership: specialist_id IN (SELECT id FROM specialists WHERE user_id = current_app_user_id())

Patient ownership: patient_person_id IN (SELECT current_user_patient_person_ids()) — covers both the patient themselves and anyone managing them as a caregiver (see patient_person_managers)

Key change from Strapi: Patients can no longer create appointments. In the current Strapi system, all authenticated users can POST to any endpoint. In Go, appointment creation is restricted to staff roles. Patients book through the public scheduling flow or are booked by staff.

Enforcement: RequireRole("specialist", "customer_support", "admin") on create/modify endpoints. RLS handles row filtering.

Schema: See ../features/appointments/schema.sql


Appointment Templates

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List templates📋 published + public✅ org✅ org✅ org✅ all
View template📋 published + public✅ org✅ org✅ org✅ any
View by slug📋 published + public✅ org✅ org✅ org✅ any
Create template
Update template
Delete template

Enforcement: RequireRole("admin") on CUD. RLS: patients see only is_public = TRUE AND published = TRUE AND organization_id = current_app_org_id().

Schema: See ../features/appointments/schema.sql


Patients

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List patients🔒 self only✅ org✅ org✅ org✅ all
View patient🔒 self only✅ org✅ org✅ org✅ any
Onboard patient
Update patient🔒 own profile✅ org✅ org✅ any
Delete patient (soft)✅ org✅ any
Impersonate patient✅ org✅ any

Specialist patient access: Specialists can VIEW all patients in their org (needed to access patient profiles during appointments) but cannot UPDATE patient records directly. Patient profile updates happen through form filling — forms with profile_field_key write to patient_persons (portable), and forms with custom_field_id write to org-scoped custom_field_values.

Enforcement: RequireRole("admin") on impersonate/delete. RLS: patients see only their own record (resolved through patient_person_id using current_user_patient_person_ids()).

Schema: See ../features/patients/schema.sql


Specialists

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List specialists✅ org (read-only)✅ org✅ org✅ org✅ all
View specialist✅ org (read-only)✅ org✅ org✅ org✅ any
Create specialist
Update specialist🔒 own profile✅ org✅ any
Delete specialist (soft)✅ org✅ any

Why patients can read specialists: Patients need to see specialist names, photos, and specialties when viewing appointments and booking.

Specialist self-edit: Specialists can update their own profile (signature, bio, etc.) but not other specialists. RequireRole("specialist", "admin") on PUT with ownership check for specialists.

Enforcement: RequireRole("admin") on create/delete. Specialist PUT: RequireRole("specialist", "admin") + ownership check.

Schema: See ../features/specialists/schema.sql


Specialties

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List specialties✅ org✅ org✅ org✅ org✅ all
View specialty✅ org✅ org✅ org✅ org✅ any
Create specialty
Update specialty
Delete specialty

Enforcement: RequireRole("admin") on CUD. RLS: org-scoped.

Schema: See ../features/specialists/schema.sql


Forms

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List forms🔒 own✅ org✅ org✅ org✅ all
View form🔒 own✅ org✅ org✅ org✅ any
Create form
Update form (save values)🔒 own✅ org✅ org✅ org✅ any
Sign form🔒 own✅ org✅ org✅ any
Delete form✅ (unsigned only)✅ (unsigned only)

Patient form filling: Patients can fill their own forms (surveys, disclaimers). They cannot create forms — forms are auto-generated during appointment creation.

Specialist form filling: Specialists fill report, analysis, advice, and prescription forms. They can also fill any form in their org (e.g., filling on behalf of a patient).

Sign restriction: Customer support cannot sign forms (they are not a party to the medical interaction). Only patients, specialists, and admins can sign.

Business rule: Signed forms are immutable. Any PUT to a signed form returns 409 Conflict.

Enforcement: RequireRole("specialist", "customer_support", "admin") on create. Sign: RequireRole("patient", "specialist", "admin"). Delete: RequireRole("admin") + status != 'signed' check. RLS: patients see only their own forms (filtered by patient_person_id through current_user_patient_person_ids()).

Schema: See ../features/forms/schema.sql


Form Templates

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List form templates✅ org (read-only)✅ org✅ org✅ org✅ all
View form template✅ org (read-only)✅ org✅ org✅ org✅ any
Create form template
Update form template
Publish form template
View version history
Delete form template

Enforcement: RequireRole("admin") on all mutations. RLS: org-scoped.

Schema: See ../features/forms/schema.sql


Reports (appointment_documents type=report)

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List reports🔒 own + published✅ org✅ org✅ org✅ all
View report🔒 own + published✅ org✅ org✅ org✅ any
Download PDF🔒 own + published (audience=patient forced)✅ org✅ org✅ org✅ any
Create report
Update report🔒 own✅ org✅ any
Publish report🔒 own✅ org✅ any
Delete report✅ org✅ any

Patient PDF access: Patients can only download PDFs for published reports. Private fields are automatically excluded (audience=patient is forced regardless of query param).

Customer support: Can VIEW reports for troubleshooting but cannot create, modify, or publish them. Reports are medical documents authored by specialists.

Enforcement: RequireRole("specialist", "admin") on create/update/publish. RequireRole("admin") on delete. RLS: patients see only published reports linked to their own appointments (filtered by patient_person_id through current_user_patient_person_ids()).

Schema: See ../features/documents/schema.sql


Prescriptions (appointment_documents type=prescription)

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List prescriptions🔒 own + published✅ org✅ org✅ org✅ all
View prescription🔒 own + published✅ org✅ org✅ org✅ any
Download PDF🔒 own + published✅ org✅ org✅ org✅ any
Create prescription
Update prescription🔒 own✅ org✅ any
Publish prescription🔒 own✅ org✅ any
Delete prescription✅ org✅ any

Same pattern as reports. Prescriptions have an additional validation: specialist must have a signature uploaded before PDF can be generated.

Enforcement: Same as reports. Additional check: specialist.signature_url IS NOT NULL on PDF generation.

Schema: See ../features/documents/schema.sql


PDF Templates (Visual PDF designer)

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List templates📋 published only📋 published only✅ org✅ all
View template📋 published only📋 published only✅ org✅ any
Create template✅ org✅ any
Update template✅ org✅ any
Publish template✅ org✅ any
Rollback template✅ org✅ any
Delete template✅ org✅ any
List components✅ org✅ org✅ org✅ all
View component✅ org✅ org✅ org✅ any
Create component✅ org✅ any
Update component✅ org✅ any
Delete component✅ org✅ any
Preview template✅ org✅ org✅ org✅ any
Render PDF🔒 own appointments✅ org✅ org✅ any

Admin-only management. PDF templates control the visual layout of generated PDFs (reports, prescriptions, certificates). Specialists/customer support can view published templates and components for preview purposes, and specialists can render PDFs for their own appointments. Template editing is admin-only.

Enforcement: RequireRole("admin") on CUD operations. RLS: org-scoped, admins can manage all templates, specialists can view published templates and render for own appointments.

Schema: See ../features/pdf-templates/schema.md


Custom Fields (entity attribute definitions)

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List custom fields✅ org (read-only)✅ org✅ org✅ org✅ all
Create custom field
Update custom field
Delete custom field✅ (non-system)✅ (non-system)

System fields (where system_key IS NOT NULL) cannot be deleted or have their key modified by anyone, including superadmin.

Enforcement: RequireRole("admin") on CUD. Business rule: system_key fields are protected.

Schema: See ../features/custom-fields/schema.sql


Custom Field Values (patient/specialist profile data)

ActionPatientSpecialistCustomer SupportAdminSuperadmin
View patient profile🔒 own✅ org✅ org✅ org✅ any
Update patient profile🔒 own✅ org✅ org✅ any
Prefill values (for forms)🔒 own✅ org✅ org✅ org✅ any
View specialist profile🔒 own✅ org✅ org✅ any
Update specialist profile🔒 own✅ org✅ any

Auto-sync: When a patient fills a form field that references custom_field_id, the custom field value is automatically upserted. This is the primary way patient profiles are updated — not via direct API calls.

Specialist profile read by patients: Patients cannot view specialist custom field values directly. They see specialist public info (name, title, specialties) from the specialists table, not from custom field values.

Enforcement: Ownership check in handler. RLS: org-scoped with application-level entity ownership filtering.

Schema: See ../features/custom-fields/schema.sql


Segments

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List segments✅ org (read-only)✅ org✅ all
View segment✅ org (read-only)✅ org✅ any
View segment members✅ org (read-only)✅ org✅ any
Create segment
Update segment (rules)
Re-evaluate segment
Delete segment
View own segments (patient)🔒 own✅ org✅ org✅ any

Customer support read access: Support staff can view segments and their members to understand patient grouping, but cannot create or modify segment rules. This is a reporting/support tool, not a management action.

Patient self-view: Patients can see which segments they belong to (via GET /v1/patients/{id}/segments) but cannot see other segment members or segment rules.

Enforcement: RequireRole("admin") on CUD + evaluate. RequireRole("customer_support", "admin") on read. Patient segment view: ownership check.

Schema: See ../features/segments/schema.sql


Exercise Library

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List/search exercises📋 published only✅ published (global+org)✅ all (global+org)✅ all (global+org)✅ all
View exercise detail📋 published only✅ published (global+org)✅ all (global+org)✅ all (global+org)✅ all
Create org exercise✅ own org✅ any org
Create global exercise
Update org exercise✅ own org✅ any
Update global exercise
Delete org exercise✅ own org✅ any
Delete global exercise
Clone exercise
Upload video✅ own org✅ any
Manage taxonomy✅ own org✅ global+any org

Global exercises (org_id IS NULL): Only superadmins can create, modify, or delete global exercises. All other roles can only read (published) global exercises.

Org exercises: Admin manages org-specific exercises. RLS enforces org isolation.

Enforcement: RequireRole("admin") on mutations. RLS dual-scope policy: organization_id IS NULL OR organization_id = current_app_org_id() for SELECT.

Schema: See ../features/exercise-library/schema.sql


Treatment Plans

ActionPatientSpecialistCustomer SupportAdminSuperadmin
List treatment plans❌ (via enrollments)✅ org✅ org (read)✅ org✅ all
Create treatment plan
Update treatment plan
Delete treatment plan
Publish plan version
Assign plan to patient
View patient enrollment🔒 own✅ org✅ org (read)✅ org✅ all
Approve patient plan
Pause/cancel enrollment
Start session (execute)🔒 own
Complete session🔒 own
Log exercise data🔒 own
View session completions🔒 own✅ org✅ org (read)✅ org✅ all
View exercise logs🔒 own✅ org✅ org (read)✅ org✅ all
View progress/analytics🔒 own✅ org✅ org (read)✅ org✅ all
Clone treatment plan
Promote custom → org
Browse plan library🔒 own (library_access)✅ (read)
Self-assign from library🔒 own (library_access)

Patient session execution: Only patients can start, complete, and log exercises for their own sessions. This is enforced via RLS (sub-query through patient_treatment_plans → patients → user_id).

Specialist creates + assigns: Specialists create plans and assign them to patients. The approval workflow (if enabled) requires a specialist or admin to approve before the patient can start.

Access grants from service plans: The telerehab_access and library_access flags on service_plans control treatment plan access. A patient must have an active patient_service_plan with telerehab_access = TRUE for a specialist to assign telerehab treatment plans to them, and library_access = TRUE for the patient to browse and self-assign plans from the library. Middleware verifies these grants before allowing the operation.

Enforcement: RequireRole("specialist", "admin") on plan mutations. RequireRole("specialist", "admin") on clone. RequireRole("admin") on promote (custom → org). RequireRole("patient", "specialist", "admin") on session execution (ownership check in handler). Library browse: RLS handles filtering; patient access gated by library_access grant on active service plan. Self-assign from library: RequireRole("patient") + library_access grant check. RLS handles row-level patient ownership.

Schema: See ../features/treatment-plans/schema.sql


Product Orders

ActionPatientSpecialistCustomer SupportAdminSuperadmin
Create product order (standalone)
Update order status (confirm/ship/deliver)
Cancel product order
View product orders🔒 own✅ (read)

Enforcement: RequireRole("specialist", "admin") on create/update/cancel. RLS: patients see only their own orders (filtered by patient_person_id through current_user_patient_person_ids()). Customer support has read-only access.


Service Plan Products

ActionPatientSpecialistCustomer SupportAdminSuperadmin
Manage plan product bundles

Enforcement: RequireRole("admin") on all mutations. RLS: org-scoped.


Export

ActionPatientSpecialistCustomer SupportAdminSuperadmin
Export CSV✅ org✅ org✅ any

Enforcement: RequireRole("customer_support", "admin"). Rate limited. Max 10,000 records per export.


GDPR

ActionPatientSpecialistCustomer SupportAdminSuperadmin
Export user data (SAR)✅ org✅ any
Delete user data✅ org✅ any
Anonymize user data✅ org✅ any

Admin-only. GDPR operations are serious data actions. Customer support cannot trigger them. In practice, a patient would request data export/deletion from support, and support escalates to an admin.

Enforcement: RequireRole("admin"). Audit log captures every GDPR operation with full context.


Audit & Telemetry Logs

ActionPatientSpecialistCustomer SupportAdminSuperadmin
View audit logs✅ org✅ all
View telemetry/metrics✅ org✅ all

Enforcement: RequireRole("admin"). RLS: organization_id = current_app_org_id() AND current_app_role() = 'admin'. Audit logs are append-only — no UPDATE or DELETE policies exist.


Field-Level Response Filtering

Beyond row-level access, certain fields are excluded or masked based on the caller's role.

Patient-Restricted Fields

When a patient calls any endpoint, these fields are stripped from responses:

EntityHidden FieldsReason
Appointmentspecialist.signature_url, specialist.email, specialist.phoneSpecialist personal contact info
FormFields where private = true in fields JSONBSpecialist-only observations, internal scores
Patientconsumer_idInternal system identifiers
Organizationintegrations, api_keysSensitive configuration

Patient-Restricted Write Fields

When a patient sends PUT/POST, these fields are silently removed from the request body:

EntityProtected FieldsReason
Patientpatient_person_id, consumer_id, organization_idCannot change ownership
Formform_template_id, appointment_id, patient_person_id, organization_id, fieldsCannot change form structure

Implementation

go
// ResponseFilter middleware strips fields based on caller role.
// Applied AFTER the handler returns data, BEFORE JSON serialization.

func ResponseFilter(role string) func(any) any {
    return func(data any) any {
        if role == "patient" {
            // Strip private form fields from response
            // Strip specialist contact info
            // Strip internal identifiers
        }
        return data
    }
}

This is implemented as a Go middleware that runs after the handler, not in the database layer. RLS handles row access; field filtering handles column access.


Changes from Current Strapi System

AreaStrapi (Current)Go (Target)Reason
Appointment creationAll authenticated users can createStaff only (specialist, support, admin)Patients book via Intakes or are booked by staff
Appointment modificationAll authenticated users (no role check)Staff only + ownership checkPatients cannot reschedule/cancel directly
Patient impersonationNo role check on routeAdmin onlySecurity: impersonation is a privileged action
Form deletionNo role checkAdmin only + unsigned checkPrevent accidental deletion of signed medical records
Specialist updateAll authenticated via field-protection strippingSpecialist (own) + AdminExplicit role check instead of silent field stripping
Meta field value deletionNo role checkAdmin onlyPrevent data loss
Report/prescription creationNo explicit restrictionSpecialist + Admin onlyMedical documents should only be created by medical staff
Segment managementNo explicit restrictionAdmin onlySegments define patient groups — management action
GDPR operationsNot implementedAdmin onlySerious data actions requiring authorization
Customer support write accessSame as admin in many placesRead-heavy, limited writesSupport assists, doesn't manage

RequireRole Middleware Usage

Quick reference for which RequireRole() calls are needed on each route group:

go
// Organizations
r.Post("/v1/organizations",                    RequireRole("superadmin"))  // create org — superadmin only
r.Patch("/v1/organizations/{id}",              RequireRole("admin"))
r.Get("/v1/organizations/{id}/api-keys",       RequireRole("admin"))
r.Post("/v1/organizations/{id}/connect-user",  RequireRole("admin"))
r.Post("/v1/organizations/{id}/domains",       RequireRole("admin"))
r.Delete("/v1/organizations/{id}/domains/{domainId}", RequireRole("admin"))
r.Post("/v1/organizations/{id}/domains/{domainId}/verify", RequireRole("admin"))

// Appointments — creation & modification
r.Post("/v1/appointments",                     RequireRole("specialist", "customer_support", "admin"))
r.Post("/v1/appointments/from-template",       RequireRole("specialist", "customer_support", "admin"))
r.Post("/v1/appointments/{id}/attach-forms",   RequireRole("specialist", "customer_support", "admin"))
r.Post("/v1/appointments/{id}/reschedule",     RequireRole("specialist", "customer_support", "admin"))
r.Post("/v1/appointments/{id}/cancel",         RequireRole("specialist", "customer_support", "admin"))
r.Delete("/v1/appointments/{id}",              RequireRole("admin"))

// Appointment templates
r.Post("/v1/appointment-templates",            RequireRole("admin"))
r.Put("/v1/appointment-templates/{id}",        RequireRole("admin"))
r.Delete("/v1/appointment-templates/{id}",     RequireRole("admin"))

// Patients
r.Post("/v1/patients/onboard",                 RequireRole("specialist", "customer_support", "admin"))
r.Post("/v1/patients/{id}/impersonate",        RequireRole("admin"))
r.Delete("/v1/patients/{id}",                  RequireRole("admin"))

// Specialists
r.Post("/v1/specialists",                      RequireRole("admin"))
r.Put("/v1/specialists/{id}",                  RequireRole("specialist", "admin"))  // + ownership check
r.Delete("/v1/specialists/{id}",               RequireRole("admin"))

// Specialties
r.Post("/v1/specialties",                      RequireRole("admin"))
r.Put("/v1/specialties/{id}",                  RequireRole("admin"))
r.Delete("/v1/specialties/{id}",               RequireRole("admin"))

// Forms
r.Post("/v1/forms",                            RequireRole("specialist", "customer_support", "admin"))
r.Post("/v1/forms/{id}/sign",                  RequireRole("patient", "specialist", "admin"))
r.Delete("/v1/forms/{id}",                     RequireRole("admin"))

// Form templates (part of forms feature)
r.Post("/v1/form-templates",                   RequireRole("admin"))
r.Put("/v1/form-templates/{id}",               RequireRole("admin"))
r.Post("/v1/form-templates/{id}/publish",      RequireRole("admin"))
r.Delete("/v1/form-templates/{id}",            RequireRole("admin"))

// Reports & prescriptions
r.Post("/v1/reports",                          RequireRole("specialist", "admin"))
r.Put("/v1/reports/{id}",                      RequireRole("specialist", "admin"))
r.Delete("/v1/reports/{id}",                   RequireRole("admin"))
r.Post("/v1/prescriptions",                    RequireRole("specialist", "admin"))
r.Put("/v1/prescriptions/{id}",               RequireRole("specialist", "admin"))
r.Delete("/v1/prescriptions/{id}",            RequireRole("admin"))

// PDF Templates
r.Route("/v1/pdf-templates",                   RequireRole("admin"))  // admin-only management
r.Route("/v1/pdf-template-components",         RequireRole("admin"))  // admin-only management

// Custom fields
r.Post("/v1/custom-fields",                    RequireRole("admin"))
r.Put("/v1/custom-fields/{id}",                RequireRole("admin"))
r.Delete("/v1/custom-fields/{id}",             RequireRole("admin"))

// Segments
r.Get("/v1/segments",                          RequireRole("customer_support", "admin"))
r.Get("/v1/segments/{id}",                     RequireRole("customer_support", "admin"))
r.Get("/v1/segments/{id}/members",             RequireRole("customer_support", "admin"))
r.Post("/v1/segments",                         RequireRole("admin"))
r.Put("/v1/segments/{id}",                     RequireRole("admin"))
r.Post("/v1/segments/{id}/evaluate",           RequireRole("admin"))
r.Delete("/v1/segments/{id}",                  RequireRole("admin"))

// Export
r.Post("/v1/export/csv",                       RequireRole("customer_support", "admin"))

// GDPR
r.Route("/v1/gdpr",                            RequireRole("admin"))  // all methods

// Exercise Library
r.Post("/v1/exercises",                             RequireRole("admin"))
r.Put("/v1/exercises/{id}",                         RequireRole("admin"))
r.Delete("/v1/exercises/{id}",                      RequireRole("admin"))
r.Put("/v1/exercises/{id}/status",                  RequireRole("admin"))
r.Post("/v1/exercises/{id}/clone",                  RequireRole("admin"))
r.Post("/v1/exercises/{id}/video",                  RequireRole("admin"))
r.Post("/v1/exercise-categories",                   RequireRole("admin"))
r.Put("/v1/exercise-categories/{id}",               RequireRole("admin"))
r.Delete("/v1/exercise-categories/{id}",            RequireRole("admin"))
// Same pattern for exercise-body-regions and exercise-equipment

// Treatment Plans
r.Post("/v1/treatment-plans",                       RequireRole("specialist", "admin"))
r.Put("/v1/treatment-plans/{id}",                   RequireRole("specialist", "admin"))
r.Delete("/v1/treatment-plans/{id}",                RequireRole("admin"))
r.Post("/v1/treatment-plans/{id}/publish",          RequireRole("specialist", "admin"))
r.Post("/v1/patient-treatment-plans",               RequireRole("specialist", "admin"))
r.Post("/v1/patient-treatment-plans/{id}/approve",  RequireRole("specialist", "admin"))
r.Put("/v1/patient-treatment-plans/{id}/status",    RequireRole("specialist", "admin"))
r.Post("/v1/treatment-plans/{id}/clone",                   RequireRole("specialist", "admin"))
r.Post("/v1/treatment-plans/{id}/promote",                 RequireRole("admin"))
// Library browse: no RequireRole (RLS handles filtering), but middleware checks library_access grant
r.Post("/v1/treatment-plan-library/self-assign",           RequireRole("patient"))  // + library_access grant check
// Session execution: RequireRole("patient", "specialist", "admin") + ownership check
r.Post("/v1/patient-treatment-plans/{id}/sessions/{num}/start",  RequireRole("patient", "specialist", "admin"))
r.Post("/v1/patient-session-completions/{id}/complete",          RequireRole("patient", "specialist", "admin"))

// Product Orders
r.Post("/v1/product-orders",                          RequireRole("specialist", "admin"))
r.Put("/v1/product-orders/{id}/status",                RequireRole("specialist", "admin"))
r.Post("/v1/product-orders/{id}/cancel",               RequireRole("specialist", "admin"))

// Service Plan Products
r.Route("/v1/service-plan-products",                   RequireRole("admin"))  // all methods

// No RequireRole needed (RLS handles filtering):
// GET /v1/appointments, GET /v1/patients, GET /v1/specialists,
// GET /v1/specialties, GET /v1/forms, GET /v1/form-templates,
// GET /v1/custom-fields, GET /v1/calendar,
// GET /v1/reports, GET /v1/prescriptions,
// GET /v1/appointment-templates, PUT /v1/forms/{id} (save values),
// PUT /v1/patients/{id} (self-edit allowed)

Ownership Checks Beyond RLS

Some endpoints need application-level ownership validation in addition to RLS:

EndpointCheckImplementation
PUT /v1/specialists/{id}Specialist can only edit own profileHandler: if role == "specialist" && specialist.UserID != currentUserID { return 403 }
PUT /v1/patients/{id}Patient can only edit own recordRLS handles via patient_person_id IN (current_user_patient_person_ids()) — no extra check needed
PUT /v1/reports/{id}Specialist can only edit own reportsHandler: if role == "specialist" && report.AppointmentSpecialistUserID != currentUserID { return 403 }
PUT /v1/prescriptions/{id}Same as reportsSame ownership check
POST /v1/forms/{id}/signPatient can only sign own formsRLS handles via patient_person_id IN (current_user_patient_person_ids())
PUT /v1/forms/{id}Patient can only save own formsRLS handles via patient_person_id IN (current_user_patient_person_ids())

Rate-Limited Endpoints

EndpointLimitKeyReason
POST /webhooks/clerk20/minIPWebhook abuse prevention
GET /v1/organizations/{id}/api-keys5/5minuser_id + pathSensitive data access
POST /v1/organizations/{id}/connect-user5/5minuser_id + pathPrevents mass user connection
POST /v1/patients/{id}/impersonate3/5minuser_id + pathPrivileged action
POST /v1/export/csv5/5minuser_id + pathResource-intensive operation
POST /v1/gdpr/*3/5minuser_id + pathSerious data operations
GET /v1/reports/{id}/pdf10/minuser_id + pathPDF generation is resource-intensive
GET /v1/prescriptions/{id}/pdf10/minuser_id + pathPDF generation is resource-intensive