Last Updated: 2026-02-07
Audited From: Actual codebase in server/
Protocol Guide's backend serves the national EMS protocol library through a jurisdiction-aware RAG pipeline. Express + tRPC provides type-safe endpoints; the core pipeline normalizes EMS queries, resolves jurisdiction (county → LEMSA/agency), executes scoped vector search against 58,000+ protocol chunks, re-ranks results, and generates cited answers via Claude.
The backend's primary function is jurisdiction-scoped protocol retrieval. Every search flows through this pipeline:
User Query: "epi dose for cardiac arrest"
│
┌───────────────▼───────────────────┐
│ 1. EMS Query Normalizer │ server/_core/ems-query-normalizer.ts
│ • Expand abbreviations │ "epi" → "epinephrine"
│ (150+ EMS terms) │
│ • Classify intent │ → medication_dosing
│ • Detect emergent indicators │ → isEmergent = true
│ • Extract medications/conditions│
└───────────────┬───────────────────┘
│
┌───────────────▼───────────────────┐
│ 2. Jurisdiction Resolution │ server/routers/search.ts
│ • countyId → county_agency_ │ via county_agency_mapping table
│ mapping → agency_id │
│ • Resolves LEMSA/agency for │
│ scoped search │
└───────────────┬───────────────────┘
│
┌───────────────▼───────────────────┐
│ 3. Vector Search (Scoped) │ server/_core/embeddings/
│ • Generate query embedding │ Voyage AI voyage-large-2 (1536 dim)
│ • pgvector cosine similarity │ Supabase search_manus_protocols RPC
│ • WHERE agency_id = X │ Filtered to user's jurisdiction
│ • Optional: multi-query fusion │ For medication/safety queries
└───────────────┬───────────────────┘
│
┌───────────────▼───────────────────┐
│ 4. Re-ranking │ server/_core/rag/scoring.ts
│ • Term frequency (+20 max) │
│ • Synonym matching (+15 max) │
│ • Protocol number match (+50) │
│ • Title relevance (+8) │
│ • Context boost: same agency │ +15 points
│ (+15), same state (+5) │
└───────────────┬───────────────────┘
│
┌───────────────▼───────────────────┐
│ 5. Claude RAG Response │ server/_core/claude.ts
│ • Retrieval-only mode │ No training data generation
│ • Mandatory citations │ [Protocol #XXX]
│ • Model: Haiku (free/simple) │
│ or Sonnet (pro/complex) │
│ • 3-10 sentence limit │
└───────────────┬───────────────────┘
│
▼
"Epinephrine 1mg IV/IO q3-5min [Protocol 4.2 - Cardiac Arrest]"
Safety-Critical Query Detection
Certain query types receive enhanced processing regardless of subscription tier:
Trigger
Enhanced Behavior
Medication dosing
Multi-query fusion (3 variations), threshold 0.38
Contraindication checks
Highest priority (100), enhanced accuracy
Emergent patterns (cardiac arrest, anaphylaxis, airway obstruction)
Always enhanced processing
Pediatric + medication
Weight-based dosing alerts, enhanced accuracy
File
Purpose
server/_core/ems-query-normalizer.ts
150+ EMS abbreviation expansion, intent classification, emergent detection
server/_core/embeddings/search.ts
Jurisdiction-scoped vector search via Supabase pgvector
server/_core/rag/search-execution.ts
Orchestrates search: cache check → threshold selection → search → re-rank → cache
server/_core/rag/scoring.ts
Advanced re-ranking with intent-specific signal weighting
server/_core/rag/multi-query.ts
Multi-query fusion for medication/safety queries
server/_core/claude.ts
Claude SDK with retrieval-only system prompt, model routing
server/_core/protocol-chunker.ts
Semantic chunking (400-1800 chars) with content type classification
The server is a single Express application that:
Mounts security middleware (Helmet, CORS, CSP nonce)
Registers raw Express routes (webhooks, OAuth, health)
Mounts tRPC router at /api/trpc
Serves static files in production
# Development (with hot reload)
pnpm dev:server
# Production
pnpm build && pnpm start
Platform: Railway
Build: esbuild bundles to dist/index.js
Static Assets: Served from dist/ after Expo web build
tRPC provides type-safe API calls. All routers are composed in server/routers.ts.
Router
Path
Description
system
system.*
Health checks, admin notifications
auth
auth.*
Login/logout, session management (Supabase OAuth)
counties
counties.*
U.S. county listing (2,713 counties across 53 states/territories)
user
user.*
User profile, county/agency jurisdiction selection, disclaimer acknowledgment
search
search.*
Core pipeline : jurisdiction-scoped semantic search (county→agency→vector search)
query
query.*
Core pipeline : protocol RAG queries (normalizer→search→Claude response)
voice
voice.*
Voice transcription with EMS terminology recognition
feedback
feedback.*
User feedback on protocol accuracy
contact
contact.*
Public contact form, waitlist
subscription
subscription.*
Stripe checkout/portal (free/pro/enterprise)
admin
admin.*
Admin-only operations
agencyAdmin
agencyAdmin.*
B2B agency management — protocol upload, team management, analytics
integration
integration.*
ePCR partner tracking (ImageTrend, ESOS, Zoll, EMSCloud)
referral
referral.*
Referral/viral growth system
jobs
jobs.*
Cron job triggers
Procedure
Type
Auth
Description
health
query
public
Health check
notifyOwner
mutation
admin
Send notification to owner
Procedure
Type
Auth
Description
me
query
public (rate limited)
Get current user
logout
mutation
CSRF
Clear session, revoke token
logoutAllDevices
mutation
protected
Revoke all tokens
changePassword
mutation
protected
Change password (verifies current)
updateEmail
mutation
protected
Update email address
securityStatus
query
protected
Check token revocation status
Procedure
Type
Auth
Description
list
query
public (rate limited)
List all counties grouped by state
get
query
public (rate limited)
Get county by ID
Procedure
Type
Auth
Description
usage
query
protected
Get daily query usage
acknowledgeDisclaimer
mutation
protected
Record disclaimer acceptance
hasAcknowledgedDisclaimer
query
protected
Check disclaimer status
selectCounty
mutation
protected
Set user's selected county
queries
query
protected
Get query history
savedCounties
query
protected
Get saved counties (tier-limited)
addCounty
mutation
protected
Add county to saved list
removeCounty
mutation
protected
Remove county from saved list
setPrimaryCounty
mutation
protected
Set primary county
primaryCounty
query
protected
Get primary county
savePushToken
mutation
protected
Save push notification token
Procedure
Type
Auth
Description
semantic
query
public (rate limited)
Semantic search with Voyage AI
getProtocol
query
public (rate limited)
Get protocol by ID
stats
query
public (rate limited)
Protocol statistics
coverageByState
query
public (rate limited)
Protocol coverage by state
totalStats
query
public (rate limited)
Total protocol counts
agenciesByState
query
public (rate limited)
Agencies in a state
agenciesWithProtocols
query
public (rate limited)
All agencies with protocols
searchByAgency
query
public (rate limited)
Search within specific agency
summarize
query
public (rate limited)
Summarize protocol content
Procedure
Type
Auth
Description
submit
mutation
protected
Submit RAG query to Claude
history
query
protected
Get query history
searchHistory
query
protected
Get search history (Pro)
syncHistory
mutation
protected (Pro)
Sync local history to cloud
clearHistory
mutation
protected
Clear search history
deleteHistoryEntry
mutation
protected
Delete single history entry
Procedure
Type
Auth
Description
transcribe
mutation
rate limited
Transcribe audio (Whisper)
uploadAudio
mutation
rate limited
Upload audio for transcription
Procedure
Type
Auth
Description
submit
mutation
protected
Submit feedback
myFeedback
query
protected
Get user's feedback
Procedure
Type
Auth
Description
submit
mutation
strict public rate limited
Submit contact form
subscribeWaitlist
mutation
strict public rate limited
Join waitlist
Procedure
Type
Auth
Description
createCheckout
mutation
protected
Create Stripe checkout session
createPortal
mutation
protected
Create Stripe portal session
status
query
protected
Get subscription status
createDepartmentCheckout
mutation
protected
Create agency checkout
Procedure
Type
Auth
Description
listFeedback
query
admin
List all feedback
updateFeedback
mutation
admin
Update feedback status
listUsers
query
admin
List all users
updateUserRole
mutation
admin
Change user role
listContactSubmissions
query
admin
List contact forms
updateContactStatus
mutation
admin
Update contact status
getAuditLogs
query
admin
Get audit logs
Procedure
Type
Auth
Description
myAgencies
query
protected
Get user's agencies
getAgency
query
protected
Get agency details
updateAgency
mutation
protected
Update agency
listMembers
query
protected
List agency members
inviteMember
mutation
protected
Invite member
updateMemberRole
mutation
protected
Change member role
removeMember
mutation
protected
Remove member
listProtocols
query
protected
List agency protocols
uploadProtocol
mutation
protected
Upload protocol
getUploadStatus
query
protected
Check upload progress
updateProtocolStatus
mutation
protected
Change protocol status
publishProtocol
mutation
protected
Publish protocol
archiveProtocol
mutation
protected
Archive protocol
listVersions
query
protected
List protocol versions
createVersion
mutation
protected
Create new version
getSearchAnalytics
query
protected
Search analytics
getProtocolAnalytics
query
protected
Protocol analytics
getUserAnalytics
query
protected
User analytics
getErrorAnalytics
query
protected
Error analytics
exportAnalytics
mutation
protected
Export analytics
Procedure
Type
Auth
Description
logAccess
mutation
strict public rate limited
Log partner access
getStats
query
admin
Integration statistics
getRecentLogs
query
admin
Recent integration logs
getDailyUsage
query
admin
Daily usage charts
Procedure
Type
Auth
Description
getMyReferralCode
query
protected
Get user's referral code
getMyStats
query
protected
Get referral statistics
getMyReferrals
query
protected
List referrals
validateCode
query
protected
Validate referral code
redeemCode
mutation
protected
Redeem referral code
getShareTemplates
query
protected
Get share templates
getLeaderboard
query
protected
Referral leaderboard
trackViralEvent
mutation
protected
Track viral event
Procedure
Type
Auth
Description
runDripEmails
mutation
CRON_SECRET
Trigger drip email job
These are raw Express routes outside of tRPC:
Endpoint
Method
Description
/api/health
GET
Basic health check
/api/ready
GET
Kubernetes readiness probe
/api/live
GET
Kubernetes liveness probe
/api/resilience
GET
Circuit breaker status
Endpoint
Method
Description
/api/auth/supabase/callback
GET
Supabase OAuth callback
Endpoint
Method
Description
/api/stripe/webhook
POST
Stripe webhook handler
Endpoint
Method
Description
/api/imagetrend/launch
GET
ImageTrend deep link
/api/imagetrend/health
GET
ImageTrend integration health
/api/imagetrend/suggest
POST
AI protocol suggestions (mock)
/api/imagetrend/export
POST
Export to ePCR (mock)
Endpoint
Method
Description
/api/summarize
POST
Protocol summarization (Claude)
/api/client-error
POST
Client error reporting (Sentry)
Supabase Auth + Custom DB
Login/Signup: Client uses Supabase Auth (email/password or OAuth)
Token Exchange: Client receives Supabase JWT
API Calls: Client sends Authorization: Bearer <jwt>
Context Creation: Server validates JWT via Supabase Admin SDK
User Lookup: Server finds/creates user in MySQL by supabase_id
Token Blacklist: Check if user's tokens have been revoked
Context Creation (server/_core/context.ts)
export async function createContext ( opts ) : Promise < TrpcContext > {
// 1. Extract Bearer token
const token = authHeader ?. replace ( "Bearer " , "" ) ;
// 2. Verify with Supabase
const { data : { user : supabaseUser } } = await supabaseAdmin . auth . getUser ( token ) ;
// 3. Find/create in our DB
user = await db . findOrCreateUserBySupabaseId ( supabaseUser . id , { ...} ) ;
// 4. Check token blacklist
if ( await isTokenRevoked ( user . id ) ) user = null ;
return { req, res, user, trace } ;
}
Pattern: Double-submit cookie
Cookie: csrf_token (httpOnly: false so JS can read)
Header: x-csrf-token
Validation: Constant-time comparison (prevents timing attacks)
Scope: All mutations (queries are exempt)
Order matters. Here's the actual order from server/_core/index.ts:
CSP Nonce Generation - Per-request nonce for inline scripts
Helmet - Security headers (CSP, HSTS, X-Frame-Options, etc.)
Timeout - 30s request timeout
HTTP Logger - Pino structured logging
CORS - Whitelist-based origin validation
Stripe Webhook - Raw body handler (before JSON parsing)
JSON Parser - 10MB limit
Cookie Middleware - Parse cookies, set CSRF token
OAuth Routes - Supabase callback
Health Endpoints - With rate limiting
AI Endpoint - /api/summarize with AI rate limiter
ImageTrend Routes - Integration endpoints
tRPC Middleware - Main API at /api/trpc
Static Files - Production only
Sentry Error Handler - Last, catches all errors
Security Headers (Helmet)
contentSecurityPolicy: {
defaultSrc : [ "'self'" ] ,
scriptSrc : [ "'self'" , ( req , res ) => `'nonce-${ res . locals . cspNonce } '` ] ,
imgSrc : [ "'self'" , "data:" , "blob:" , "https://*.supabase.co" ] ,
connectSrc : [ "'self'" , "https://*.supabase.co" , /* whitelisted domains */ ] ,
}
hsts: { maxAge : 31536000 , preload : true }
frameguard: "deny"
Service
Purpose
Env Variable
Supabase
Auth, vector storage (pgvector)
SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY
MySQL
Primary database (users, queries, etc.)
DATABASE_URL
Anthropic (Claude)
RAG responses
ANTHROPIC_API_KEY
Voyage AI
Embedding generation
VOYAGE_API_KEY
Stripe
Payments
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET
Service
Purpose
Env Variable
Redis/Upstash
Distributed rate limiting, caching
UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN
Resend
Transactional emails
RESEND_API_KEY
Sentry
Error tracking
SENTRY_DSN
OpenAI
Voice transcription (Whisper)
OPENAI_API_KEY
Forge Storage
File uploads
BUILT_IN_FORGE_API_URL, BUILT_IN_FORGE_API_KEY
Tier
Query Type
Model
Cost
Free
All queries
Claude Haiku 4.5
~$0.0003-0.0005/query
Pro
Simple queries
Claude Haiku 4.5
~$0.0003-0.0005/query
Pro
Complex queries
Claude Sonnet 4.6
~$0.002-0.004/query
Complex query indicators: "multiple", "compare", "differential", "pediatric", "pregnancy", "mechanism"
Custom Error Types (server/_core/errors.ts)
// Base class
class ProtocolGuideError extends Error {
code : string ;
userMessage : string ;
statusCode : number ;
retryable : boolean ;
requestId ?: string ;
}
// Claude errors
ClaudeRateLimitError // 429 - retryable
ClaudeAuthError // 500 - not retryable
ClaudeServerError // 503 - retryable
ClaudeOverloadedError // 503 - retryable
ClaudeTimeoutError // 504 - retryable
// Voyage errors
VoyageRateLimitError
VoyageAuthError
VoyageServerError
VoyageTimeoutError
Claude API calls use exponential backoff:
Max retries: 3
Initial delay: 1000ms
Max delay: 10000ms
Backoff multiplier: 2x
Jitter: 10-20% random variation
All tRPC errors include:
{
"code" : " UNAUTHORIZED" ,
"message" : " User-friendly message" ,
"data" : {
"requestId" : " uuid-for-support" ,
"timestamp" : " ISO-8601"
}
}
Procedure Type
Limit
Notes
publicProcedure
None
Open access
publicRateLimitedProcedure
10 req/15min per IP
Public endpoints
strictPublicRateLimitedProcedure
5 req/15min per IP
Sensitive public endpoints
rateLimitedProcedure
Tier-based daily limits
Authenticated endpoints
Daily Query Limits (tRPC)
Tier
Daily Queries
Free
10/day
Pro
100/day
Enterprise
Unlimited
Express Rate Limiters (Redis-based)
Limiter
Endpoint
Limit
publicLimiter
Most endpoints
Tier-aware, IP-based
searchLimiter
Search endpoints
Free: 30/min, Pro: 100/min
aiLimiter
/api/summarize
Free: 10/min, Pro: 50/min
Rate Limit Headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1706054400
X-RateLimit-Daily-Limit: 10
X-RateLimit-Daily-Remaining: 8
X-RateLimit-Daily-Reset: 1706140800
Retry-After: 300
Dual Database Architecture
MySQL (Drizzle ORM) - Primary database
Users, queries, feedback, agencies
Connection pooling with resilience wrapper
Supabase (PostgreSQL + pgvector) - Vector storage
Protocol embeddings
Semantic search via search_protocols_semantic RPC
Database Modules (server/db/)
Module
Purpose
connection.ts
Connection management, pooling
users.ts
User CRUD
users-auth.ts
OAuth, auth operations
users-usage.ts
Usage tracking, tier management
counties.ts
County operations
protocols.ts
Protocol CRUD
protocols-search.ts
Semantic search
queries.ts
Query history
feedback.ts
Feedback, contact forms
admin.ts
Admin ops, audit logs
agencies.ts
Agency management
protocol-versions.ts
Version control
Circuit Breaker: Prevents cascade failures
Fallback Cache: In-memory cache when Redis unavailable
Slow Query Monitoring: Logs queries >500ms as warning, >2000ms as error
Located in netlify/edge-functions/:
Function
Path
Purpose
cache-static.ts
/api/static/*
CDN caching for stats/coverage
geo-route.ts
-
Geo-based routing
server/
├── _core/ # Core infrastructure
│ ├── index.ts # Express server entry
│ ├── trpc.ts # tRPC setup, procedures
│ ├── context.ts # Request context creation
│ ├── claude.ts # Claude AI integration
│ ├── errors.ts # Custom error types
│ ├── logger.ts # Pino logging
│ ├── rateLimit.ts # In-memory rate limiting
│ ├── rateLimitRedis.ts # Redis rate limiting
│ ├── redis.ts # Redis client
│ ├── sentry.ts # Sentry integration
│ ├── health.ts # Health check handlers
│ ├── oauth.ts # OAuth routes
│ ├── email.ts # Resend email service
│ ├── cookies.ts # Cookie utilities
│ ├── csrf.ts # CSRF (deprecated, use tRPC)
│ ├── tier-validation.ts # Subscription tier checks
│ ├── token-blacklist.ts # Token revocation
│ ├── timeout.ts # Request timeout
│ ├── tracing.ts # Distributed tracing
│ ├── env.ts # Environment validation
│ ├── embeddings/ # Voyage AI embeddings
│ ├── guardrails/ # Dose safety checks
│ ├── rag/ # RAG optimization
│ └── resilience/ # Circuit breakers, caches
│
├── api/ # Express route handlers
│ ├── imagetrend.ts # ImageTrend launch
│ ├── imagetrend-suggest.ts # AI suggestions
│ ├── summarize.ts # Protocol summarization
│ └── client-error.ts # Error reporting
│
├── routers/ # tRPC routers
│ ├── index.ts # Router exports
│ ├── auth.ts
│ ├── counties.ts
│ ├── user.ts
│ ├── search.ts
│ ├── query.ts
│ ├── voice.ts
│ ├── feedback.ts
│ ├── contact.ts
│ ├── subscription.ts
│ ├── admin.ts
│ ├── integration.ts
│ ├── jobs.ts
│ ├── agency-admin/ # Agency management
│ │ ├── agency.ts
│ │ ├── analytics.ts
│ │ ├── middleware.ts
│ │ ├── protocols.ts
│ │ ├── staff.ts
│ │ └── versions.ts
│ └── referral/ # Referral system
│ ├── analytics-procedures.ts
│ ├── code-procedures.ts
│ ├── constants.ts
│ └── user-procedures.ts
│
├── db/ # Database modules
│ ├── index.ts
│ ├── config.ts
│ ├── connection.ts
│ ├── users.ts
│ ├── users-auth.ts
│ ├── users-usage.ts
│ ├── counties.ts
│ ├── protocols.ts
│ ├── protocols-search.ts
│ ├── queries.ts
│ ├── feedback.ts
│ ├── admin.ts
│ ├── agencies.ts
│ └── protocol-versions.ts
│
├── webhooks/
│ └── stripe.ts # Stripe webhook handler
│
├── jobs/
│ ├── protocol-processor.ts
│ └── send-drip-emails.ts
│
├── emails/
│ └── templates/
│ └── index.ts # Email templates
│
├── lib/
│ ├── pricing.ts # Pricing configuration
│ └── state-codes.ts # State code utilities
│
├── routers.ts # Main router composition
├── db.ts # DB re-exports
├── storage.ts # File storage (Forge)
├── stripe.ts # Stripe client
└── subscription-access.ts # Subscription helpers
Adding a New tRPC Procedure
Create procedure in appropriate router (server/routers/*.ts)
Choose correct procedure type:
publicProcedure - No auth
publicRateLimitedProcedure - No auth + rate limit
protectedProcedure - Auth + CSRF
adminProcedure - Admin only
rateLimitedProcedure - Auth + daily limits
Add to router export
Types auto-propagate to client via lib/trpc.ts
Adding a New Express Route
Add handler in server/api/*.ts
Register in server/_core/index.ts
Add appropriate rate limiter
Update CORS if needed
See docs/ENVIRONMENT.md for complete list. Critical ones:
# Required
DATABASE_URL=mysql://...
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_SERVICE_ROLE_KEY=...
ANTHROPIC_API_KEY=sk-ant-...
VOYAGE_API_KEY=...
STRIPE_SECRET_KEY=sk_...
STRIPE_WEBHOOK_SECRET=whsec_...
# Production
NODE_ENV=production
APP_URL=https://protocol-guide.com