Version: 1.0 Last Updated: 2026-01-23
- Overview
- Authentication
- Router Organization
- System Router
- Auth Router
- Counties Router
- User Router
- Search Router
- Query Router
- Voice Router
- Feedback Router
- Contact Router
- Subscription Router
- Admin Router
- Agency Admin Router
- Integration Router
- Referral Router
- Error Handling
- Rate Limiting
Protocol Guide uses tRPC for type-safe API communication between the mobile app and backend server. All procedures are organized by domain and support end-to-end TypeScript type safety.
Base URL: https://api.protocolguide.app/trpc
Features:
- Type-safe API contracts
- SuperJSON serialization (supports Date objects, etc.)
- Automatic validation via Zod schemas
- Built-in error handling
- Context-aware authentication
Protocol Guide uses Supabase Auth with Bearer token authentication.
Header Format:
Authorization: Bearer <supabase_access_token>
| Type | Authentication | Description |
|---|---|---|
publicProcedure |
None | No authentication required |
protectedProcedure |
User | Requires authenticated user |
paidProcedure |
Pro/Enterprise | Requires paid subscription tier |
rateLimitedProcedure |
User + Rate Limit | Enforces daily query limits |
adminProcedure |
Admin Role | Requires admin user role |
agencyAdminProcedure |
Agency Admin | Requires agency admin access |
Authenticated procedures receive a ctx.user object:
{
id: number;
email: string | null;
name: string | null;
role: "user" | "admin";
tier: "free" | "pro" | "enterprise";
selectedCountyId: number | null;
stripeCustomerId: string | null;
subscriptionStatus: string | null;
subscriptionEndDate: Date | null;
}appRouter/
├── system # Health checks, notifications
├── auth # Authentication
├── counties # County listings
├── user # User profile and settings
├── search # Semantic protocol search
├── query # Protocol query submission
├── voice # Voice transcription
├── feedback # User feedback
├── contact # Contact form
├── subscription # Stripe payments
├── admin # Admin operations
├── agencyAdmin # B2B agency management
├── integration # Partner tracking
└── referral # Viral referral system
Health check endpoint for monitoring.
Type: query
Auth: Public
Input:
{
timestamp: number; // Must be >= 0
}Output:
{
ok: boolean;
}Example:
const result = await trpc.system.health.query({ timestamp: Date.now() });
// { ok: true }Send notification to system owner (admin only).
Type: mutation
Auth: Admin
Input:
{
title: string; // Required, min 1 char
content: string; // Required, min 1 char
}Output:
{
success: boolean;
}Example:
const result = await trpc.system.notifyOwner.mutate({
title: "Critical Alert",
content: "Database backup completed successfully"
});Get current authenticated user information.
Type: query
Auth: Public (returns null if not authenticated)
Input: None
Output:
User | nullExample:
const user = await trpc.auth.me.query();
// { id: 123, email: "medic@example.com", name: "John Doe", tier: "pro", ... }Clear authentication session cookie.
Type: mutation
Auth: Public
Input: None
Output:
{
success: true;
}Example:
await trpc.auth.logout.mutate();List all available counties grouped by state.
Type: query
Auth: Public
Input: None
Output:
{
counties: Array<{
id: number;
name: string;
state: string;
// ... other county fields
}>;
grouped: Record<string, Array<County>>;
}Example:
const { counties, grouped } = await trpc.counties.list.query();
// grouped["CA"] = [{ id: 1, name: "Los Angeles County", ... }, ...]Get a specific county by ID.
Type: query
Auth: Public
Input:
{
id: number;
}Output:
County | nullExample:
const county = await trpc.counties.get.query({ id: 1 });Get user's query usage statistics.
Type: query
Auth: Protected
Input: None
Output:
{
count: number; // Queries used today
limit: number; // Daily query limit
tier: "free" | "pro" | "enterprise";
}Example:
const usage = await trpc.user.usage.query();
// { count: 5, limit: 10, tier: "free" }Record user's acknowledgment of medical disclaimer (P0 CRITICAL for legal compliance).
Type: mutation
Auth: Protected
Input: None
Output:
{
acknowledgedAt: Date;
}Example:
await trpc.user.acknowledgeDisclaimer.mutate();Check if user has acknowledged medical disclaimer.
Type: query
Auth: Protected
Input: None
Output:
{
hasAcknowledged: boolean;
}Set user's selected county.
Type: mutation
Auth: Protected
Input:
{
countyId: number;
}Output:
{
success: boolean;
}Get user's query history (consolidated from former user.queries).
Type: query
Auth: Protected
Input:
{
limit?: number; // 1-100, default 50
}Output:
Array<{
id: number;
queryText: string;
responseText: string;
protocolRefs: string[];
createdAt: Date;
countyId: number;
}>Get user's saved counties (multi-county access).
Type: query
Auth: Protected
Input: None
Output:
{
counties: Array<County>;
canAdd: boolean;
currentCount: number;
maxAllowed: number;
tier: "free" | "pro" | "enterprise";
}Add a county to user's saved counties.
Type: mutation
Auth: Protected
Input:
{
countyId: number;
isPrimary?: boolean; // Default false
}Output:
{
success: boolean;
error?: string;
}Errors:
BAD_REQUEST: County limit reached or invalid county
Remove a county from user's saved counties.
Type: mutation
Auth: Protected
Input:
{
countyId: number;
}Output:
{
success: boolean;
}Set a saved county as the user's primary county.
Type: mutation
Auth: Protected
Input:
{
countyId: number;
}Output:
{
success: boolean;
}Get user's primary county.
Type: query
Auth: Protected
Input: None
Output:
County | nullSemantic search across protocols using Voyage AI embeddings and pgvector.
Type: query
Auth: Public
Features:
- Query normalization (EMS abbreviations, typos)
- Redis caching (1 hour TTL)
- Multi-query fusion for complex queries
- Advanced re-ranking
- Latency monitoring
Input:
{
query: string; // 1-500 chars
countyId?: number; // Filter by county
limit?: number; // 1-50, default 10
stateFilter?: string; // Two-letter state code
}Output:
{
results: Array<{
id: number;
protocolNumber: string;
protocolTitle: string;
section: string | null;
content: string; // Truncated to 500 chars
fullContent: string; // Complete content
relevanceScore: number; // 0-1 similarity score
countyId: number;
sourcePdfUrl: null;
protocolEffectiveDate: null;
lastVerifiedAt: null;
protocolYear: null;
}>;
totalFound: number;
query: string; // Original query
normalizedQuery: string; // Normalized query
fromCache: boolean;
latencyMs: number;
}Example:
const results = await trpc.search.semantic.query({
query: "cardiac arrest epi dose",
countyId: 1,
limit: 10
});Cache Headers:
X-Cache-Hit: true|falseCache-Control: public, max-age=3600
Get a specific protocol by ID.
Type: query
Auth: Public
Input:
{
id: number;
}Output:
ProtocolChunk | nullGet protocol statistics.
Type: query
Auth: Public
Input: None
Output:
{
totalProtocols: number;
totalAgencies: number;
lastUpdated: Date;
}Get protocol coverage by state.
Type: query
Auth: Public
Input: None
Output:
Array<{
state: string;
countyCount: number;
protocolCount: number;
}>Get total protocol statistics.
Type: query
Auth: Public
Input: None
Output:
{
totalProtocols: number;
totalAgencies: number;
totalStates: number;
}Get agencies (counties) by state with protocol counts.
Type: query
Auth: Public
Input:
{
state: string; // Two-letter state code
}Output:
Array<{
id: number;
name: string;
state: string;
protocolCount: number;
}>Get all agencies with protocols, optionally filtered by state.
Type: query
Auth: Public
Input:
{
state?: string; // Optional state filter
}Output:
Array<{
id: number;
name: string;
state: string;
protocolCount: number;
}>Search protocols within a specific agency using Voyage AI and pgvector.
Type: query
Auth: Public
Input:
{
query: string; // 1-500 chars
agencyId: number; // MySQL county ID (auto-mapped to Supabase)
limit?: number; // 1-50, default 10
}Output: Same as search.semantic
Submit a protocol query with Claude RAG.
Type: mutation
Auth: Protected
Features:
- Query normalization
- Intelligent Claude model routing (Haiku/Sonnet based on complexity)
- Usage limit enforcement
- Optimized search with re-ranking
Input:
{
countyId: number;
queryText: string; // 1-1000 chars
}Output:
{
success: boolean;
error: string | null;
response: {
text: string; // Claude's response
protocolRefs: string[]; // Referenced protocols
model: string; // Claude model used
tokens: {
input: number;
output: number;
};
responseTimeMs: number;
normalizedQuery: string; // Normalized query
queryIntent: string; // Detected intent
isComplexQuery: boolean;
} | null;
}Example:
const result = await trpc.query.submit.mutate({
countyId: 1,
queryText: "What's the epinephrine dose for cardiac arrest in adults?"
});Errors:
- Daily query limit reached (free tier: 10/day)
- No matching protocols found
Get user's query history.
Type: query
Auth: Protected
Input:
{
limit?: number; // 1-100, default 50
}Output:
Array<Query>Get user's search history for cloud sync (Pro feature).
Type: query
Auth: Protected
Input:
{
limit?: number; // 1-100, default 50
}Output:
Array<SearchHistoryEntry>Sync local search history to cloud (Pro feature).
Type: mutation
Auth: Protected (Pro/Enterprise only)
Input:
{
localQueries: Array<{
queryText: string; // 1-500 chars
countyId?: number;
timestamp: string | Date;
deviceId?: string; // Max 64 chars
}>;
}Output:
{
success: boolean;
merged: number; // Number of entries merged
serverHistory: Array<SearchHistoryEntry>;
}Errors:
FORBIDDEN: Requires Pro subscription
Clear user's search history.
Type: mutation
Auth: Protected
Input: None
Output:
{
success: boolean;
}Delete a single history entry.
Type: mutation
Auth: Protected
Input:
{
entryId: number;
}Output:
{
success: boolean;
}Errors:
NOT_FOUND: Entry not found
Transcribe audio to text using OpenAI Whisper.
Type: mutation
Auth: Protected
Input:
{
audioUrl: string; // Must be from authorized storage domain
language?: string; // Optional language hint
}Output:
{
success: boolean;
error: string | null;
text: string | null;
}Allowed URLs:
https://storage.protocol-guide.com/https://*.supabase.co/storage/https://*.r2.cloudflarestorage.com/
Example:
const result = await trpc.voice.transcribe.mutate({
audioUrl: "https://storage.protocol-guide.com/voice/123/audio.webm",
language: "en"
});Upload audio file for transcription.
Type: mutation
Auth: Protected
Input:
{
audioBase64: string; // Max 10MB
mimeType: string; // e.g., "audio/webm"
}Output:
{
url: string; // Storage URL for uploaded audio
}Example:
const { url } = await trpc.voice.uploadAudio.mutate({
audioBase64: base64EncodedAudio,
mimeType: "audio/webm"
});Submit user feedback.
Type: mutation
Auth: Protected
Input:
{
category: "error" | "suggestion" | "general";
subject: string; // 1-255 chars
message: string; // Min 1 char
protocolRef?: string; // Max 255 chars
}Output:
{
success: boolean;
error: string | null;
}Example:
await trpc.feedback.submit.mutate({
category: "suggestion",
subject: "Add dark mode",
message: "Would love a dark mode option for night shifts"
});Get user's submitted feedback.
Type: query
Auth: Protected
Input: None
Output:
Array<{
id: number;
category: string;
subject: string;
message: string;
status: "pending" | "reviewed" | "resolved" | "dismissed";
createdAt: Date;
adminNotes?: string;
}>Submit public contact form.
Type: mutation
Auth: Public
Input:
{
name: string; // 1-255 chars
email: string; // Valid email, max 320 chars
message: string; // 10-5000 chars
}Output:
{
success: boolean;
error: string | null;
}Example:
await trpc.contact.submit.mutate({
name: "John Doe",
email: "john@example.com",
message: "I'd like to request protocols for our county..."
});Create Stripe checkout session for individual subscription.
Type: mutation
Auth: Protected
Input:
{
plan: "monthly" | "annual";
successUrl: string; // Valid URL
cancelUrl: string; // Valid URL
}Output:
{
success: boolean;
error: string | null;
url: string | null; // Stripe checkout URL
}Example:
const { url } = await trpc.subscription.createCheckout.mutate({
plan: "monthly",
successUrl: "https://app.protocolguide.app/success",
cancelUrl: "https://app.protocolguide.app/pricing"
});
// Redirect user to urlCreate Stripe customer portal session for managing subscription.
Type: mutation
Auth: Protected
Input:
{
returnUrl: string; // Valid URL
}Output:
{
success: boolean;
error: string | null;
url: string | null; // Customer portal URL
}Errors:
- No subscription found (user has no Stripe customer ID)
Get current subscription status.
Type: query
Auth: Protected
Input: None
Output:
{
tier: "free" | "pro" | "enterprise";
subscriptionStatus: string | null;
subscriptionEndDate: Date | null;
}Create Stripe checkout session for department/agency subscription (B2B).
Type: mutation
Auth: Protected (Agency Admin only)
Input:
{
agencyId: number;
tier: "starter" | "professional" | "enterprise";
seatCount: number; // 1-1000
interval: "monthly" | "annual";
successUrl: string;
cancelUrl: string;
}Output:
{
success: boolean;
error: string | null;
url: string | null;
}Errors:
- Not authorized to manage this agency
- Agency not found
All admin procedures require adminProcedure authentication (admin role).
List all feedback with optional filters.
Type: query
Auth: Admin
Input:
{
status?: "pending" | "reviewed" | "resolved" | "dismissed";
limit?: number; // 1-100, default 50
offset?: number; // Default 0
}Output:
{
feedback: Array<Feedback>;
total: number;
}Update feedback status and add admin notes.
Type: mutation
Auth: Admin
Input:
{
feedbackId: number;
status: "pending" | "reviewed" | "resolved" | "dismissed";
adminNotes?: string;
}Output:
{
success: boolean;
}Side Effects: Creates audit log entry
List all users with optional filters.
Type: query
Auth: Admin
Input:
{
tier?: "free" | "pro" | "enterprise";
role?: "user" | "admin";
limit?: number; // 1-100, default 50
offset?: number; // Default 0
}Output:
{
users: Array<User>;
total: number;
}Update a user's role.
Type: mutation
Auth: Admin
Input:
{
userId: number;
role: "user" | "admin";
}Output:
{
success: boolean;
}Errors:
BAD_REQUEST: Cannot change your own roleNOT_FOUND: User not found
Side Effects: Creates audit log entry
List contact form submissions.
Type: query
Auth: Admin
Input:
{
status?: "pending" | "reviewed" | "resolved";
limit?: number; // 1-100, default 50
offset?: number; // Default 0
}Output:
{
submissions: Array<ContactSubmission>;
total: number;
}Update contact submission status.
Type: mutation
Auth: Admin
Input:
{
submissionId: number;
status: "pending" | "reviewed" | "resolved";
}Output:
{
success: boolean;
}Side Effects: Creates audit log entry
Get audit logs (admin actions).
Type: query
Auth: Admin
Input:
{
limit?: number; // 1-100, default 50
offset?: number; // Default 0
}Output:
{
logs: Array<{
id: number;
userId: number;
action: string;
targetType: string;
targetId: string;
details: object;
createdAt: Date;
}>;
total: number;
}All agency admin procedures require agencyAdminProcedure authentication (agency admin access).
Get current user's agencies.
Type: query
Auth: Protected
Input: None
Output:
Array<{
id: number;
name: string;
role: "owner" | "admin" | "protocol_author" | "member";
// ... agency fields
}>Get agency details.
Type: query
Auth: Protected
Input:
{
agencyId: number;
}Output:
AgencyErrors:
NOT_FOUND: Agency not found
Update agency settings.
Type: mutation
Auth: Agency Admin
Input:
{
agencyId: number;
name?: string; // 1-255 chars
contactEmail?: string; // Valid email, max 320
contactPhone?: string; // Max 20 chars
address?: string; // Max 500 chars
settings?: {
brandColor?: string;
allowSelfRegistration?: boolean;
requireEmailVerification?: boolean;
protocolApprovalRequired?: boolean;
};
}Output:
{
success: boolean;
}List agency members.
Type: query
Auth: Agency Admin
Input:
{
agencyId: number;
}Output:
Array<{
id: number;
userId: number;
role: "owner" | "admin" | "protocol_author" | "member";
user: {
id: number;
name: string;
email: string;
} | null;
joinedAt: Date;
}>Invite member to agency.
Type: mutation
Auth: Agency Admin
Input:
{
agencyId: number;
email: string;
role?: "admin" | "protocol_author" | "member"; // Default "member"
}Output:
{
success: boolean;
token: string; // Invitation token
}Side Effects: Creates invitation record (expires in 7 days)
Update member role.
Type: mutation
Auth: Agency Admin
Input:
{
agencyId: number;
memberId: number;
role: "admin" | "protocol_author" | "member";
}Output:
{
success: boolean;
}Errors:
NOT_FOUND: Member not foundFORBIDDEN: Cannot change owner role or your own role
Remove member from agency.
Type: mutation
Auth: Agency Admin
Input:
{
agencyId: number;
memberId: number;
}Output:
{
success: boolean;
}Errors:
NOT_FOUND: Member not foundFORBIDDEN: Cannot remove owner or yourself
List agency protocols.
Type: query
Auth: Agency Admin
Input:
{
agencyId: number;
status?: "draft" | "review" | "approved" | "published" | "archived";
limit?: number; // 1-100, default 50
offset?: number; // Default 0
}Output:
{
protocols: Array<ProtocolVersion>;
total: number;
}Upload new protocol PDF.
Type: mutation
Auth: Agency Admin
Input:
{
agencyId: number;
fileName: string; // Max 255 chars
fileBase64: string; // Max 20MB
mimeType?: string; // Default "application/pdf"
protocolNumber: string; // Max 50 chars
title: string; // Max 255 chars
version?: string; // Max 20 chars, default "1.0"
effectiveDate?: string; // ISO date string
}Output:
{
success: boolean;
uploadId: number;
versionId: number;
fileUrl: string;
}Errors:
BAD_REQUEST: Only PDF files supported, file too large
Get protocol upload processing status.
Type: query
Auth: Agency Admin
Input:
{
agencyId: number;
uploadId: number;
}Output:
{
id: number;
status: "pending" | "processing" | "completed" | "failed";
progress?: number;
error?: string;
// ... upload fields
}Errors:
NOT_FOUND: Upload not found
Update protocol status (workflow).
Type: mutation
Auth: Agency Admin
Input:
{
agencyId: number;
versionId: number;
status: "draft" | "review" | "approved" | "published" | "archived";
}Output:
{
success: boolean;
}Valid Transitions:
- draft → review, archived
- review → draft, approved, archived
- approved → published, draft
- published → archived
- archived → draft
Errors:
BAD_REQUEST: Invalid status transitionNOT_FOUND: Protocol version not found
Publish protocol (makes it live in search).
Type: mutation
Auth: Agency Admin
Input:
{
agencyId: number;
versionId: number;
}Output:
{
success: boolean;
}Errors:
BAD_REQUEST: Protocol must be approved before publishingNOT_FOUND: Protocol version not found
Side Effects: Creates audit log entry
Archive protocol.
Type: mutation
Auth: Agency Admin
Input:
{
agencyId: number;
versionId: number;
}Output:
{
success: boolean;
}Side Effects: Creates audit log entry
List protocol versions.
Type: query
Auth: Agency Admin
Input:
{
agencyId: number;
protocolNumber: string;
}Output:
Array<ProtocolVersion>Create new version from existing protocol.
Type: mutation
Auth: Agency Admin
Input:
{
agencyId: number;
fromVersionId: number;
newVersion: string; // Max 20 chars
changes?: string; // Change log description
}Output:
{
success: boolean;
versionId: number;
}Errors:
NOT_FOUND: Source version not found
Handles integration partner tracking (ImageTrend, ESO, Zoll, etc.).
HIPAA Compliance: This router intentionally does NOT log or store any PHI (Protected Health Information).
Log an integration access event.
Type: mutation
Auth: Public
Input:
{
partner: "imagetrend" | "esos" | "zoll" | "emscloud";
agencyId?: string; // Max 100 chars
agencyName?: string; // Max 255 chars
searchTerm?: string; // Max 500 chars
userAge?: number; // IGNORED - not stored (HIPAA)
impression?: string; // IGNORED - not stored (HIPAA)
responseTimeMs?: number;
resultCount?: number;
}Output:
{
success: boolean;
logged: boolean;
requestId: string;
}HIPAA Note: userAge and impression parameters are accepted for API compatibility but are NOT persisted to the database.
Get integration statistics (admin only).
Type: query
Auth: Admin
Input:
{
partner?: "imagetrend" | "esos" | "zoll" | "emscloud";
days?: number; // 1-365, default 30
}Output:
{
stats: Array<{
partner: string;
accessCount: number;
uniqueAgencies: number;
avgResponseTimeMs: number | null;
}>;
total: number;
periodDays: number;
}Get recent integration access logs (admin only).
Type: query
Auth: Admin
Input:
{
partner?: "imagetrend" | "esos" | "zoll" | "emscloud";
limit?: number; // 1-100, default 50
offset?: number; // Default 0
}Output:
{
logs: Array<IntegrationLog>;
total: number;
}Get daily integration usage for charts (admin only).
Type: query
Auth: Admin
Input:
{
partner?: "imagetrend" | "esos" | "zoll" | "emscloud";
days?: number; // 1-90, default 30
}Output:
{
dailyUsage: Array<{
date: string;
partner: string;
count: number;
}>;
}Viral referral system with gamification.
Get or create user's referral code.
Type: query
Auth: Protected
Input: None
Output:
{
code: string;
usesCount: number;
createdAt: Date;
}Get user's referral statistics.
Type: query
Auth: Protected
Input: None
Output:
{
totalReferrals: number;
successfulReferrals: number;
pendingReferrals: number;
proDaysEarned: number;
creditsEarned: number;
currentTier: "bronze" | "silver" | "gold" | "platinum" | "ambassador";
rank: number | null;
nextTierProgress: number; // 0-100
nextTierName: string | null;
referralsToNextTier: number;
}Get referral history (who I've referred).
Type: query
Auth: Protected
Input:
{
limit?: number; // 1-50, default 20
offset?: number; // Default 0
}Output:
{
referrals: Array<{
id: number;
redeemedAt: Date;
convertedToPaid: boolean;
conversionDate: Date | null;
reward: object;
referredUser: {
name: string;
email: string; // Masked for privacy
};
}>;
total: number;
}Validate a referral code (public - for signup flow).
Type: query
Auth: Public
Input:
{
code: string; // 1-20 chars
}Output:
{
valid: boolean;
error?: string;
referrerName?: string;
benefits?: {
trialDays: number;
description: string;
};
}Redeem a referral code (called during signup).
Type: mutation
Auth: Protected
Input:
{
code: string; // 1-20 chars
}Output:
{
success: boolean;
benefit: string;
}Errors:
NOT_FOUND: Invalid or expired referral codeBAD_REQUEST: Cannot use your own code or already redeemed
Get share message templates.
Type: query
Auth: Protected
Input: None
Output:
{
sms: string;
whatsapp: string;
email: {
subject: string;
body: string;
};
generic: string;
shareUrl: string;
code: string;
}Get top referrers leaderboard.
Type: query
Auth: Protected
Input:
{
limit?: number; // 1-100, default 25
timeframe?: "all_time" | "this_month" | "this_week"; // Default "all_time"
}Output:
{
leaderboard: Array<{
rank: number;
userId: number;
userName: string;
totalReferrals: number;
successfulReferrals: number;
tier: string;
}>;
timeframe: string;
}Track a viral event (share, view, etc.).
Type: mutation
Auth: Protected
Input:
{
eventType:
| "referral_code_generated"
| "referral_code_shared"
| "referral_code_copied"
| "share_button_tapped"
| "shift_share_shown"
| "shift_share_accepted"
| "shift_share_dismissed"
| "social_share_completed";
metadata?: {
shareMethod?: "sms" | "whatsapp" | "email" | "copy" | "qr";
referralCode?: string;
platform?: string;
};
}Output:
{
tracked: boolean;
}Note: Fails silently (returns tracked: false) to not break user experience.
tRPC uses standard HTTP-like error codes:
| Code | HTTP | Description |
|---|---|---|
UNAUTHORIZED |
401 | Authentication required or invalid |
FORBIDDEN |
403 | User lacks required permissions |
NOT_FOUND |
404 | Resource not found |
BAD_REQUEST |
400 | Invalid input or validation error |
TOO_MANY_REQUESTS |
429 | Rate limit exceeded |
INTERNAL_SERVER_ERROR |
500 | Server error |
{
error: {
code: string; // Error code
message: string; // Human-readable message
data?: {
code: string;
httpStatus: number;
path: string;
stack?: string; // Only in development
};
};
}try {
const result = await trpc.query.submit.mutate({
countyId: 1,
queryText: "test"
});
} catch (error) {
if (error.data?.code === "TOO_MANY_REQUESTS") {
// Show upgrade prompt
} else if (error.data?.code === "UNAUTHORIZED") {
// Redirect to login
} else {
// Show generic error
}
}| Tier | Daily Queries | Counties |
|---|---|---|
| Free | 10 | 1 |
| Pro | Unlimited | 5 |
| Enterprise | Unlimited | Unlimited |
For procedures with rate limiting, the response includes:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 5
X-RateLimit-Reset: 1234567890
const usage = await trpc.user.usage.query();
console.log(`${usage.count}/${usage.limit} queries used today`);
if (usage.count >= usage.limit) {
// Show upgrade prompt
}Full TypeScript type definitions are available in:
server/_core/trpc.ts- Core tRPC setupserver/_core/context.ts- Request contextdrizzle/schema.ts- Database schema types
import { trpc } from "./utils/trpc";
// Query
const counties = await trpc.counties.list.query();
// Mutation
const result = await trpc.query.submit.mutate({
countyId: 1,
queryText: "cardiac arrest protocol"
});tRPC subscriptions are not currently implemented but can be added for real-time features.
For API support, contact: support@protocolguide.app
Documentation last updated: 2026-01-23