Last Updated: 2026-02-07
Database System: PostgreSQL 17 (Supabase)
ORM: Drizzle ORM
Source of Truth: /drizzle/schema.ts
- Overview
- EMS Jurisdiction Data Model
- Tables
- Enums
- Relationships
- Indexes
- Row Level Security (RLS)
- Database Functions
- Triggers
- Vector/Embedding Storage
Protocol Guide uses a PostgreSQL database hosted on Supabase to store the national EMS protocol library — 58,000+ protocol chunks from 2,738 agencies across 53 states and territories, with vector embeddings for jurisdiction-scoped semantic search.
- Primary Database: Supabase PostgreSQL 17
- Schema Management: Drizzle ORM migrations
- Vector Search: pgvector extension with 1536-dimension embeddings (Gemini Embedding 2 Preview
gemini-embedding-2-preview; Voyage AI removed 2026-03-24) - Security: Row Level Security (RLS) policies for data isolation and PHI exposure reduction
- Full-Text Search: PostgreSQL tsvector with GIN indexes
The database models the U.S. EMS jurisdiction hierarchy. Understanding this is essential — every search query is scoped through this chain:
User selects County → county_agency_mapping → agency_id → manus_protocol_chunks
┌──────────────────────┐ ┌──────────────────────────┐
│ manus_agencies │ │ manus_protocol_chunks │
│ (LEMSAs, state EMS │ │ (58,000+ vector-embedded │
│ offices, regional │ │ protocol segments) │
│ authorities) │ ├──────────────────────────┤
├──────────────────────┤ │ agency_id (FK) ──────────┤──► scopes search
│ id (PK) │◄──────│ agency_name │
│ name │ │ state_code │
│ state_code │ │ protocol_number │
│ state_name │ │ protocol_title │
│ protocol_count │ │ section │
│ integration_partner │ │ content │
└──────────────────────┘ │ embedding (vector 1536) │
▲ │ protocol_year │
│ │ protocol_effective_date │
┌───────┴──────────────┐ └──────────────────────────┘
│ county_agency_mapping│
│ (bridges user county │
│ selection to the │
│ correct LEMSA) │
├──────────────────────┤
│ county_id (FK) │
│ agency_id (FK) │ ◄── One LEMSA may cover multiple counties
│ agency_name │ (e.g., Central CA EMS → Fresno, Kings,
│ state_code │ Madera, Tulare counties)
└──────────────────────┘
│
▼
┌──────────────────────┐
│ counties │
│ (2,713 U.S. counties)│
├──────────────────────┤
│ id (PK) │
│ name │
│ state │
│ uses_state_protocols │ ◄── TRUE = county uses statewide protocols
│ protocol_version │ (not regional LEMSA protocols)
└──────────────────────┘
EMS protocols are not national. Each LEMSA (Local Emergency Medical Services Agency) independently writes and publishes clinical protocols for its jurisdiction. A paramedic in Los Angeles County follows different medication dosages, standing orders, and treatment algorithms than one in San Diego County.
The county_agency_mapping table exists because:
- Users think in terms of "I work in Fresno County" (county)
- Protocols are organized by LEMSA/agency (not county)
- One LEMSA often covers multiple counties
- The mapping resolves this mismatch: county_id → agency_id → scoped protocol search
| From | To | Relationship | Purpose |
|---|---|---|---|
county_agency_mapping.county_id |
counties.id |
Many-to-1 | User's county selection |
county_agency_mapping.agency_id |
manus_agencies.id |
Many-to-1 | Maps county to protocol authority |
manus_protocol_chunks.agency_id |
manus_agencies.id |
Many-to-1 | Scopes protocols to their authority |
manus_users.selected_agency_id |
manus_agencies.id |
Many-to-1 | User's persistent jurisdiction preference |
┌───────────────────────────────────────────────────────────────────┐
│ Supabase PostgreSQL │
├───────────────────────────────────────────────────────────────────┤
│ Jurisdiction Tables │ User Tables │ Vector Search │
│ ────────────────── │ ──────────── │ ───────────── │
│ manus_agencies (2,738) │ manus_users │ manus_protocol_ │
│ counties (2,713) │ agencies │ chunks (58,000+) │
│ county_agency_mapping │ agency_members │ pgvector extension │
│ │ subscriptions │ search_manus_ │
│ Analytics Tables │ queries │ protocols (RPC) │
│ ────────────────── │ bookmarks │ │
│ analytics_events │ feedback │ │
│ search_analytics │ │ │
│ content_gaps │ │ │
└───────────────────────────────────────────────────────────────────┘
EMS provider accounts. Each user has a selected_agency_id linking them to their LEMSA/agency jurisdiction. Maps to users export in schema.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
auth_id |
uuid | YES | - | Supabase auth.users.id |
manus_open_id |
text | YES | - | Legacy Manus OAuth ID |
name |
text | YES | - | Display name |
email |
text | YES | - | Email address |
login_method |
text | YES | - | OAuth provider used |
role |
text | YES | 'user' | User role (user/admin) |
created_at |
timestamptz | YES | now() | Account creation |
updated_at |
timestamptz | YES | now() | Last update |
last_signed_in |
timestamptz | YES | - | Last login timestamp |
tier |
text | YES | 'free' | Subscription tier |
query_count_today |
integer | YES | 0 | Daily query count |
last_query_date |
date | YES | - | Date of last query |
selected_agency_id |
integer | YES | - | Currently selected agency |
stripe_customer_id |
text | YES | - | Stripe customer ID |
subscription_id |
text | YES | - | Stripe subscription ID |
subscription_status |
text | YES | - | active/canceled/etc |
subscription_end_date |
timestamptz | YES | - | Subscription expiry |
EMS agencies and organizations (B2B accounts for agency-level features). Distinct from manus_agencies which is the national registry of protocol-issuing authorities used for search scoping.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
name |
varchar(255) | NO | - | Agency name |
slug |
varchar(100) | NO | - | URL-safe identifier |
state_code |
varchar(2) | NO | - | Two-letter state code |
state |
varchar(2) | YES | - | State code (duplicate) |
county |
varchar(100) | YES | - | County name |
agency_type |
agency_type | YES | - | Type of agency |
logo_url |
varchar(500) | YES | - | Logo URL |
contact_email |
varchar(320) | YES | - | Contact email |
contact_phone |
varchar(20) | YES | - | Contact phone |
address |
text | YES | - | Physical address |
supabase_agency_id |
integer | YES | - | Legacy mapping ID |
stripe_customer_id |
varchar(255) | YES | - | Stripe customer ID |
subscription_tier |
subscription_tier | YES | 'starter' | Subscription level |
subscription_status |
varchar(50) | YES | - | Subscription status |
settings |
json | YES | - | Agency settings JSON |
created_at |
timestamp | NO | now() | Creation timestamp |
updated_at |
timestamp | NO | now() | Last update |
Indexes:
idx_agencies_slugon (slug)idx_agencies_stateon (state)idx_agencies_state_codeon (state_code)
Agency membership and roles.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
agency_id |
integer | NO | - | FK to agencies.id |
user_id |
integer | NO | - | FK to manus_users.id |
role |
member_role | NO | 'member' | Member role |
invited_by |
integer | YES | - | FK to inviting user |
invited_at |
timestamp | YES | - | Invitation timestamp |
accepted_at |
timestamp | YES | - | Acceptance timestamp |
status |
member_status | YES | 'pending' | Membership status |
created_at |
timestamp | NO | now() | Creation timestamp |
Indexes:
idx_agency_members_agencyon (agency_id)idx_agency_members_useron (user_id)
Legacy protocol storage (county-based).
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
county_id |
integer | NO | - | FK to counties.id |
protocol_number |
varchar(50) | NO | - | Protocol identifier |
protocol_title |
varchar(255) | NO | - | Protocol title |
section |
varchar(255) | YES | - | Category/section |
content |
text | NO | - | Protocol text content |
source_pdf_url |
varchar(500) | YES | - | Source PDF URL |
created_at |
timestamp | NO | now() | Creation timestamp |
protocol_effective_date |
varchar(20) | YES | - | Effective date |
last_verified_at |
timestamp | YES | - | Last verification |
protocol_year |
integer | YES | - | Protocol year |
Indexes:
idx_protocols_countyon (county_id)idx_protocols_sectionon (section)idx_protocols_numberon (protocol_number)idx_protocols_yearon (protocol_year)
Geographic county reference data.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
name |
varchar(255) | NO | - | County name |
state |
varchar(64) | NO | - | State name |
uses_state_protocols |
boolean | NO | - | Uses state protocols |
protocol_version |
varchar(50) | YES | - | Protocol version |
created_at |
timestamp | NO | now() | Creation timestamp |
Indexes:
idx_counties_stateon (state)
User query history (AI-powered searches).
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
user_id |
integer | NO | - | FK to manus_users.id |
county_id |
integer | NO | - | FK to counties.id |
query_text |
text | NO | - | User's query |
response_text |
text | YES | - | AI response |
protocol_refs |
json | YES | - | Referenced protocols |
created_at |
timestamp | NO | now() | Query timestamp |
User-saved protocol bookmarks.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
user_id |
integer | NO | - | FK to manus_users.id |
protocol_number |
varchar(50) | NO | - | Protocol identifier |
protocol_title |
varchar(255) | NO | - | Protocol title |
section |
varchar(255) | YES | - | Section bookmarked |
content |
text | NO | - | Bookmarked content |
agency_id |
integer | YES | - | FK to agencies.id |
agency_name |
varchar(255) | YES | - | Agency name |
created_at |
timestamp | NO | now() | Bookmark timestamp |
User feedback and error reports.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
user_id |
integer | NO | - | FK to manus_users.id |
category |
feedback_category | NO | - | Feedback type |
protocol_ref |
varchar(255) | YES | - | Related protocol |
county_id |
integer | YES | - | FK to counties.id |
subject |
varchar(255) | NO | - | Feedback subject |
message |
text | NO | - | Feedback message |
status |
feedback_status | NO | 'pending' | Review status |
admin_notes |
text | YES | - | Admin notes |
created_at |
timestamp | NO | now() | Submission time |
updated_at |
timestamp | NO | now() | Last update |
Contact form submissions.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
name |
varchar(255) | NO | - | Submitter name |
email |
varchar(320) | NO | - | Contact email |
message |
text | NO | - | Message content |
status |
contact_status | NO | 'pending' | Review status |
created_at |
timestamp | NO | now() | Submission time |
Audit trail designed for PHI-minimizing, HIPAA-aligned operations.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
user_id |
integer | YES | - | FK to manus_users.id |
action |
varchar(50) | NO | - | Action performed |
entity_type |
varchar(50) | YES | - | Entity type affected |
entity_id |
varchar(100) | YES | - | Entity ID affected |
metadata |
json | YES | - | Additional data |
ip_address |
varchar(45) | YES | - | Client IP |
user_agent |
text | YES | - | Client user agent |
created_at |
timestamp | NO | now() | Action timestamp |
Indexes:
idx_audit_logs_useron (user_id)idx_audit_logs_actionon (action)idx_audit_logs_createdon (created_at)
Partner integration analytics with patient-age/impression fields omitted and a HIPAA-aligned safeguard posture.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
partner |
integration_partner | NO | - | Partner name |
agency_id |
varchar(100) | YES | - | Partner agency ID |
agency_name |
varchar(255) | YES | - | Partner agency name |
search_term |
varchar(500) | YES | - | Search query |
response_time_ms |
integer | YES | - | Response latency |
result_count |
integer | YES | - | Results returned |
ip_address |
varchar(45) | YES | - | Client IP |
user_agent |
varchar(500) | YES | - | Client user agent |
created_at |
timestamp | NO | now() | Log timestamp |
Note: PHI fields (userAge, impression) were intentionally removed to reduce PHI exposure.
Indexes:
idx_integration_logs_partneron (partner)idx_integration_logs_created_aton (created_at)idx_integration_logs_agency_idon (agency_id)
OAuth provider connections.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
user_id |
integer | NO | - | FK to manus_users.id |
provider |
varchar(50) | NO | - | OAuth provider name |
provider_user_id |
varchar(255) | NO | - | Provider's user ID |
access_token |
text | YES | - | OAuth access token |
refresh_token |
text | YES | - | OAuth refresh token |
expires_at |
timestamp | YES | - | Token expiry |
created_at |
timestamp | NO | now() | Link creation |
updated_at |
timestamp | NO | now() | Last update |
Indexes:
idx_auth_providers_useron (user_id)idx_auth_providers_provideron (provider, provider_user_id)
User-county associations.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
user_id |
integer | NO | - | FK to manus_users.id |
county_id |
integer | NO | - | FK to counties.id |
is_primary |
boolean | YES | false | Primary county flag |
created_at |
timestamp | NO | now() | Association created |
Indexes:
idx_user_counties_useron (user_id)idx_user_counties_countyon (county_id)
User state subscriptions.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
user_id |
integer | NO | - | FK to manus_users.id |
state_code |
varchar(2) | NO | - | Two-letter state code |
access_level |
access_level | YES | 'view' | Access permissions |
subscribed_at |
timestamp | YES | now() | Subscription start |
expires_at |
timestamp | YES | - | Subscription expiry |
Indexes:
idx_user_states_useron (user_id)idx_user_states_stateon (state_code)
User-agency subscriptions.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
user_id |
integer | NO | - | FK to manus_users.id |
agency_id |
integer | NO | - | FK to agencies.id |
access_level |
access_level | YES | 'view' | Access permissions |
is_primary |
boolean | YES | false | Primary agency flag |
role |
varchar(100) | YES | - | Role in agency |
verified_at |
timestamp | YES | - | Verification date |
subscribed_at |
timestamp | YES | now() | Subscription start |
expires_at |
timestamp | YES | - | Subscription expiry |
Indexes:
idx_user_agencies_useron (user_id)idx_user_agencies_agencyon (agency_id)
User search history for cloud sync.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
user_id |
integer | NO | - | FK to manus_users.id |
county_id |
integer | YES | - | FK to counties.id |
search_query |
text | NO | - | Search query text |
results_count |
integer | YES | - | Number of results |
created_at |
timestamp | NO | now() | Search timestamp |
Indexes:
idx_search_history_useron (user_id)idx_search_history_createdon (created_at)
Protocol version control for agencies.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
agency_id |
integer | NO | - | FK to agencies.id |
protocol_number |
varchar(50) | NO | - | Protocol identifier |
title |
varchar(255) | NO | - | Protocol title |
version |
varchar(20) | NO | - | Version string |
status |
protocol_status | NO | 'draft' | Publication status |
source_file_url |
varchar(500) | YES | - | Source file URL |
effective_date |
timestamp | YES | - | Effective date |
expires_date |
timestamp | YES | - | Expiration date |
approved_by |
integer | YES | - | FK to approving user |
approved_at |
timestamp | YES | - | Approval timestamp |
published_at |
timestamp | YES | - | Publication time |
published_by |
integer | YES | - | FK to publishing user |
chunks_generated |
integer | YES | 0 | Number of chunks |
metadata |
json | YES | - | Additional metadata |
change_log |
text | YES | - | Version changes |
created_at |
timestamp | NO | now() | Creation time |
created_by |
integer | NO | - | FK to creating user |
updated_at |
timestamp | NO | now() | Last update |
Indexes:
idx_protocol_versions_agencyon (agency_id)idx_protocol_versions_statuson (status)
Protocol file upload tracking.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
agency_id |
integer | NO | - | FK to agencies.id |
user_id |
integer | NO | - | FK to uploading user |
file_name |
varchar(255) | NO | - | Original filename |
file_url |
varchar(500) | NO | - | Storage URL |
file_size |
integer | YES | - | File size in bytes |
mime_type |
varchar(100) | YES | - | MIME type |
status |
upload_status | YES | 'pending' | Processing status |
progress |
integer | YES | 0 | Processing progress % |
chunks_created |
integer | YES | 0 | Chunks generated |
error_message |
text | YES | - | Error details |
processing_started_at |
timestamp | YES | - | Processing start |
completed_at |
timestamp | YES | - | Completion time |
created_at |
timestamp | NO | now() | Upload time |
Indexes:
idx_protocol_uploads_agencyon (agency_id)idx_protocol_uploads_useron (user_id)
Stripe payment webhook processing.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
event_id |
varchar(255) | NO | - | Stripe event ID |
event_type |
varchar(100) | NO | - | Event type |
payload |
json | YES | - | Event payload |
processed |
boolean | YES | false | Processing status |
processed_at |
timestamp | YES | - | Processing time |
error |
text | YES | - | Error message |
created_at |
timestamp | NO | now() | Receipt time |
Indexes:
idx_stripe_events_idon (event_id)idx_stripe_events_typeon (event_type)idx_stripe_events_processedon (processed)
Push notification tokens.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
user_id |
integer | NO | - | FK to manus_users.id |
token |
text | NO | - | Push token |
platform |
varchar(20) | YES | - | ios/android/web |
created_at |
timestamp | NO | now() | Registration time |
last_used_at |
timestamp | NO | now() | Last used |
Indexes:
push_tokens_user_idxon (user_id)
Email campaign tracking.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
user_id |
integer | NO | - | FK to manus_users.id |
email_type |
varchar(50) | NO | - | Email template type |
sent_at |
timestamp | NO | now() | Send timestamp |
Indexes:
drip_emails_user_idxon (user_id)drip_emails_type_idxon (email_type)
Pre-launch email capture.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
serial | NO | auto | Primary key |
email |
varchar(320) | NO | - | Email address |
source |
varchar(100) | YES | 'landing_page' | Signup source |
created_at |
timestamp | NO | now() | Signup time |
Indexes:
waitlist_signups_email_idxon (email)waitlist_signups_created_idxon (created_at)
See /drizzle/analytics-schema.ts for full definitions.
Generic event tracking.
| Column | Type | Description |
|---|---|---|
id |
serial | Primary key |
user_id |
integer | Optional user reference |
session_id |
varchar(64) | Session identifier |
event_type |
varchar(50) | search/protocol/user/conversion |
event_name |
varchar(100) | Specific event name |
properties |
json | Event properties |
device_type |
varchar(20) | ios/android/web/pwa |
timestamp |
timestamp | Event time |
Detailed search behavior tracking.
| Column | Type | Description |
|---|---|---|
id |
serial | Primary key |
user_id |
integer | Optional user reference |
session_id |
varchar(64) | Session identifier |
query_text |
varchar(500) | Search query |
state_filter |
varchar(2) | State filter applied |
results_count |
integer | Number of results |
selected_result_rank |
integer | Which result clicked |
search_method |
varchar(20) | text/voice/example_click |
timestamp |
timestamp | Search time |
Protocol viewing analytics.
| Column | Type | Description |
|---|---|---|
id |
serial | Primary key |
user_id |
integer | Optional user reference |
protocol_chunk_id |
integer | FK to protocol chunk |
access_source |
varchar(50) | search/history/bookmark/deep_link |
time_spent_seconds |
integer | View duration |
scroll_depth |
real | 0-1 scroll percentage |
timestamp |
timestamp | Access time |
Session-level usage tracking.
| Column | Type | Description |
|---|---|---|
id |
serial | Primary key |
user_id |
integer | Optional user reference |
session_id |
varchar(64) | Unique session ID |
device_type |
varchar(20) | Device type |
start_time |
timestamp | Session start |
end_time |
timestamp | Session end |
search_count |
integer | Searches in session |
protocols_viewed |
integer | Protocols viewed |
Pre-aggregated daily metrics for dashboards.
User retention cohort analysis.
Zero-result searches for content improvement.
Subscription conversion funnel tracking.
Aggregated feature usage statistics.
These tables exist only in Supabase and are not in the Drizzle schema.
The search corpus — 58,000+ vector-embedded protocol segments from LEMSAs and EMS agencies nationwide. Each row is a semantically bounded chunk (400-1800 chars) of a clinical protocol, tagged with its jurisdiction (agency_id + state_code) and embedded via Gemini Embedding 2 Preview for cosine similarity search (Voyage AI removed 2026-03-24).
This table is the core of the RAG pipeline. Every search query generates an embedding, then searches this table filtered by agency_id (from the user's county→agency mapping) to return jurisdiction-specific protocol content.
| Column | Type | Description |
|---|---|---|
id |
integer | Primary key |
agency_id |
integer | FK to manus_agencies — scopes chunk to its LEMSA/agency |
agency_name |
text | Denormalized agency name (e.g., "LA County EMS Agency") |
state_code |
char(2) | Denormalized state code (e.g., "CA") — enables state-level filtering |
protocol_number |
text | LEMSA-assigned protocol identifier (e.g., "R-001", "TP-4.2") |
protocol_title |
text | Clinical protocol name (e.g., "Cardiac Arrest - Adult") |
section |
text | Protocol category: Cardiac, Respiratory, Trauma, Medical, Pediatric |
content |
text | Protocol text chunk (400-1800 chars, 150 char overlap with adjacent chunks) |
embedding |
vector(1536) | Gemini Embedding 2 Preview gemini-embedding-2-preview embedding for cosine similarity search (Voyage AI removed 2026-03-24) |
source_pdf_url |
text | Original PDF source URL from LEMSA website |
protocol_year |
text | Protocol version year (e.g., "2024") |
protocol_effective_date |
date | When protocol became clinically active |
has_images |
boolean | Whether source protocol contains clinical images |
image_urls |
jsonb | Array of extracted image URLs |
content_type |
text | Chunk classification: medication, procedure, assessment, general |
search_vector |
tsvector | Full-text search vector (BM25 fallback) |
embedding_version |
text | Embedding model version for migration tracking |
created_at |
timestamptz | Ingestion timestamp |
last_verified_at |
timestamptz | Last verification against source PDF |
Indexes (jurisdiction-optimized):
idx_manus_chunks_embedding_hnsw— HNSW vector index for cosine similarity (primary search path)idx_manus_chunks_agency_id— Agency filtering (used in every jurisdiction-scoped search)idx_manus_chunks_state_code— State-level filtering (used when searching all agencies in a state)idx_manus_chunks_state_agency— Composite state + agency (most selective filter)idx_manus_chunks_protocol_number— Direct protocol number lookupidx_manus_chunks_search_vector— GIN full-text search (BM25 fallback when Gemini embeddings unavailable)
National registry of protocol-issuing authorities (LEMSAs, state EMS offices, regional councils). This is the primary jurisdiction table — 2,738 agencies across 53 states/territories. Every protocol chunk references an agency_id from this table, and every search query is scoped by it.
In California, each row represents a LEMSA (Local Emergency Medical Services Agency). In other states, rows may represent state EMS offices, regional medical direction councils, or fire department-based agencies.
| Column | Type | Description |
|---|---|---|
id |
integer | Primary key — the agency_id referenced throughout the system |
name |
text | Agency/LEMSA name (e.g., "Los Angeles County EMS Agency") |
state_code |
char(2) | Two-letter state code (e.g., "CA", "TX") |
state_name |
text | Full state name |
protocol_count |
integer | Number of protocol chunks ingested for this agency |
parent_protocol_source_id |
integer | FK for protocol inheritance (agencies sharing base protocols) |
agency_type |
agency_type_enum | Classification: fire_dept, ems_agency, hospital, state_office, regional_council |
call_volume_tier |
call_volume_tier_enum | high/mid/low — used for ingestion prioritization |
is_verified |
boolean | Whether protocol data has been verified against source |
integration_partner |
integration_partner_enum | ePCR vendor: imagetrend, esos, zoll, emscloud |
-- Contact/Feedback Status
CREATE TYPE contact_status AS ENUM ('pending', 'reviewed', 'resolved');
CREATE TYPE feedback_category AS ENUM ('error', 'suggestion', 'general');
CREATE TYPE feedback_status AS ENUM ('pending', 'reviewed', 'resolved', 'dismissed');
-- Integration Partners
CREATE TYPE integration_partner AS ENUM ('imagetrend', 'esos', 'zoll', 'emscloud', 'none');
-- User Roles & Tiers
CREATE TYPE user_role AS ENUM ('user', 'admin');
CREATE TYPE user_tier AS ENUM ('free', 'pro', 'enterprise');
-- Agency Types
CREATE TYPE agency_type AS ENUM ('fire_dept', 'ems_agency', 'hospital', 'state_office', 'regional_council');
CREATE TYPE subscription_tier AS ENUM ('starter', 'professional', 'enterprise');
-- Membership
CREATE TYPE member_role AS ENUM ('owner', 'admin', 'protocol_author', 'member');
CREATE TYPE member_status AS ENUM ('pending', 'active', 'suspended');
-- Protocol Status
CREATE TYPE protocol_status AS ENUM ('draft', 'review', 'approved', 'published', 'archived');
CREATE TYPE upload_status AS ENUM ('pending', 'processing', 'chunking', 'embedding', 'completed', 'failed');
-- Access Levels
CREATE TYPE access_level AS ENUM ('view', 'contribute', 'admin');--- JURISDICTION CHAIN (critical path for search) ---
counties (1) ───< (many) county_agency_mapping ────> (1) manus_agencies
manus_agencies (1) ───< (many) manus_protocol_chunks
manus_agencies (1) ───< (many) manus_agencies (self-ref: parent_protocol_source_id)
--- USER RELATIONSHIPS ---
manus_users (1) ────> (1) manus_agencies (selected_agency_id — jurisdiction preference)
manus_users (1) ───< (many) queries
manus_users (1) ───< (many) bookmarks
manus_users (1) ───< (many) feedback
manus_users (1) ───< (many) search_history
manus_users (1) ───< (many) user_counties ────> (1) counties
manus_users (1) ───< (many) user_states
manus_users (1) ───< (many) user_agencies ────> (1) agencies
manus_users (1) ───< (many) user_auth_providers
manus_users (1) ───< (many) agency_members ────> (1) agencies
manus_users (1) ───< (many) push_tokens
manus_users (1) ───< (many) analytics_events
--- B2B AGENCY MANAGEMENT ---
agencies (1) ───< (many) agency_members
agencies (1) ───< (many) protocol_versions
agencies (1) ───< (many) protocol_uploads
agencies (1) ───< (many) user_agencies
--- COUNTY RELATIONSHIPS ---
counties (1) ───< (many) county_agency_mapping
counties (1) ───< (many) protocol_chunks
counties (1) ───< (many) queries
counties (1) ───< (many) user_counties
counties (1) ───< (many) search_history
| Table | Index | Type | Purpose |
|---|---|---|---|
| manus_protocol_chunks | idx_manus_chunks_embedding_hnsw | HNSW | Vector similarity search |
| manus_protocol_chunks | idx_manus_chunks_search_vector | GIN | Full-text search |
| manus_protocol_chunks | idx_manus_chunks_state_agency | B-tree | State + agency filtering |
| manus_users | idx_users_auth_id | B-tree | Auth lookup |
| agencies | idx_agencies_slug | B-tree | URL routing |
| audit_logs | idx_audit_logs_created | B-tree | Time-based queries |
| Table | Index Count |
|---|---|
| manus_protocol_chunks | 12+ |
| manus_users | 5+ |
| agencies | 3 |
| agency_members | 2 |
| audit_logs | 3 |
| search_history | 2 |
-- Get current user's internal ID from auth.uid()
CREATE FUNCTION get_current_user_id() RETURNS INTEGER;
-- Check if current user is admin
CREATE FUNCTION is_admin() RETURNS BOOLEAN;
-- Check if user is member of agency
CREATE FUNCTION is_agency_member(agency_id_param INTEGER) RETURNS BOOLEAN;
-- Check if user is agency admin/owner
CREATE FUNCTION is_agency_admin(agency_id_param INTEGER) RETURNS BOOLEAN;| Table | Public Read | User Self-Access | Admin Access | Service Role |
|---|---|---|---|---|
| manus_users | No | Yes | Yes | Full |
| agencies | Yes | - | Yes | Full |
| agency_members | No | Yes (own) | Yes | Full |
| queries | No | Yes | Yes | Full |
| bookmarks | No | Yes | - | Full |
| search_history | No | Yes | - | Full |
| feedback | No | Yes | Yes | Full |
| audit_logs | No | No | Read-only | Full |
| counties | Yes | - | Yes | Full |
| protocol_chunks | Yes | - | - | Full |
| contact_submissions | No | No | Yes | Full |
| integration_logs | No | No | Read-only | Full |
| stripe_webhook_events | No | No | No | Full |
| push_tokens | No | Yes | - | Full |
| drip_emails_sent | No | Read-only | - | Full |
- User Isolation: Users can only access their own data
- Agency Scoping: Agency members access agency data per role
- Admin Elevation: Admins have elevated access where needed
- Service Role: Backend has full access for operations
- Public Safety: Medical protocols remain publicly accessible
- PHI Safeguards: No unauthorized PHI access
Semantic protocol search with vector embeddings.
CREATE FUNCTION search_manus_protocols(
query_embedding vector(1536),
agency_filter integer DEFAULT NULL,
state_filter text DEFAULT NULL,
match_count integer DEFAULT 10,
match_threshold float DEFAULT 0.3,
agency_name_filter text DEFAULT NULL,
state_code_filter char(2) DEFAULT NULL
) RETURNS TABLE (
id integer,
agency_id integer,
protocol_number text,
protocol_title text,
section text,
content text,
image_urls text[],
similarity float
);Hybrid search combining vector similarity with full-text keyword relevance.
CREATE FUNCTION search_manus_protocols_fts(
query_text TEXT,
query_embedding vector(1536),
agency_filter INTEGER DEFAULT NULL,
state_code_filter TEXT DEFAULT NULL,
match_count INTEGER DEFAULT 10,
match_threshold FLOAT DEFAULT 0.3
) RETURNS TABLE (
id INTEGER,
agency_id INTEGER,
protocol_number TEXT,
protocol_title TEXT,
section TEXT,
content TEXT,
image_urls TEXT[],
similarity FLOAT,
fts_rank FLOAT
);Returns protocol inheritance hierarchy: Agency → Regional → State.
CREATE FUNCTION get_protocol_inheritance_chain(agency_id_param INTEGER)
RETURNS TABLE (
level INTEGER,
id INTEGER,
name TEXT,
agency_type agency_type_enum,
state_code CHAR(2)
);Auto-updates updated_at column on row changes.
CREATE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';Applied to tables with updated_at columns:
| Table | Trigger Name |
|---|---|
| manus_users | update_users_updated_at |
| feedback | update_feedback_updated_at |
| agencies | update_agencies_updated_at |
| user_auth_providers | update_user_auth_providers_updated_at |
| protocol_versions | update_protocol_versions_updated_at |
Auto-updates search_vector on manus_protocol_chunks:
CREATE TRIGGER manus_chunks_search_update
BEFORE INSERT OR UPDATE OF protocol_title, section, content
ON manus_protocol_chunks
FOR EACH ROW
EXECUTE FUNCTION manus_chunks_search_vector_trigger();Keeps manus_agencies.protocol_count in sync:
CREATE TRIGGER trg_update_agency_protocol_count
AFTER INSERT OR UPDATE OR DELETE ON manus_protocol_chunks
FOR EACH ROW
EXECUTE FUNCTION update_agency_protocol_count();- Model: Gemini Embedding 2 Preview
gemini-embedding-2-preview(Voyage AI removed 2026-03-24) - Dimensions: 1536
- Similarity Metric: Cosine distance
-- HNSW index for fast approximate nearest neighbor search
CREATE INDEX idx_manus_chunks_embedding_hnsw
ON manus_protocol_chunks
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);Index Parameters:
m = 16: Number of bi-directional links (default, balanced)ef_construction = 64: Higher = better index quality, slower buildvector_cosine_ops: Optimized for cosine similarity
- Dataset Size: ~58K+ protocol chunks
- Search Latency: <100ms for top-10 results
- Index Size: ~200MB
- Accuracy: 95%+ recall with HNSW
The system uses a hybrid search strategy:
- Vector Similarity (70% weight): Semantic understanding of query intent
- Full-Text Search (30% weight): Keyword matching for precise terms
-- Combined score formula
ORDER BY (vector_similarity * 0.7) + (fts_rank * 0.3) DESC-- Weighted tsvector: title (A) > section (B) > content (C)
search_vector =
setweight(to_tsvector('english', protocol_title), 'A') ||
setweight(to_tsvector('english', section), 'B') ||
setweight(to_tsvector('english', content), 'C')Key migration files in /drizzle/migrations/:
| Migration | Description |
|---|---|
| 0025_add_notification_tables.sql | Push tokens, drip emails |
| 0026_postgresql_updated_at_triggers.sql | Auto-update triggers |
| 0027_add_row_level_security_policies.sql | Comprehensive RLS |
| 0030_optimize_manus_protocol_chunks.sql | Vector & FTS indexes |
| 0032_add_waitlist_signups.sql | Waitlist table |
- DATABASE-ARCHITECTURE-ANALYSIS.md - Architecture overview
- supabase-schema-audit.md - Supabase-specific audit
- INDEX_ANALYSIS.md - Index optimization analysis
- SCHEMA_RELATIONSHIPS_DIAGRAM.md - Visual diagrams