# How to Add a Health Symptom Checker to Your App — Copy-Paste Prompts Included

- Tool: App Features
- Last updated: July 2026

## TL;DR

An AI health symptom checker needs four pieces: a multi-turn conversational UI, a Claude claude-sonnet-4-5 Edge Function with a strict triage-only system prompt, a disclaimer gate users must accept before first use, and an emergency banner that appears when triage_level is 'emergency'. With Lovable you can ship a working build in 1–2 days. Costs run $25–60/month at 1,000 users — Claude API at roughly $0.03 per session plus Supabase Pro.

## What a Health Symptom Checker Actually Is

A health symptom checker is an AI-powered conversational tool that asks users structured questions about how they feel and returns triage guidance — not a diagnosis. The key product distinction is triage versus diagnosis: the feature must tell users whether to seek emergency care, schedule a doctor visit, or manage symptoms at home. It must never state what disease someone has. Claude claude-sonnet-4-5 handles the multi-turn conversation and classification well, but only if the system prompt enforces strict output rules. The real engineering challenges are: enforcing the disclaimer before first use at the database level (not just frontend state), persisting full conversation context across stateless Edge Function calls, and rendering an emergency banner immediately when triage_level is 'emergency' regardless of how the user reached that screen.

## Anatomy of the Feature

Seven components. The disclaimer gate and the structured JSON output from Claude are the two pieces that determine whether your build is legally defensible and functionally correct.

- **Symptom Input Chat** (ui): A multi-turn conversational UI built with shadcn/ui Chat components (web) or a Flutter ListView of message bubbles. Each user message is appended to the local state and sent to the Triage Engine Edge Function along with the full conversation history from symptoms_sessions.
- **Triage Engine** (backend): A Supabase Edge Function calling Claude claude-sonnet-4-5 via the @anthropic-ai/sdk package. The system prompt instructs Claude to: (1) never state a definitive diagnosis, (2) classify every response into one of four triage levels (self-care, appointment, urgent, emergency), (3) escalate immediately to 'emergency' for chest pain, shortness of breath, stroke symptoms, or suicidal ideation, and (4) return structured JSON: {triage_level, summary, recommendations, follow_up_question}.
- **Body Part Selector** (ui): An SVG body diagram (react-body-highlighter on web or a custom Flutter SVG widget) that lets users tap an affected region to pre-fill symptom context. This reduces free-text entry errors and gives the AI a more structured starting point than 'I feel bad'.
- **Symptom History Log** (data): Two Supabase tables: symptoms_sessions stores the full conversation per session with triage outcome; symptoms_entries stores individual symptom data points (what hurts, how long, severity). RLS ensures users only access their own rows.
- **Disclaimer Gate** (ui): A modal shown on first app open with the disclaimer text and a checkbox. The acceptance timestamp is stored in Supabase user_metadata (auth.users metadata column). The Triage Engine Edge Function checks this field before processing any message, so bypassing the modal by navigating directly to the chat URL is blocked at the API level.
- **Emergency Alert Banner** (ui): A full-width red banner rendered conditionally when the Claude response JSON contains triage_level: 'emergency'. Shows '911' prominently, poison control number (1-800-222-1222 in the US), and nearest emergency room locator link. The banner persists until the user manually dismisses it or starts a new session.
- **PDF Export** (backend): A Supabase Edge Function using pdf-lib (Deno-compatible, open-source) to generate a session summary PDF: symptom timeline, triage outcome, AI recommendations. User downloads it to share with their doctor at an appointment.

## Data model

Two tables cover the full session and individual symptom tracking, with RLS ensuring strict per-user data isolation. Run this in the Supabase SQL editor:

```sql
create table public.symptoms_sessions (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references auth.users(id) on delete cascade not null,
  messages jsonb not null default '[]',
  triage_level text check (triage_level in ('self-care', 'appointment', 'urgent', 'emergency')),
  disclaimer_accepted_at timestamptz,
  created_at timestamptz not null default now()
);

create table public.symptoms_entries (
  id uuid primary key default gen_random_uuid(),
  session_id uuid references public.symptoms_sessions(id) on delete cascade not null,
  symptom text not null,
  duration text,
  severity int check (severity between 1 and 10),
  created_at timestamptz not null default now()
);

alter table public.symptoms_sessions enable row level security;
alter table public.symptoms_entries enable row level security;

create policy "Users see own sessions"
  on public.symptoms_sessions for select
  using (auth.uid() = user_id);

create policy "Users insert own sessions"
  on public.symptoms_sessions for insert
  with check (auth.uid() = user_id);

create policy "Users update own sessions"
  on public.symptoms_sessions for update
  using (auth.uid() = user_id);

create policy "Users see own entries"
  on public.symptoms_entries for select
  using (
    exists (
      select 1 from public.symptoms_sessions s
      where s.id = session_id and s.user_id = auth.uid()
    )
  );

create policy "Users insert own entries"
  on public.symptoms_entries for insert
  with check (
    exists (
      select 1 from public.symptoms_sessions s
      where s.id = session_id and s.user_id = auth.uid()
    )
  );

create index symptoms_sessions_user_idx
  on public.symptoms_sessions (user_id, created_at desc);
```

The messages JSONB column stores the full Anthropic conversation format array. The Edge Function reads this on every turn and appends it to the Claude API call so multi-turn context is preserved across stateless function invocations.

## Build paths

### Lovable — fit 4/10, 1–2 days

Lovable's native Supabase integration, Edge Functions, and the Claude AI Connector in the Cloud tab make the full stack viable in one project. Expect several follow-up prompts to get triage logic and disclaimer enforcement correct.

1. Create a new Lovable project and connect Lovable Cloud; go to Cloud tab → AI and add the Anthropic Claude connector with your API key stored in Secrets
2. Run the data model SQL in Cloud tab → Database → SQL Editor to create the symptoms_sessions and symptoms_entries tables with RLS
3. Paste the prompt below and let Agent Mode build the disclaimer gate, chat UI, Triage Engine Edge Function, and emergency banner
4. Test the triage logic by typing 'I have chest pain and difficulty breathing' — verify the emergency banner appears and 911 is prominently displayed
5. Publish and verify the disclaimer gate cannot be bypassed by navigating directly to the chat URL while the Edge Function checks disclaimer_accepted_at

Starter prompt:

```
Build a health symptom checker app with these components: 1) A disclaimer modal on first app open. It should show text explaining this is not medical advice, include a checkbox 'I understand this is not medical advice and will seek professional help for serious symptoms', and a Continue button. When accepted, store the timestamp in Supabase auth user metadata under disclaimer_accepted_at. Block access to the chat until accepted. 2) A multi-turn chat interface with shadcn/ui. Each user message appends to state and calls a Supabase Edge Function named 'triage-engine'. 3) The triage-engine Edge Function must: load the full messages array from symptoms_sessions for this session, append the new user message, call Claude claude-sonnet-4-5 via @anthropic-ai/sdk with this system prompt: 'You are a medical triage assistant. NEVER state a definitive diagnosis. Use language like could be consistent with or symptoms suggest. Always classify your response into exactly one triage level: self-care, appointment, urgent, or emergency. Return valid JSON with fields: {triage_level: string, summary: string, recommendations: string[], follow_up_question: string}. For chest pain, stroke symptoms (face drooping, arm weakness, speech difficulty), difficulty breathing, or suicidal ideation, always return triage_level: emergency.', save the updated messages array back to symptoms_sessions, and return the JSON response to the client. 4) Check disclaimer_accepted_at in user metadata at Edge Function start; return 403 if not set. 5) Render an emergency alert banner (red background, white text) showing Call 911 immediately whenever triage_level is emergency. Parse triage_level from the JSON response, do not detect it from prose text. 6) Show a session history list on a separate screen grouped by date.
```

Limitations:

- Lovable's 70% problem is likely to surface on nuanced triage logic — the emergency escalation for complex symptom combinations and the disclaimer bypass prevention may need 2–3 follow-up prompts to get right
- Medical disclaimer UX requires careful review; Lovable generates functional UI but the exact legal language and checkbox copy should be reviewed by a lawyer before publishing
- Lovable does not auto-provision HIPAA-compliant infrastructure; for apps targeting US users with PHI, custom development and a BAA with your cloud provider are required

### V0 — fit 3/10, 1–2 days

V0 generates a clean Next.js chat UI with shadcn/ui that serves as an excellent frontend layer. API routes handle Claude calls server-side. Best if you already have a Next.js app to integrate into.

1. Paste the prompt below into V0 to generate the chat UI, disclaimer modal, emergency banner, and session history components
2. Add your Supabase and Anthropic environment variables in the Vercel Dashboard (NEXT_PUBLIC_SUPABASE_URL, SUPABASE_SERVICE_KEY, ANTHROPIC_API_KEY)
3. Run the data model SQL in the Supabase SQL editor and verify RLS policies are active
4. Wire the Claude API route: create app/api/triage/route.ts that loads the session's messages from Supabase, calls claude-sonnet-4-5, saves the updated messages, and returns the triage JSON
5. Test the disclaimer bypass prevention: try fetching the triage API route without a valid session to confirm the API returns 403

Starter prompt:

```
Build a Next.js health symptom checker with these components: 1) A DisclaimerModal component shown before first chat access. Checkbox and Continue button. On accept, call a /api/accept-disclaimer server action that stores disclaimer_accepted_at in Supabase user metadata. 2) A SymptomChat client component with a scrollable message list and text input. On submit, POST to /api/triage with {session_id, message}. Show a loading spinner while waiting. 3) The /api/triage route (POST, server-side): check Supabase user metadata for disclaimer_accepted_at and return 403 if missing; load messages from symptoms_sessions; call Claude claude-sonnet-4-5 with @anthropic-ai/sdk using a system prompt that enforces triage-only output (no diagnoses), classifies into self-care/appointment/urgent/emergency, and returns structured JSON {triage_level, summary, recommendations, follow_up_question}; save updated messages; return JSON response. 4) An EmergencyBanner component rendered when triage_level === emergency — red background, 'Call 911' in bold, poison control 1-800-222-1222, dismiss button. 5) A SessionHistory page listing past sessions by date with triage level badge. Use Supabase for all storage with RLS. Use server components for data fetching, client components only for interactive chat.
```

Limitations:

- V0 does not auto-provision Supabase or Neon; you must manually connect a database and configure all environment variables in Vercel Dashboard
- Medical session persistence in Next.js requires careful server-side session handling — V0's first pass may use localStorage for session state which exposes data client-side and breaks SSR
- V0 preview sandbox may render the disclaimer modal but cannot test actual Claude API calls; deploy to Vercel to verify the full triage flow

### Flutterflow — fit 2/10, 2–3 days

FlutterFlow can render the chat UI and call a REST API backend. The body diagram and complex triage state management require substantial custom Dart code, making this the slowest path.

1. Build the disclaimer modal in FlutterFlow using an Alert Dialog component; add a CheckboxListTile for the acceptance checkbox and wire a Custom Action to store disclaimer_accepted_at via Supabase API
2. Create a Page State variable 'messages' (list of JSON objects) and a Custom Action 'SendMessage' that POSTs to your Supabase Edge Function triage-engine with the full messages array
3. Render the message list using a ListView with a Conditional widget that shows different bubble styles for user vs assistant messages
4. Add a Conditional Widget at the top of the chat page that shows the emergency banner (red Container with 911 text) when the page state variable triage_level equals 'emergency'
5. In Settings → Permissions, add microphone usage description if you plan to add voice symptom input later; not required for text-only

Limitations:

- Multi-turn conversation state management is complex in FlutterFlow's visual action system — you need custom Dart code to maintain the full messages array and update it correctly on each turn
- The body SVG diagram requires a custom Flutter widget; FlutterFlow's image widgets cannot handle interactive SVG tap regions without code
- CORS errors from FlutterFlow calling the Supabase Edge Function are common — ensure your Edge Function returns Access-Control-Allow-Origin: * and handles OPTIONS preflight

### Custom — fit 5/10, 2–3 weeks

Required for HIPAA-compliant US health apps, ICD-10 symptom taxonomy integration, or audit logging for clinical review. Full control over every compliance and data governance requirement.

1. Set up HIPAA-aligned infrastructure: enable Supabase encryption at rest, configure a Business Associate Agreement (BAA) with your cloud provider, enable audit logging for all PHI access
2. Integrate a clinical symptom taxonomy: map free-text symptoms to ICD-10 codes using a structured pre-processing step before the Claude call; this allows structured clinical output alongside the triage recommendation
3. Implement audit logging: every Claude API call, every triage result, and every disclaimer acceptance is written to an immutable audit_log table with service_role; no UPDATE or DELETE policy on audit rows
4. Build multi-language support: detect user locale, pass it to Claude's system prompt, and route to language-appropriate emergency numbers (not 911 for non-US users)

Limitations:

- HIPAA compliance requires legal review and cannot be achieved through infrastructure alone — content policies, access controls, breach notification procedures, and training are all required
- ICD-10 taxonomy integration adds significant complexity and requires a medical content specialist to validate the symptom-to-code mapping accuracy

## Gotchas

- **Claude names a specific disease, creating legal liability** — Without a carefully engineered system prompt, Claude's default behavior is to be helpful and specific — which in a health context means stating 'this sounds like appendicitis' or 'you may have a UTI'. This is legally dangerous for a non-clinical application and can mislead users into self-treating when they need care. Fix: Add explicit system prompt instructions: 'Never state a definitive diagnosis. Use only language like could be consistent with, symptoms may suggest, or commonly associated with. Always recommend professional evaluation.' Test by asking about common conditions — if Claude names them directly, tighten the constraint language.
- **Users bypass the disclaimer gate by navigating directly to the chat URL** — If the disclaimer check only lives in React state or localStorage, any user who navigates directly to /chat or clears their browser data skips it entirely. This is a legal liability, not just a UX issue — in many jurisdictions, you must demonstrate that users were informed of the non-medical nature of the tool before using it. Fix: Store disclaimer_accepted_at in Supabase user_metadata via auth.updateUser(). In the Triage Engine Edge Function, check this field before processing any message. If it's null or missing, return a 403 with a redirect to the disclaimer page. The check lives server-side and cannot be bypassed by frontend manipulation.
- **Multi-turn conversation loses context after 5+ messages** — Each call to a Supabase Edge Function starts with a clean execution context — there is no server-side conversation memory between calls. If the Edge Function only receives the current message rather than the full history, Claude answers each turn as if it's the first message, producing incoherent follow-up questions. Fix: On every Edge Function call, load the full messages JSONB array from symptoms_sessions for the current session, append the new user message, pass the complete array as the messages parameter to the Anthropic SDK call, then save the updated array (with Claude's response appended) back to the row.
- **Emergency banner never appears even when user describes chest pain** — If Claude returns its triage level embedded in prose text ('Based on your symptoms, I would classify this as an emergency...') rather than in a structured JSON field, the frontend cannot reliably parse it. Pattern matching on the word 'emergency' in prose text is fragile and misses edge cases. Fix: Instruct Claude via the system prompt to always return valid JSON with a triage_level field as the first key. Parse the JSON response in the Edge Function and include triage_level as a top-level field in the API response. The frontend renders the banner by checking response.triage_level === 'emergency' — no text parsing.
- **CORS error when calling Edge Function from FlutterFlow** — Supabase Edge Functions require explicit CORS headers for cross-origin requests. A FlutterFlow app making HTTP requests to a custom Edge Function will fail with a CORS error if the function doesn't return Access-Control-Allow-Origin and doesn't handle OPTIONS preflight requests. Fix: In your Deno Edge Function, return these headers on every response: Access-Control-Allow-Origin: *, Access-Control-Allow-Headers: authorization, x-client-info, apikey, content-type. Add an explicit handler that returns 200 for OPTIONS method requests before any other logic.

## Best practices

- Always enforce the disclaimer at the Edge Function level, not just frontend state — store acceptance timestamp in Supabase user_metadata and verify it server-side
- Return structured JSON from Claude with an explicit triage_level field; never parse triage severity from prose text
- Pass the full conversation history (messages JSONB array) to Claude on every Edge Function call — the function is stateless and needs the full context for coherent multi-turn responses
- Test the emergency escalation path explicitly: type 'I have chest pain, left arm pain, and shortness of breath' and verify the red 911 banner renders before building anything else
- Include a clear 'This is not a substitute for professional medical advice' disclaimer in the UI at all times, not only in the gate modal
- Log every triage session (anonymized) to review output quality — Claude's triage accuracy should be audited against known symptom patterns before you grow your user base
- Add a session cooldown or rate limit (max 3 sessions per user per 24 hours) to prevent abuse and control Claude API costs

## Frequently asked questions

### Is a symptom checker app legal to build?

Yes, with the right guardrails. Apps that provide triage guidance (should I see a doctor?) are generally legal as long as they do not diagnose specific conditions and include clear disclaimers. Apps that cross into providing clinical diagnosis or treatment recommendations may be regulated as medical devices in the US (FDA) or EU (MDR). Consult a health tech lawyer before launching if your app targets patients or healthcare providers.

### Do I need a medical license to publish this?

Not for a general wellness or triage guidance app that clearly states it is not medical advice. Medical licenses are required for apps that provide clinical decision support to licensed practitioners, act as prescription tools, or make specific diagnostic claims. The safeguard is consistent disclaimer language, structured triage output (not diagnosis), and no specific disease naming.

### How do I prevent Claude from giving a specific diagnosis?

Engineer the system prompt with explicit negative constraints: 'Never state a definitive diagnosis. Never say the user has [condition]. Use only language such as symptoms could be consistent with or may suggest. Always recommend professional evaluation.' Test it by describing symptoms of common conditions like appendicitis or UTI — if Claude names them directly, tighten the language. Include an output validator in your Edge Function that checks for diagnostic phrasing before returning to the client.

### Can users save their symptom history?

Yes. The symptoms_sessions table stores the full conversation per session with triage outcome and timestamp. Users can view past sessions in a history screen grouped by date, see how their triage levels have changed over time, and download session summaries as PDF using the pdf-lib Edge Function for sharing with their doctor.

### What happens when someone describes an emergency?

The Claude system prompt instructs it to return triage_level: 'emergency' for chest pain, stroke symptoms (face drooping, arm weakness, speech difficulty), severe difficulty breathing, and suicidal ideation. The Edge Function surfaces this field in the API response, and the frontend renders a full-screen red banner with '911' prominently displayed and poison control (1-800-222-1222 in the US). The banner persists until dismissed.

### How much does the Claude API cost per session?

A typical symptom checker session of 5–8 message turns uses approximately 2,000 tokens total at $3 per 1M input tokens and $15 per 1M output tokens for claude-sonnet-4-5. That works out to roughly $0.03 per session. At 1,000 sessions per month you're spending about $30 on Claude API — affordable at that scale.

### Does this work without an account (guest mode)?

It can, with limitations. Guest mode removes session history persistence (no Supabase auth to scope rows to) and makes disclaimer enforcement harder since you're relying on localStorage instead of user_metadata. If you want guest mode, store a session token in localStorage and scope the symptoms_sessions rows to that token, but accept that users who clear their browser lose all history and the disclaimer check is less reliable.

### How do I make this HIPAA compliant?

HIPAA compliance requires a Business Associate Agreement (BAA) with every service provider that handles PHI — Supabase, your email provider, any third-party analytics. Supabase offers BAAs on their Team plan ($599/mo). Beyond infrastructure, you need access audit logs, encryption at rest and in transit, breach notification procedures, and staff training. HIPAA is a compliance program, not just a technical configuration. Engage a health tech lawyer and a compliance consultant before targeting US clinical users.

---

Source: https://www.rapidevelopers.com/app-features/health-symptom-checker
© RapidDev — https://www.rapidevelopers.com/app-features/health-symptom-checker
