A clinically defensible Health Systems Observatory for Canadian emergency department wait-time methodology and data quality.
Wait Time Canada is not a simple wait-time leaderboard. It's a comprehensive health systems observatory that audits and exposes methodological inconsistencies in Canadian emergency department reporting across provinces.
- 4 Provinces: Quebec, Ontario, Alberta, British Columbia
- 380+ Hospitals: Near-real-time monitoring across all regions
- Hourly Updates: Automated data collection currently via GitHub Actions
- First-in-Canada: Real-time ED stretcher occupancy visualization (Quebec)
- Ontario Public Resources Module:
/resourcesnow serves facilities, OSM-backed AED fallback data, Health Canada recalls, AQHI, analytics-only Ontario EMS system context, Ontario reserve-system long-term drinking water advisories, and an official Ontario naloxone link-out with a first-class source catalog
Provincial health authorities report ER wait times using fundamentally different methodologies:
- Quebec starts the clock at REGISTRATION (administrative check-in)
- Ontario starts at TRIAGE (clinical assessment)
- Different statistical measures (P90 vs rolling averages)
- Different patient populations (all vs mid-acuity)
Direct comparison without methodology awareness is clinically misleading. This observatory makes those differences transparent.
As reflected in the current runtime and roadmap baseline on 2026-04-23:
- 4 provinces remain in the active audited source set: Quebec, Ontario, Alberta, and British Columbia
- 380+ hospitals are represented across the current provincial inventories
- 4 distinct statistic types are still present across the active provinces:
MEAN,ROLLING_AVG,POINT_ESTIMATE, andP90 - 6 of 6 cross-province source pairs still fail direct comparability on the current ontology tags
- 1 province currently publishes live stretcher occupancy in the platform: Quebec
These are methodology and operations findings, not performance rankings. They describe what the system is auditing and how the data is structured, not which province or hospital is "better."
- Scraper freshness currently reflects an hourly GitHub Actions cadence rather than continuous ingestion.
- The frontend is now live on the shared VPS at
https://wait-time.ca; the backend scheduler path remains on GitHub Actions because the Ontario source is not reachable from this VPS. - Provincial methodology labels are inferred from public documentation and can lag unannounced reporting changes by source organizations.
- Reported wait times and occupancy values are limited to what provinces publish; the platform cannot surface unreported overcrowding or internal flow constraints.
- The equity layer is currently implemented for Ontario only and should not be generalized to other provinces without province-specific source and tract validation.
- The direct-VPS frontend now uses short-lived in-process response caching for repeated anonymous reads; this reduces Neon public transfer without changing scraper cadence, raw retention, or aggregate storage policy.
- Ontology-Based Architecture: Every measurement tagged with start_event, end_event, statistic_type, patient_scope
- Divergence Warnings: Automatic alerts when comparing incompatible metrics
- Comparability Matrix: Visual display of cross-province methodology alignment
- Deep Linking: Share specific hospital comparisons with methodology context
- Anomaly Detection: Automated flagging of suspicious measurements
- Data Quality Dashboard: Real-time visibility into scraper health, measurement counts, staleness
- Heartbeat Monitoring: Dead Man's Switch alerts via Pushover if data becomes stale
- Methodology Change Detection: Tracks when provincial reporting methods change
- Facilities & Services: Ontario facility search grounded in MOHSERLO reference-directory data
- AED Fallback Map: OpenStreetMap-based AED locations with explicit crowdsourced/incomplete labeling
- Safety Alerts: Health Canada recalls and safety alerts with optional Drug Product Database enrichment
- Ontario EMS Context: Annual land-ambulance response-time summaries presented as background system context only
- Ontario Water Advisories: Official ISC long-term drinking water advisories for Ontario reserve systems with freshness-aware suppression
- Official Naloxone Link-Out: Ontario naloxone guidance is linked to the provincial source rather than republished until reuse rights are clearer
- AQHI Overlay: Cached GeoMet air-quality snapshots with freshness-aware suppression when upstream data is stale
- Source Transparency: Provenance links, last refresh timestamps, and domain-specific caveats on every public-health slice
- First-in-Canada: Stretcher occupancy percentage visualization
- Color-Coded Indicators:
- π’ Green (<90%): Below capacity
- π‘ Yellow (90-110%): Near capacity
- π΄ Red (>110%): Overcrowded with pulse animation
- Clinical Context: >100% indicates overcrowding and potential extended waits
- Peer Benchmarking: Hospital performance vs regional/provincial averages
- Temporal Patterns: Hour-of-day, day-of-week, monthly trend analysis
- Regional Intelligence: 15 health regions mapped with analytics segmentation
- System Trends: Province-wide performance tracking with 90d/6m/1y views
- Mapbox Integration: Geographic visualization of all hospitals
- Live Data Indicators: Real-time updates highlighted
- Distance Calculation: User location-based sorting
- Cluster Markers: Efficient rendering of 380+ locations
- Citation-Ready: CSV/JSON export with methodology metadata
- Granularity Control: Raw measurements, hourly, daily, weekly, or monthly aggregates
- Research-Grade: Full audit trail with payload hashing and parser versioning
- Access Burden Estimator: Fuel + parking cost calculations for patient decision-making
- Provincial Gas Price Awareness: ON $1.55/L, QC $1.60/L, BC $1.75/L
- Distance-Based Analysis: Nearest hospital identification within radius
- Equity Layer Scaffold: Foundation for census tract income overlays (future)
graph TD
subgraph "Provincial Sources"
QC[Quebec MSSS<br/>BeautifulSoup]
ON[Ontario Health<br/>HTTP + HTML tables]
AB[Alberta AHS<br/>Playwright]
BC[BC PHSA<br/>JSON/__NEXT_DATA__]
end
subgraph "Current Scheduler (GitHub Actions)"
CRON[Hourly Cron<br/>Scrapers]
HB[120-Minute Threshold<br/>Heartbeat Monitor]
end
subgraph "Database (Neon PostgreSQL)"
SOURCES[(sources)]
HOSPITALS[(hospitals)]
MEASUREMENTS[(measurements)]
AGGREGATES[(measurement_aggregates)]
QUALITY[(data_quality_snapshots)]
STATUS[(scraper_status)]
end
subgraph "Next.js 14 Frontend"
API[API Routes<br/>/api/hospitals<br/>/api/analytics<br/>/api/data-quality]
PAGES[Pages<br/>Map View<br/>Analytics Dashboard<br/>Methods Page<br/>Resources]
MAP[Mapbox GL JS<br/>380+ Hospital Markers]
end
subgraph "Services"
DB_SVC[DatabaseService]
AGG_SVC[AggregationService]
DQ_SVC[DataQualityService]
ANOM_SVC[AnomalyDetectionService]
end
QC --> CRON
ON --> CRON
AB --> CRON
BC --> CRON
CRON --> DB_SVC
DB_SVC --> MEASUREMENTS
DB_SVC --> STATUS
HB --> STATUS
HB -->|Alert if stale| PUSHOVER[Pushover Notifications]
MEASUREMENTS --> AGG_SVC
AGG_SVC --> AGGREGATES
MEASUREMENTS --> ANOM_SVC
ANOM_SVC --> DQ_SVC
DQ_SVC --> QUALITY
SOURCES -.-> API
HOSPITALS -.-> API
MEASUREMENTS -.-> API
AGGREGATES -.-> API
QUALITY -.-> API
API --> PAGES
PAGES --> MAP
style QC fill:#4A90E2
style ON fill:#4A90E2
style AB fill:#4A90E2
style BC fill:#4A90E2
style CRON fill:#F5A623
style HB fill:#F5A623
style PUSHOVER fill:#D0021B
style MAP fill:#50E3C2
- Language: Python 3.12+
- Testing: pytest with broad unit coverage plus database-backed integration tests; some backend integration and smoke checks require
DATABASE_URL, and the local pipeline smoke path also expects a frontend server onlocalhost:3000 - Scrapers: 4 provincial scrapers (BeautifulSoup, HTTP/HTML parsing, Playwright, JSON extraction)
- Database: Neon PostgreSQL 17 with 15 tables, including the public-health-hub source/resource/alert/system-context lane
- Services:
- DatabaseService, AggregationService, DataQualityService
- AnomalyDetectionService, MethodologyChangeDetector
- GeocodingService (Nominatim), HeartbeatService
- CLI Tools: Scraper runner, database cleanup, storage stats, seeding, aggregation, region mapping
- Framework: Next.js 14 App Router + TypeScript
- Testing: Vitest + React Testing Library for unit/component coverage, with Playwright browser verification kept CI-first, manual-dispatch, and outside the default push/PR merge gate
- Mapping: Mapbox GL JS
- Components: 30+ React components with targeted coverage across major user-facing flows
- API Routes: Hospitals/comparisons, analytics, data quality, and public resources endpoints
- Pages: Home (map + list), /data-quality, /analytics, /methods, /resources, /about
sources- Provincial data source metadatahospitals- Facility metadata with verification workflowmeasurements- Audit log with ontology tags (payload hashing, not full HTML)scraper_status- Heartbeat monitoringscraper_alert_state- Stateful alert incident tracking and recovery suppressionmeasurement_aggregates- Permanent statistical summaries (hourly/daily/weekly/monthly)data_quality_snapshots- Daily scraper reliability metricsmethodology_change_events- Detected methodology shiftsregions- Province region metadata for analyticshospital_regions- Hospital-to-region mappingspublic_data_sources- Public-health-hub source catalog and freshness metadataresource_locations- Normalized facilities and AED locations for/resourcespublic_health_alerts- Health Canada recall and safety alert recordspublic_health_system_metrics- Ontario EMS system-context records for analytics-only/resourcescontext cardspublic_health_source_alert_state- Stateful alerting for public-health ingest incidents
- GitHub Actions: Scrapers run hourly, heartbeat checks every 30 minutes
- Playwright Browsers: Automated for Alberta and browser-based verification flows
- Failure Alerting: Pushover notifications for scraper/heartbeat failures
- Operational Posture: Hourly scraper cadence + 30-minute heartbeat checks on GitHub Actions with state-change alerting and a 120-minute stale threshold
- Frontend Transfer Guardrails:
/api/health,/api/status,/api/hospitals,/api/hospitals/[slug]/trends, and the main analytics routes use shared cache headers plus short-lived in-process server caching on the shared VPS runtime
This project demonstrates multiple CanMEDS competencies for medical school applications:
- Sophisticated metric ontology system for research validity
- Statistical aggregation pipeline (percentiles, rolling averages)
- Methodology change detection and documentation
- Citation-ready data export with full provenance tracking
- Clinical defensibility through methodology transparency
- Divergence warnings prevent misleading comparisons
- Data quality monitoring ensures operational trust
- Peer benchmarking enables evidence-based decisions
- Access Burden Estimator helps vulnerable populations make informed decisions
- Transparency around ED capacity constraints (occupancy data)
- Equity layer foundation for income-based analysis
- Provincial gas price awareness for financial planning
- Multi-province scaling demonstrates systems architecture
- Regional analytics dashboards for health authority insights
- Automated data collection reduces manual burden
- Comprehensive operational documentation
- Province-aware telehealth routing (811 variations by province)
- Attribution to official provincial sources
- Methodology timeline preserves institutional knowledge
This section is for local development. The current production baseline is
split: the public frontend runs on the shared VPS at https://wait-time.ca,
while the authoritative backend scheduler and heartbeat path remain on GitHub
Actions because the Ontario source is still not reliably reachable from that
VPS. Use docs/operations/direct-vps-frontend.md,
docs/operations/direct-vps-backend.md, and
docs/operations/scraper-scheduling.md for the live deployment/runtime
baseline.
- Python 3.12+
- Node.js 22+
- PostgreSQL (Neon account recommended)
- Mapbox account (free tier sufficient)
Wait Time Canada uses standard PostgreSQL and can be run against any compatible database, but the default documented path is Neon:
- create a Neon project
- copy the pooled
DATABASE_URL - export it into your shell before running backend commands
- run the repo migrations and analytics bootstrap commands below
This keeps local setup lightweight while preserving a fully portable Postgres schema and migration workflow.
git clone https://github.com/yourusername/waittimecanada.git
cd waittimecanada
# Frontend environment
cp frontend/.env.example frontend/.env.local
# Edit frontend/.env.local with NEXT_PUBLIC_MAPBOX_TOKEN
# Backend runtime config
export DATABASE_URL="postgresql://user:pass@host:5432/dbname" # pragma: allowlist secretbackend/.env.local can still be kept as a human convenience template, but the
backend runtime expects DATABASE_URL to already be present in the process
environment.
cd backend
# Create virtual environment
python3 -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install dependencies
pip install --upgrade pip
pip install -e '.[dev]' # Note: use quotes in zsh
# Install Playwright browsers (required for Alberta scraper)
playwright install chromium
# Apply database migrations
python run_migrations.py
# Seed sources and bootstrap analytics
python -m waittime.cli.bootstrap_analytics --days 180cd frontend
# Install dependencies
npm install
# Run development server
npm run devOpen http://localhost:3000 to view the app.
cd backend
source .venv/bin/activate
# Run all scrapers
python -m waittime.cli.scraper --all
# Or run single province
python -m waittime.cli.scraper --source quebec-mssssource .venv/bin/activate
# Scrapers
python -m waittime.cli.scraper --all # Run all scrapers
python -m waittime.cli.scraper --source ontario-health # Single province
python -m waittime.cli.scraper --dry-run --all # Test without DB writes
# Maintenance
python -m waittime.cli.check_heartbeat # Verify scraper health
python -m waittime.cli.aggregate --backfill # Regenerate aggregates
python -m waittime.cli.cleanup --dry-run # Preview cleanup
python -m waittime.cli.storage_stats # Inspect measurements table growth
# Testing
pytest tests/unit # Unit tests only
pytest tests/integration # Integration tests (requires DATABASE_URL)
pytest tests/ -v --cov=waittime # Full suite with coverage; backend smoke tests may skip without DATABASE_URL and a local frontend servercd frontend
# Development
npm run dev # Start dev server
npm run build # Production build
npm run start # Run production build
# Quality
npm run type-check # TypeScript validation
npm run lint # ESLint checks
npm run test:unit # Vitest unit testsdocs/planning/roadmap.md- Active roadmap and milestone statusdocs/operations/scraper-scheduling.md- Production operations guideAGENTS.md- repository agent instructions and project architecture
docs/adr/- Architecture Decision Records (16 ADRs)backend/docs/methodologies/- Provincial methodology documentationdocs/case-studies/ottawa-gatineau-divergence.md- Cross-border methodology divergence case studybackend/README.md- Backend architecture and testingfrontend/README.md- Frontend architecture and testing
docs/API.md- API contracts and examplesdocs/reference/data-dictionary.md- Data Dictionary & Schemadocs/architecture/data-flow.md- Data Flow Architecture- Endpoints:
/api/hospitals,/api/comparisons,/api/analytics/*,/api/data-quality
waittimecanada/
βββ backend/
β βββ src/waittime/
β β βββ scrapers/ # Provincial scrapers (QC, ON, AB, BC)
β β βββ services/ # Business logic services
β β βββ core/ # Models, enums, ontology
β β βββ cli/ # Command-line tools
β βββ tests/ # 375+ tests (unit + integration)
β β # plus an opt-in local smoke/E2E path
β βββ migrations/ # Database schema migrations
β βββ seed_data/ # Hospital/region seed data
β βββ docs/ # Backend-specific documentation
βββ frontend/
β βββ app/ # Next.js 14 App Router
β β βββ api/ # API routes
β β βββ data-quality/ # Data quality dashboard
β β βββ analytics/ # Analytics dashboard
β β βββ methods/ # Methodology documentation page
β βββ components/ # React components
β βββ tests/ # 287 frontend tests
β βββ utils/ # Utilities (cache, distance, date)
βββ docs/
β βββ planning/ # Roadmap, strategic plans
β βββ operations/ # Operational guides
β βββ adr/ # Architecture decisions
β βββ methodologies/ # Provincial methodology docs
β βββ architecture/ # System architecture
βββ .github/workflows/ # CI/CD automation (10 workflows)
- Scraper Cron: Runs hourly via GitHub Actions
- Heartbeat Monitor: Checks scraper health every 30 minutes
- Failure Alerts: Pushover notifications for stale data or errors
- Database Cleanup: Manual cleanup keeps a 30-day raw-measurement window; the GitHub Actions maintenance path uses bounded batched deletes and reports storage growth
- Frontend CI: Type checking, linting, unit tests
- Scraper CI: Python tests, coverage reporting
- Production Readiness: Pre-deployment validation
- Docs CI: Documentation quality checks
- Frontend: Shared VPS via host Caddy + loopback Docker container
- Backend: GitHub Actions runners remain live; VPS backend cutover is deferred on Ontario reachability from this host
- Database: Managed Neon PostgreSQL (Launch active in production; free tier is acceptable for local evaluation only)
- Secrets Management: GitHub Secrets for
DATABASE_URL,PUSHOVER_*,MAPBOX_TOKEN
Quick Production Check:
./scripts/verify-production-ops.sh yourusername/waittimecanada- β Never provide medical advice or triage recommendations
- β Always include emergency disclaimer: "Call 911 for emergencies"
- β Display telehealth routing (811 varies by province)
- β Preserve source semantics - never normalize incompatible metrics
- β Surface divergence warnings when comparisons are invalid
- β Hash payloads (SHA256) instead of storing full HTML
- β Tag every measurement with complete ontology metadata
- β Government health authority sources are trusted and auto-approved
- β Quality enforced via anomaly detection and data quality monitoring
- β Heartbeat monitoring ensures data freshness
- β Methodology change detection tracks reporting shifts
- β Link back to official provincial sources
- β Display data provenance in all visualizations
- β Citation-ready export formats
- β Never claim work from automated tools as human authorship
- β M1-M4: Database foundation, Ontario/Quebec scrapers, methodology warnings, PWA setup
- β M7-M8: UX polish, SEO, landing page optimization
- β M9: Portfolio launch artifacts (About section, testimonial governance)
- β M10-M11: Multi-province expansion, Access Burden Estimator
- β M12: Research infrastructure (citation export, Dead Man's Switch alerts)
- β M13: Aggregation pipeline (hourly/daily/weekly/monthly)
- β M14: Data quality & anomaly detection (3 new DB tables)
- β M15: Analytics & benchmarking (peer comparison, temporal patterns)
- β M16: Multi-province operationalization (4 provinces, 380+ hospitals, region mapping)
- β M17: Quebec occupancy implementation (scraper + API)
- β M18: Occupancy frontend UI (visual indicators on hospital cards)
- β M28: Ontario real-data equity layer (StatsCan tract integration)
- β M29: Ontario equity academic rigor hardening (uncertainty + interpretation limits)
- β M30-M33: Reliability hardening, divergence briefs, deployment readiness, and historical occupancy trends
- β Operations: Production verification and comprehensive documentation
- β Operations: CI coverage artifacts retained for both frontend and backend verification
- Backend: broad unit coverage, plus opt-in database-backed integration and
local pipeline smoke checks when
DATABASE_URLand a local frontend server are available - Frontend: Vitest unit/component coverage is part of the normal workflow, while Playwright browser verification stays CI-first and manual-dispatch to conserve GitHub free-tier minutes
- Current source of truth: see
docs/planning/roadmap.mdfor the active validation and deployment posture instead of relying on a fixed test-count snapshot here
- Update Frequency: Hourly via GitHub Actions
- Heartbeat Threshold: 120 minutes (alerts if exceeded)
- Current Status: All 4 scrapers operational β
- Additional provinces (Nova Scotia, New Brunswick)
- Multi-province equity layer beyond Ontario
- Public-facing credibility artifacts (mission/equity/stewardship section, real Zenodo DOI, case study, quantified findings)
- Privacy-safe usage analytics and launch follow-through artifacts
- Prometheus/Grafana monitoring dashboard
- Smart scheduling (reduce frequency during overnight hours)
- Occupancy-based hospital recommendations
- Manitoba scraper (data source unclear)
- Saskatchewan scraper (no public data available)
- Territories expansion (limited data availability)
See docs/planning/roadmap.md for detailed status and next steps.
Contributions are welcome! Please see:
CONTRIBUTING.md- Development workflow and guidelinesCHANGELOG.md- Project history and versioningCITATION.cff- How to cite this project
Use GitHub issue templates for feature requests and data quality reports.
MIT License - See LICENSE for details.
Data Sources:
- Quebec: Ministère de la Santé et des Services sociaux (MSSS)
- Ontario: Health Ontario
- Alberta: Alberta Health Services (AHS)
- British Columbia: Provincial Health Services Authority (PHSA)
All data sourced from publicly available provincial health authority websites.
Wait Time Canada is a public health data and methods observatory focused on transparent interpretation of Canadian emergency department reporting.
The project exists to improve access to operational health-system information, make methodological differences visible, and support safer interpretation of reported wait-time data by patients, clinicians, researchers, journalists, and policy users.
Wait Time Canada is developed with a health equity, EDIA, and barrier-reduction lens. Public health information is often fragmented, inconsistently defined, difficult to compare, or difficult to find. Those gaps can create barriers for people navigating care, for communities experiencing unequal access, and for decision-makers trying to improve the system responsibly.
The project aims to reduce those barriers by improving transparency, surfacing limitations, documenting methodological differences, and prioritizing public-interest access to operational health-system information.
Wait Time Canada prioritizes provenance, comparability, limitations, and responsible reuse of public data. It does not provide medical advice, does not claim to resolve underlying reporting inconsistencies, and does not present invalid cross-system comparisons as if they were directly comparable.
Contact: Use the repository issue tracker or the repository owner's GitHub profile for project inquiries.
- For medical emergencies: Call 911 immediately
- For health advice: Call your provincial health line (811 in most provinces)
- Never delay emergency care based on wait time estimates
Wait time data is for informational purposes only. Clinical decisions should always prioritize patient safety over convenience.