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_010but 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¶
- Null fields are omitted from JSON (
response_model_exclude_none=True) - No "N/A", dashes, empty sections, or placeholder text
- Doctor photo null → initials avatar with specialty-colored gradient
- Hero image null → brand gradient (teal → deep ocean)
- Provider profile tabs conditional — hidden when insufficient data
- 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.xmlwith priority weights robots.txt: allow storefront paths, disallow/api/