Skip to content

Provider Storefront

Public, SEO-friendly marketplace where patients discover hospitals, doctors, treatments, and destinations. No authentication required. No pricing displayed — cost estimates happen inside the AI navigator conversation.


Architecture

graph LR
    subgraph "Public Web (curaway.ai)"
        A[Patient Browser]
    end

    subgraph "Backend (services.curaway.ai)"
        B[Public Router<br/>GET only, no auth]
        C[Cache Service<br/>Upstash Redis]
        D[PostgreSQL]
        E[Qdrant Search]
    end

    A -->|GET /api/v1/public/*| B
    B --> C
    C -->|MISS| D
    C -->|MISS| E
    C -->|HIT| A

    style B fill:#008B8B,color:#fff
    style C fill:#FF7F50,color:#fff

API Endpoints (9)

All under /api/v1/public/ — no authentication, rate limited to 60 req/min per IP.

Endpoint Cache TTL Description
GET /providers 6hr Paginated, filterable by country, specialty, accreditation, language
GET /providers/{slug} 6hr Full profile with facilities, doctors, cultural support, travel info
GET /doctors 6hr Filterable by specialty, language, gender, country
GET /doctors/{slug} 6hr Doctor profile (requires completeness >= 0.60)
GET /treatments 24hr Treatment categories with provider counts
GET /treatments/{slug} 6hr Treatment detail with offering providers
GET /destinations 24hr Country list with provider counts and visa info
GET /destinations/{slug} 6hr Country detail with all providers and travel defaults
GET /search?q= 1hr Parallel PostgreSQL + Qdrant search, grouped results

No Pricing Rule

No response from the public API contains pricing data. Fields excluded: - cost, price, priceRange, cost_breakdown, cost_range_usd_cents - procedure_costs, procedure_costs_converted, cost_index

Procedures are returned as List[str] (names only, not objects with pricing).


Caching Strategy

graph TD
    A[Request] --> B{Redis Cache}
    B -->|HIT| C[Return cached + X-Cache: HIT]
    B -->|MISS| D[Query PostgreSQL]
    B -->|ERROR| E[Query PostgreSQL + X-Cache: MISS-ERROR]
    D --> F[Store in Redis]
    F --> G[Return + X-Cache: MISS]
    E --> G
  • Non-fatal: Redis failure logs PUBLIC_010 but never blocks the request
  • Cache keys: pub:{entity}:{md5(params)[:12]}
  • Invalidation: QStash event on provider/doctor update → bust specific keys + Vercel ISR revalidation

Provider Completeness Scoring

13 weighted dimensions determine display tier:

Dimension Weight Threshold
Description (200+ chars) 0.10
Accreditations (>= 1) 0.10
Bed count 0.05
Specialties (>= 3) 0.10
Procedures (>= 3) 0.10
Doctors (>= 1) 0.15
Facilities (>= 2) 0.10
Languages (>= 3) 0.05
Cultural support (>= 2 fields) 0.05
Hero image 0.05
International patients 0.05
Rating 0.05
Travel info 0.05

Tiers:

Tier Score Display
Gold >= 0.80 Full card with all details
Standard >= 0.50 Same layout, collapsed gaps
Basic < 0.50 Compact: name, location, specialties only

Travel Defaults

Country and city-level travel information loaded from config/travel_defaults.yaml. Covers 8 countries (India, Turkey, Thailand, UAE, Spain, Mexico, South Korea, Costa Rica).

Provider-specific travel data takes precedence over defaults. The Travel tab always has content.


Missing Data UX Rules

  1. Null fields are omitted from JSON (response_model_exclude_none=True)
  2. No "N/A", dashes, empty sections, or placeholder text
  3. Doctor photo null → initials avatar with specialty-colored gradient
  4. Hero image null → brand gradient (teal → deep ocean)
  5. Provider profile tabs conditional — hidden when insufficient data
  6. Stats bar hides individual unknown metrics

Frontend Routes (10)

/treatments                                        → Treatment index
/treatments/:treatmentSlug                         → Treatment detail
/destinations                                      → Destination index
/destinations/:countrySlug                         → Country detail
/destinations/:countrySlug/hospitals               → Provider listing
/destinations/:countrySlug/hospitals/:providerSlug → Provider profile
/doctors                                           → Doctor listing
/doctors/:doctorSlug                               → Doctor profile
/search                                            → Search results
/how-it-works                                      → How it works

Error Codes

Code HTTP Description
PUBLIC_PROVIDER_NOT_FOUND_001 404 Provider slug not found
PUBLIC_DOCTOR_NOT_FOUND_002 404 Doctor slug not found
PUBLIC_TREATMENT_NOT_FOUND_003 404 Treatment slug not found
PUBLIC_DESTINATION_NOT_FOUND_004 404 Country slug not found
PUBLIC_INVALID_FILTER_005 400 Invalid filter value
PUBLIC_INVALID_SORT_006 400 Invalid sort_by parameter
PUBLIC_SEARCH_QUERY_TOO_SHORT_007 400 Query < 2 characters
PUBLIC_SEARCH_QUERY_TOO_LONG_008 400 Query > 200 characters
PUBLIC_RATE_LIMIT_EXCEEDED_009 429 60 req/min exceeded
PUBLIC_CACHE_ERROR_010 500 Redis failure (non-fatal)
PUBLIC_PAGINATION_INVALID_011 400 Invalid page/per_page
PUBLIC_SLUG_CONFLICT_012 409 Duplicate slug

Feature Flags

Flag Default Purpose
PUBLIC_API_ENABLED OFF Master switch for entire public API
STOREFRONT_PAGES_ENABLED OFF Frontend route visibility
PUBLIC_SEARCH_ENABLED OFF Search can be disabled independently
STOREFRONT_CACHE_ENABLED OFF Disable cache for debugging
PROVIDER_COMPLETENESS_DISPLAY OFF Show/hide completeness badges

SEO

  • JSON-LD: MedicalOrganization (providers), Physician (doctors), MedicalProcedure (treatments), BreadcrumbList (all pages)
  • Meta title + description on every page
  • Open Graph tags on every page
  • Canonical URLs, rel="next"/"prev" on paginated listings
  • Sitemap at /sitemap.xml with priority weights
  • robots.txt: allow storefront paths, disallow /api/