HHE Practitioner Directory

Architecture Briefing — System Documentation & Decision Points
Date: 2026-03-26 Phase: MVP Stack: Next.js 16 + PostgreSQL + Clerk + Twilio
Discovery/Search: Active Auth (Clerk): Active SMS Reminders: Active Typesense: Initialized Stripe: Initialized Redis: Initialized Phone Field: Missing

Platform at a Glance

A holistic health practitioner directory enabling discovery via faceted search with geolocation, appointment booking, Stripe Connect payments, and Twilio SMS reminders. Built on Next.js 16 with React Server Components.

8
Core Entities
10
API Endpoints
3
Active Integrations
3
Dormant Services
155+
Specialty Categories
7
Decisions Pending
ServiceStatusIntegration Level
PostgreSQL + Prisma 6ActiveFull CRUD, faceted search, SMS logging
Clerk AuthActiveOAuth sign-in/up, webhook sync, route protection
Twilio SMSActiveReminders, opt-out, status tracking, cron batch
Typesense SearchInitializedClient created, dual keys — no collections or sync
Stripe ConnectInitializedClient created — no payment endpoints
Redis CacheInitializedSingleton created — health check only

Service Integration Map

All service clients use the singleton pattern with global reference to prevent connection leaks during Next.js hot reload. Production instances are created once per process.

System Integration Flow

Connection Patterns

PostgreSQL

Prisma singleton with connection pooling. Dev logging: queries + errors. Prod: errors only. Extensions: pg_trgm + pgvector.

Redis

ioredis singleton. Max 3 retries per request. Exponential backoff: 50ms × n, capped at 2s.

Typesense

HTTPS on port 443. 2s connection timeout. Dual keys: admin (server) + search-only (NEXT_PUBLIC_ client).

Twilio

Account SID + Auth Token init. E.164 phone format. Helper functions: sendSMS, formatPhoneNumber, isValidPhoneNumber.

Stripe

API v2025-10-29.clover. TypeScript mode. Throws on missing STRIPE_SECRET_KEY env var.

Clerk

ClerkProvider wraps root layout. Webhook signature verification at /api/webhooks/clerk. OAuth redirect flows.

Entity Relationships & Status Lifecycles

8 core entities with PostgreSQL. Snake_case fields in DB, camelCase aliases in API responses. Decimals serialized to numbers. Arrays stored as PostgreSQL text arrays.

Entity Relationship Diagram

Critical Gap: Users table has no phone field. The SMS reminder pipeline executes correctly but produces zero delivered messages — all reminders bounce with error code NO_PHONE_NUMBER.

Booking & SMS Status Lifecycles

Index Strategy

TableIndexPurpose
practitioners[city, state]Location filtering
practitioners[is_active, is_verified]Active/verified filtering
practitioners[latitude, longitude]Geo queries
bookings[scheduled_at]Reminder window queries
bookings[status]Status filtering
sms_logs[to_phone_number]Opt-out lookup
sms_logs[booking_id]Reminder deduplication
reviews[is_published, practitioner_id]Published reviews per practitioner

Endpoint Architecture & Data Flow

Endpoint Inventory

RouteMethodAuthPurpose
/api/practitionersGETPublicSearch/filter with 9 query params + faceted counts
/api/practitioners/[id]GETPublicFull profile with services, reviews, availability
/api/healthGETPublicDB + Redis + Typesense health with latency
/api/reminders/sendPOSTBearerSend SMS reminder for specific booking
/api/cron/remindersPOSTCRON_SECRETBatch send 24-hour reminders (hourly cron)
/api/webhooks/clerkPOSTSignatureUser lifecycle sync
/api/webhooks/stripePOSTSignaturePayment event processing
/api/webhooks/twilio-smsPOSTSMS status updates + inbound messages

Search API Data Flow (GET /api/practitioners)

Component Boundaries & State Flow

Server-first architecture with clear RSC → Client boundaries. All interactive UI uses "use client". State management via useState + useEffect — no external state library.

Route Groups

GroupPathAuthPurpose
(auth)/sign-in, /sign-upPublicClerk authentication UI
(public)/directory, /directory/[id]PublicPractitioner discovery
(protected)/dashboard/*ClerkUser management (placeholder)

Filter Propagation & State Flow

Key Boundary

DirectoryPage (server) passes searchParams to DirectoryClient (client). The client manages all filter state and API fetching. Filter changes trigger: state update → useEffect → buildAPIUrl() → fetch → re-render with new results + counts.

Geolocation Integration

QuickFilters (User-initiated)

"Near Me" button triggers navigator.geolocation. 10s timeout. No cache. Passes lat/lng to API with radius=25mi.

ProfileClient (Auto-request)

Auto-requests on mount. 5-minute cache (maximumAge: 300000). Shows DistanceBadge with color coding: near (≤3mi), moderate (≤10mi), far (>10mi).

Full Lifecycle: Schedule → Send → Track → Opt-Out

The SMS system is the most mature integration — full lifecycle from cron scheduling to delivery tracking to opt-out handling.

SMS Reminder Lifecycle

Blocking Issue: The entire reminder pipeline executes correctly but produces zero delivered messages because users have no phone field. Reminders create bounced sms_log entries with error code NO_PHONE_NUMBER. Resolving CPI-3 (Phone Number Source) unblocks this system.

Configuration

Cron Job

Hourly trigger. Secured by CRON_SECRET (Bearer token or x-cron-secret header). Processes up to 100 bookings per run.

Reminder Window

23-24 hours before appointment. Deduplication via sms_logs check (SENT or DELIVERED status).

Throttling

50ms delay between sends. First 10 errors tracked in response. Batch continues on individual failures.

Opt-Out

STOP/UNSUBSCRIBE/QUIT/CANCEL keywords detected. All sms_logs for that phone marked UNSUBSCRIBED.

7 Architecture Decisions Pending

These items require stakeholder input before engineering can proceed. Each has a recommended path.

CPI-1: Search Architecture Priority
Recommended: Integrate Typesense now
Current SQL search lacks ranking and fuzzy matching. Typesense client already initialized. Geo-search eliminates in-memory Haversine bottleneck. Decision: keep SQL-only, integrate now, or defer post-MVP?
CPI-2: Payment Flow Model
Recommended: Stripe Connect marketplace
stripe_account_id field already exists on practitioners. Connect handles onboarding, split payments, automated payouts, 1099 reporting. Options: direct charge, Connect marketplace, or escrow.
CPI-3: Phone Number Source
Recommended: Add to users table + sync from Clerk
Unblocks the entire SMS reminder pipeline which currently produces zero delivered messages. Add phone field via migration, sync from Clerk webhook events, allow manual override in dashboard.
CPI-4: Redis Caching Strategy
Recommended: Cache search results first
Search API is the most expensive endpoint — dynamic WHERE, in-memory distance filtering, faceted counts. Cache by query hash with 5-minute TTL. Options: practitioner listings, search results, or session data.
CPI-5: Dashboard Priority Features
Recommended: Practitioner dashboard first
Directory is usable by clients without dashboard. Practitioners need profile editing, service management, booking calendar, earnings, reviews. Build practitioner features first.
CPI-6: Review Moderation Model
Recommended: Auto-publish with flagging
is_published and moderated_at fields already exist. Auto-publish on creation, apply keyword/sentiment flagging, queue flagged reviews for admin moderation.
CPI-7: Availability & Time Slot Granularity
Recommended: Add time slots
Current availability is day-level only with no slot validation or double-booking prevention. Time slots enable precise scheduling, conflict detection, and buffer time. Decision on slot duration: 15min, 30min, or 60min.

Capture your decisions in the interactive input form →

Dependency Chain & Phase Gates

These items activate when current priorities and client decisions resolve.

Dependency Flow

CPI-1 + CPI-3
Gate 1: SMS + Search Sprint
CPI-2
Gate 2: Payment Sprint
CPI-5
Gate 3: Dashboard Sprint

Deferred Items

ItemBlocked OnCurrent State
Typesense Collection SyncCPI-1 (Search priority)Client initialized, no collections
Stripe Connect OnboardingCPI-2 (Payment model)Client initialized, stripe_account_id field exists
SMS Delivery ActivationCPI-3 (Phone source)Pipeline functional, zero deliveries
Dashboard Feature BuildCPI-5 (Dashboard scope)Layout + routes exist, pages are placeholders
Booking Conflict DetectionCPI-7 (Time slots)Day-level availability, no slot validation
Semantic Search (pgvector)D-1 complete (Typesense first)search_vector field exists, embedding pipeline undesigned