Skip to main content

Plan-Based Default Permissions

Date: 2026-04-27
Status: Approved

Background

All users currently get role defaults from DEFAULT_PERMISSIONS_BY_ROLE in permissions.ts, regardless of the clinic’s subscription plan. Admin can override per-user after the fact via /staff/:id/permissions, but there are no plan-gated defaults. The ssh & Associates tenant (Pro+, active) serves as the golden standard. Their receptionist has a hand-tuned permission set that will become the canonical Pro+ receptionist default.

Goal

Auto-assign the right permission baseline at invite acceptance based on the clinic’s subscription plan. Admin can still override any individual user’s permissions afterward.

Permission Sets

Pro+ (price_pro_plus)

RoleBasis
adminAll permissions (unchanged)
doctorFull current DEFAULT_PERMISSIONS_BY_ROLE.doctor (unchanged)
receptionistssh & Associates golden standard (see below)
patientCurrent defaults (unchanged)
Pro+ receptionist — vs current default: Added:
  • inventory.create, inventory.edit, inventory.adjust_stock, inventory.manage_suppliers
  • settings.signatures.view, settings.signatures.manage
Removed:
  • bridge.view (clinical tool, not front-desk)
  • comms.bulk.send (admin-level blast)
  • reports.stats (reporting is admin concern)
  • audit.activity.view (admin concern)

Pro (price_pro)

Same as Pro+ except: Doctor on Pro — remove from Pro+ doctor set:
  • ai.dicom_analysis.view, ai.dicom_analysis.run
  • ai.revenue_forecast, ai.churn_risk, ai.monthly_summary
  • bridge.view, bridge.capture
  • reports.financial, reports.revenue (keep reports.stats)
  • billing.insurance.view, billing.insurance.create, billing.insurance.edit, billing.insurance.manage_attachments
  • clinical.ipd.view, clinical.ipd.admit, clinical.ipd.edit, clinical.ipd.discharge
Receptionist on Pro — remove from Pro+ receptionist set:
  • inventory.create, inventory.edit, inventory.manage_suppliers (keep inventory.view, inventory.adjust_stock, inventory.view_alerts)
  • settings.signatures.manage (keep settings.signatures.view)
  • ai.daily_brief (keep ai.appointment_nudges, ai.payment_reminder)
  • lab.laboratories.view (keep lab.cases.* and lab.services.view)

Fallback (trial / no plan / unknown plan)

Use Pro defaults. Gives new clinics a functional but conservative baseline until a plan is confirmed.

Architecture

1. permissions.ts — new exports

Add two new constants:
export const PRO_PLUS_PERMISSIONS_BY_ROLE: Record<string, PermissionKey[]>
export const PRO_PERMISSIONS_BY_ROLE: Record<string, PermissionKey[]>
Add a resolver helper:
export function getDefaultsForPlan(role: string, planId?: string | null): PermissionKey[]
Maps price_pro_plusPRO_PLUS_PERMISSIONS_BY_ROLE, price_proPRO_PERMISSIONS_BY_ROLE, anything else → PRO_PERMISSIONS_BY_ROLE (safe fallback). Keep DEFAULT_PERMISSIONS_BY_ROLE intact for backwards compatibility — nothing removes it.

2. getEffectivePermissions — signature change

export async function getEffectivePermissions(
  user: { id: string; role: string },
  clinicId?: string | null,
  subscriptionPlanId?: string | null   // new optional param
): Promise<PermissionMap>
Replace:
const defaults = buildPermissionMap(DEFAULT_PERMISSIONS_BY_ROLE[role] || []);
With:
const defaults = buildPermissionMap(getDefaultsForPlan(role, subscriptionPlanId));

3. Callers — pass plan ID

Two call sites need updating: api.ts /me endpoint — already fetches the clinic record which includes subscriptionPlanId. Pass it through. auth.ts invitation acceptance — already has clinicId; add a single extra DB select for the clinic’s subscriptionPlanId before calling getEffectivePermissions, or fetch it as part of the existing clinic lookup. No other callers need changing; they pass undefined for subscriptionPlanId and get Pro defaults as the safe fallback.

Data Flow

Invite accepted
  → auth.ts fetches clinic (has subscriptionPlanId)
  → getEffectivePermissions(user, clinicId, subscriptionPlanId)
  → getDefaultsForPlan(role, 'price_pro_plus') → PRO_PLUS_PERMISSIONS_BY_ROLE[role]
  → buildPermissionMap(keys)
  → mergePermissions(defaults, userClinicAssignment.permissions)  // {} for new user
  → correct plan-gated defaults in effect from day 1

No DB Migration Needed

Existing users with permissions: {} in userClinicAssignments already rely on in-code defaults. Switching from DEFAULT_PERMISSIONS_BY_ROLE to plan-gated defaults is transparent — their empty override is merged on top of the new defaults just as before. Existing users with explicit overrides (like the ssh & Associates receptionist) are unaffected — their stored permissions remain and continue to override the base.

Out of Scope

  • UI for plan-based permission comparison (admin can see effective permissions via existing staff page)
  • Automatic re-sync when clinic upgrades/downgrades plans (existing users keep their stored overrides; only new invites get new defaults)
  • Enterprise plan permissions (add when needed, defaults to all-on like admin)