Skip to main content
ORbit is a multi-platform surgical analytics application built on Next.js (web) and SwiftUI (iOS) with a shared PostgreSQL database via Supabase.
This page provides a high-level architectural overview. For detailed schema documentation, see the Data model page. For specific subsystems, see the dedicated pages for Milestones, Scoring, and Flags.

Technology stack

LayerTechnology
Web frontendNext.js (App Router), TypeScript, Tailwind CSS
iOS frontendSwiftUI, MVVM + Repository pattern
UI componentsshadcn/ui, Radix UI, lucide-react icons
ChartsRecharts
Drag and drop@dnd-kit
DatabasePostgreSQL via Supabase
AuthSupabase Auth with RLS
HostingVercel (web), App Store (iOS)

Database overview

The database contains 70+ tables, 85 RPC functions, 25 triggers, and 3 materialized views.

Core tables

TablePurpose
facilitiesMulti-tenant facility definitions
usersAll platform users with facility assignment
casesSurgical cases — the central entity
case_milestonesOne row per milestone per case
case_completion_statsDenormalized stats cache (41 columns)
or_roomsOperating rooms per facility
procedure_typesProcedure catalog with categories
payersInsurance/payer information

Milestone system tables

TablePurpose
milestone_typesGlobal milestone definitions (read-only reference)
facility_milestonesPer-facility milestone definitions
milestone_templatesTemplate definitions (name, facility, flags)
milestone_template_itemsItems within a template (milestone, phase, order)
surgeon_template_overridesPer-surgeon template assignments
facility_phasesPhase definitions per facility
case_milestones references facility_milestone_id, never milestone_type_id. To get global type info, join through facility_milestones.source_milestone_type_id.

Analytics tables

TablePurpose
case_completion_statsPer-case denormalized stats (timing, financial, sequencing)
surgeon_scorecardsCached ORbit Score results
flag_rulesConfigurable flag detection rules
case_flagsFlag instances detected on cases
financial_targetsMonthly profit targets per facility

Trigger system

Cases table (8 triggers)

The cases table has the most triggers of any table. Be cautious modifying case INSERT/UPDATE logic.
TriggerEventAction
record_case_statsAfter UPDATE (data_validated = true)Writes 41 columns to case_completion_stats
sync_soft_delete_columnsBefore UPDATEKeeps is_active and deleted_at in sync
Milestone triggersAfter INSERTPre-creates case_milestones rows from the resolved template

Soft delete trigger

Applied to 20+ tables. The sync_soft_delete_columns() function enforces bidirectional sync:
-- Setting deleted_at also sets is_active = false
-- Setting is_active = false also sets deleted_at = NOW()
-- Clearing either one clears the other
Tables with soft deletes include: body_regions, cancellation_reasons, complexities, cost_categories, delay_types, facility_milestones, implant_companies, milestone_types, or_rooms, payers, procedure_types, users, and more.

Stats pipeline

When a case is validated (data_validated = true), the record_case_stats trigger fires and writes to case_completion_stats:
CategoryColumn countExamples
Identity/FK9case_id, facility_id, surgeon_id, procedure_type_id
Timing9total_duration, surgical_duration, schedule_variance
Sequencing5is_first_case_of_day_room, surgeon_case_sequence
Financial11reimbursement, profit, or_hourly_rate
Workflow7data_validated, is_excluded, exclusion_reason
Three materialized views aggregate this data:
ViewAggregation level
surgeon_procedure_statsPer surgeon, per procedure type
facility_procedure_statsPer facility, per procedure type
surgeon_overall_statsPer surgeon (all procedures)

Row-level security

Every table has RLS policies enforced. The key pattern:
  • Users can only access rows where facility_id matches their assigned facility
  • Global admins can access rows across all facilities
  • Auth is validated via Supabase Auth tokens in middleware

Application structure

apps/web/or-flow-app/
├── app/                    # Next.js App Router pages
├── components/
│   ├── ui/                 # shadcn base components
│   ├── layouts/            # DashboardLayout, Sidebar, Header
│   ├── cases/              # Case management components
│   ├── dashboard/          # Dashboard components
│   ├── analytics/          # Analytics page components
│   ├── data-quality/       # Data quality components
│   ├── settings/           # Settings page components
│   └── block-schedule/     # Block scheduling components
├── lib/
│   ├── dal/                # Data access layer
│   ├── hooks/              # React hooks (useSupabaseQuery, etc.)
│   ├── constants/          # Metrics catalog, status config
│   └── utils/              # Formatting, analytics utilities
├── types/                  # TypeScript type definitions
└── supabase/migrations/    # Database migration files

Data fetching pattern

All data fetching uses the useSupabaseQuery hook:
const { data, isLoading, error } = useSupabaseQuery(
  ['cases', facilityId],
  async (supabase) => {
    const { data, error } = await supabase
      .from('cases')
      .select('*')
      .eq('facility_id', facilityId)
      .eq('is_active', true);
    if (error) throw error;
    return data;
  }
);

Critical patterns

Violating any of these patterns causes bugs. They’re enforced across the codebase.
  • Facility scoping — every query filters by facility_id
  • Soft deletes — always filter is_active = true
  • Milestone v2.0 — use facility_milestone_id as FK
  • No any types — TypeScript strict mode enforced
  • Structured logging — use the logger module, not console.log

Key architectural decisions

Supabase provides PostgreSQL, Auth, RLS, Realtime, and Edge Functions in one platform. The PostgREST layer eliminates the need for a custom REST API for most operations, and RLS enforces security at the database level rather than in application code.
The ORbit Score engine runs client-side in orbitScoreEngine.ts and caches results to surgeon_scorecards. This allows the scoring algorithm to be iterated quickly without database migrations. Cached results are refreshed nightly via pg_cron.
The 41-column denormalized table avoids expensive multi-join queries for analytics. When a case is validated, the trigger pre-computes everything once, and analytics reads are simple SELECTs. The tradeoff is storage and trigger complexity.

Next steps

Dev setup

Set up your local development environment.

Data model

Full reference for all 70+ tables.