CareCall System Understanding
Version 1 · 10 questions · active
Evaluate whether an engineering method understands CareCall's architecture, security, configuration and operational workflows after independently examining the repository.
Instructions given to participants
Answer each question based on evidence found in the supplied system/repository.
For each answer:
- Answer the question directly.
- Explain how the system implements the behavior.
- Cite repository evidence using file:line references where possible.
- Distinguish significant claims as:
- CONFIRMED
- INFERRED
- UNKNOWN
- Identify gaps, ambiguities or implementation concerns.
- Do not assume functionality exists merely because it would normally be expected in an application of this type.
- Do not use another participant's answers or artifacts.
Repository implementation evidence takes precedence over assumptions, documentation claims or expected product behavior.
01 Multi-client / Tenant Isolation
Does CareCall support multiple client organizations? If so, how are clients configured, and what mechanisms prevent one client from viewing or modifying another client's patients, campaigns, configuration or results?
Claude Code CONFIRMED
CareCall's tenant unit is the "clinic," not a separate client/organization concept layered on top -- administration and RLS are scoped by clinic_id. admin-manage's create_user/reset_password/deactivate_user actions enforce clinic-admin-limited-to-staff/own-clinic checks in code (admin-manage L113-204, requireTargetInScope L72-L84). Roles and clinic_id live only in auth.users.app_metadata, written server-side. Data-layer isolation is implemented via Postgres RLS driven by helper functions such as jwt_clinic_id() referenced in the portal-roles migration, but the full RLS policy matrix was not line-by-line re-audited in this pass.
CONFIRMED that clinic scoping exists and is enforced at the admin-action layer; INFERRED (not fully re-verified) that every table's RLS policy actually enforces clinic isolation on every read/write path.
Gap: start-campaign itself performs no clinic or role check at all -- it only requires a Bearer token be present. Whether this is by design (cron uses a service-role Bearer) or a real gap is UNKNOWN from the repository evidence alone.
Evidence
supabase/functions/admin-manage/index.ts L113-204, L72-84; supabase/functions/start-campaign/index.ts L88-130; supabase/migrations/20260705000000_portal_roles.sql
Grok CONFIRMED
Yes -- multi-clinic (tenant) isolation is implemented.
How clients are configured: rows in the clinics table. Admins create clinics via admin-manage (create_clinic). Clinic-scoped tables (patients, campaigns, providers, call_logs, etc.) carry a required clinic_id.
How isolation works: user role and clinic_id live in auth.users.app_metadata (written only by admin-manage, not client-writable). SQL helpers jwt_role(), jwt_clinic_id(), is_admin(), is_clinic_admin() back Postgres RLS policies that scope SELECT/INSERT/UPDATE by is_admin() OR clinic_id = jwt_clinic_id() (with stricter write rules for clinic admins on some tables). Edge Functions that mutate admin data check role/clinic in application code (admin-manage). Dialer/tools use the service role (bypass RLS) but thread clinic_id from campaign/call context.
Limitation: platform admin can see all clinics -- cross-clinic isolation for non-admins is the intended guard. The VITE_MULTI_CLINIC feature flag can hide the clinic switcher in the UI while the schema still supports multiple clinics.
Evidence
clinics table; admin-manage (create_clinic); SQL helpers jwt_role()/jwt_clinic_id()/is_admin()/is_clinic_admin(); RLS policies on clinic-scoped tables; VITE_MULTI_CLINIC flag
OpenAI CONFIRMED
CareCall has a multi-clinic data model that can represent multiple client organizations, with each clinic acting as a tenant. Each clinic has its own patients and clinicians; campaigns and campaign results; calling hours and timezone; callback number and SMS settings; appointment-type-to-Telnyx-assistant mappings; and users assigned through app_metadata.clinic_id.
Tenant identity and role are stored in the user's Supabase JWT metadata. PostgreSQL Row-Level Security generally restricts patients, campaigns, providers, appointments, call logs, audit records, and assistant mappings to the user's clinic. Platform Admins can access all clinics (portal_roles.sql:120).
Important limitations:
- The latest migration deliberately lets every authenticated user read every clinics row using using (true). This exposes clinic configuration across tenants, although not necessarily their patients or campaigns (single_clinic_read.sql:17).
- The UI is currently configured as a single-clinic build. The Admin clinic switcher is disabled unless VITE_MULTI_CLINIC is enabled (App.tsx:59).
- Edge Functions use the Supabase service-role key and therefore bypass RLS.
- start-campaign neither validates the bearer token nor checks tenant/role ownership. Anyone able to reach it can supply a campaign ID -- or request a global sweep -- with any bearer-prefixed value. This is a serious cross-tenant authorization weakness.
- Tenant isolation has not been verified through automated multi-tenant tests; no such tests were found.
Conclusion: the schema and most database policies support multiple tenants, but the current build and some privileged endpoints are not safely production-ready for strong multi-client isolation.
Evidence
supabase/migrations/20260705000000_portal_roles.sql:120; supabase/migrations/20260706140000_single_clinic_read.sql:17; web/src/App.tsx:59
02 PII / PHI Protection When Using AI
CareCall handles healthcare-related information. What controls exist to prevent PII or PHI from being unnecessarily sent to an AI model or external AI service? Identify where anonymization, filtering, minimization or other safeguards are implemented, and identify any gaps.
Claude Code CONFIRMED
Several concrete safeguards are implemented. In assistant-tools, verify_patient compares the caller-stated date of birth against the record server-side and never returns the real DOB to the model (L82-107). The Telnyx webhook's machine-answer path plays a scripted, PHI-free callback message rather than exposing patient details (L136-178). The public booking API's "context" action is intentionally PHI-minimal, and the generic {state: "expired"} response for invalid/expired tokens avoids giving an attacker an oracle to distinguish real patients from bad tokens.
Gaps: the artifacts don't establish a single, systemic PII/PHI minimization policy applied uniformly before every LLM/AI call -- the safeguards found are point checks embedded in specific handlers (verify_patient, machine-path message) rather than one enforced boundary. The AI Assistant's own prompt/instruction content (telnyx/assistant-instructions.md) was referenced as existing but its actual text controlling PHI handling in conversation was not reproduced or audited, so whether the assistant is instructed to avoid soliciting/repeating PHI beyond DOB verification is UNKNOWN from this evidence.
Classification: CONFIRMED for the specific mechanisms found; UNKNOWN for a systemic PHI-minimization policy across all AI-facing text.
Evidence
supabase/functions/assistant-tools/index.ts L82-107; supabase/functions/telnyx-call-events/index.ts L136-178; supabase/functions/booking-api/index.ts (context/verify actions)
Grok CONFIRMED
Controls present in code:
- DOB never sent to the model -- assistant-tools verify_patient: AI sends stated DOB; server compares and returns only match / locked / remaining_attempts.
- Dynamic variables to the AI are limited -- telnyx-call-events injects patient_first_name, campaign_context, clinic_name only, not DOB, full name, phone, or record IDs beyond call context.
- Progressive slot disclosure -- at most 3 days then 3 times, speech-ready strings, so the model should not invent times.
- Voicemail path is PHI-free -- fixed callback message + optional SMS; AI is not started on the machine-answer path.
- Booking link pre-verify minimization -- public booking-api context returns clinic name, appointment-type label, and state, not patient name/DOB, until verify succeeds (then first name only).
- Token at rest -- booking tokens stored as SHA-256 hashes.
Gaps:
- Transcripts/summaries from Telnyx Insights are stored on call_logs and shown in portal Call History -- those can contain conversational PHI after the call.
- Pre-call/voicemail SMS can include first name and clinic name (and optionally a booking link).
- Telnyx (and any recording/insights pipeline) is an external processor; BAAs are noted in the README as a production requirement, not enforced in code.
- Service-role Edge Functions can read full patient rows server-side (necessary for verify/book); the protection is "don't return PHI to the model," not "never load PHI on the server."
Evidence
assistant-tools verify_patient; telnyx-call-events (dynamic_variables); booking-api context action; booking token SHA-256 hashing
OpenAI CONFIRMED
There are several deliberate minimization controls:
- The AI is not given the patient's DOB on file. It receives the DOB stated by the patient, and CareCall compares it server-side. Only match/lockout status is returned (assistant-tools/index.ts:62).
- Slot and booking tools derive the patient and campaign from the call's server-side call_control_id; the AI does not choose a patient ID.
- Before verification, scheduling tools return 403.
- The prompt explicitly says not to reveal DOB, partial matches, medical details beyond the appointment reason, or scheduling information after verification failure (assistant-instructions.md:24).
- Voicemail messages are designed to be PHI-free.
- Public booking context exposes clinic name, appointment-type label, timezone, and link state -- not DOB or full patient identity.
- Booking-link tokens are stored only as SHA-256 hashes.
However, this is minimization rather than anonymization. Telnyx and its AI service still receive: patient phone number to place the call; patient first name; clinic name; free-text campaign context; the DOB spoken by the patient; and the conversation and potentially its transcript/insights.
Important gaps:
- There is no automated PHI redaction or filtering of campaign_context. A staff member could put sensitive clinical information into that free-text field, which is then injected into the AI prompt.
- There is no transcript or summary redaction before storage.
- No code-level data-loss-prevention classifier or minimum-necessary field validator was found.
- Stored audit entries can contain complete patient row JSON, duplicating PHI.
- Vendor BAA, retention, model-training, recording, and regional-processing controls are operational matters and are not enforced by this repository.
- The privacy restrictions in the prompt are partly behavioral instructions to a hosted AI, not deterministic enforcement.
Evidence
supabase/functions/assistant-tools/index.ts:62; telnyx/assistant-instructions.md:24
03 Campaign Reporting
What reporting or results functionality exists for completed or active campaigns? What campaign outcomes can a user review, and where does the underlying result data come from?
Claude Code UNKNOWN
This is not well covered by the five artifacts, which focus on the Edge Function API surface and explicitly decline to expand portal PostgREST access into synthetic REST operations. Capability 6 in the capability inventory states the staff portal reads/writes patients, campaigns, campaign_patients, appointments, call_logs, providers, clinics, and a review queue via the Supabase JS client under RLS, but no specific reporting screen, aggregation, or metric was inventoried -- table-level PostgREST access is explicitly called out as out of scope for inventing OpenAPI paths.
The underlying result data for any reporting view would come from call_logs, campaign_patients, and appointments, since those are the tables the Edge Functions write to during dialing (dialPatient + call_logs, hangup finalize/no_answer). But the actual reporting UI, its filters, or how outcomes are aggregated/presented were not inspected in this pass.
Classification: UNKNOWN -- evidence establishes the underlying data tables but not the reporting functionality/UI itself, which sits in frontend page components not covered by this evidence set.
Evidence
carecall-capability-inventory.md Capability 6; supabase/functions/start-campaign/index.ts (call_logs/campaign_patients writes); web/src/lib/supabase.ts, web/src/pages/* (not further inspected)
Grok CONFIRMED
What exists:
- Dashboard -- counts (patients on file, in campaign, needs review), live-ish activity, recent calls with result/summary.
- Campaign detail -- pulls campaign_stats (totals, pending, booked, declined, unreached, needs_human, booking_rate_pct).
- Review queue -- patients flagged for human follow-up (needs_human, verification failures, etc.) with reasons.
- Call History -- per-call result, AMD result, duration, AI summary, transcript, recording URL when present.
- Patient detail -- that patient's call history.
Where data comes from: operational tables campaign_patients (status machine) and call_logs (result, summary, transcript, amd_result, recording_url, verified, duration); the campaign_stats aggregate view (defined in migrations); and the Insights path (Telnyx Insights webhook -> telnyx-call-events -> updates call_logs summary/transcript, outcome only if the result was still empty).
There is no separate analytics/export product surface beyond these portal views and underlying tables.
Evidence
Dashboard; campaign detail (campaign_stats); Review queue; Call History; Patient detail; campaign_patients, call_logs tables; campaign_stats view; telnyx-call-events (Insights path)
OpenAI CONFIRMED
CareCall provides several reporting views.
The campaign detail page shows: total assigned patients; booked; pending (including patients awaiting a pre-call SMS delay); unreached; needs review; booking-rate percentage; patient-level status; attempt count; last attempt time; flag/review reason. It also provides a debug call-log view showing answering-machine result, whether identity was verified, verification-attempt count, call result, and call duration (CampaignDetail.tsx:119).
Call History adds patient, campaign, result, duration, AI-generated summary, and stored transcript (CallHistory.tsx:18).
Possible result/status values include: booked, declined, callback_requested, no_answer, voicemail, wrong_number, verification_failed, needs_human, transferred, error, and operational states such as pending, notified, calling, and resolved.
The data comes from three main sources: (1) campaign_patients, updated by the dialer, assistant tools, booking link, webhooks, and review UI; (2) call_logs, populated at dial time and updated by Telnyx events, assistant tools, and Insights payloads; (3) campaign_stats, a PostgreSQL aggregate view over campaigns and campaign-patient statuses.
The Insights-derived summary, transcript, and fallback outcome are less certain because the code accepts several possible Telnyx payload shapes without a checked-in authoritative schema.
Evidence
web/src/pages/CampaignDetail.tsx:119; web/src/pages/CallHistory.tsx:18
04 User Roles and Permissions
What user roles exist in CareCall? For each role, what can the user view, configure, initiate, modify or administer?
Claude Code CONFIRMED
Three roles are enforced in code: admin, clinic_admin, and staff, stored only in auth.users.app_metadata. admin-manage's action dispatcher enforces a per-action matrix (L113-290): admin can set_role and create_clinic; clinic_admin can create_user/reset_password/deactivate_user/reactivate_user but scoped to staff within their own clinic via requireTargetInScope; any authenticated user can clear_force_password_change for themselves; self-deactivation is explicitly blocked (L240-244).
Beyond admin-manage, role enforcement elsewhere is inconsistent: start-campaign performs no role check at all (any Bearer is accepted), and the assistant/booking surfaces use entirely different authorization models (shared secret, opaque token) rather than the three-role system. Portal-side RLS presumably also encodes some version of this role matrix via SQL helper functions (is_admin(), jwt_clinic_id()), but that policy set was not independently re-verified in this pass.
Classification: CONFIRMED for the admin-manage role matrix; INFERRED for RLS enforcing the same roles across portal tables (not re-audited); explicitly not enforced (a gap, not an unknown) for start-campaign.
Evidence
supabase/functions/admin-manage/index.ts L34-290; supabase/functions/_shared/lib.ts L55-67; supabase/functions/start-campaign/index.ts L88-130
Grok CONFIRMED
Roles (CONFIRMED): admin | clinic_admin | staff, stored in JWT app_metadata.role (+ clinic_id for non-admins).
- admin -- all clinics; create clinics; create any role; set roles; full user admin; see all clinic-scoped data via RLS.
- clinic_admin -- own clinic only; create staff only; reset/deactivate staff in own clinic; clinic-scoped config/data writes where policies allow.
- staff -- own clinic read/participate (run campaigns, patients, review within RLS); not user/clinic admin actions.
Enforcement is not UI-only: DB RLS on clinic-scoped tables; the admin-manage Edge Function's role matrix for user/clinic admin actions; the UI hides actions by role, but DB/function checks are the real boundary.
start-campaign only checks that a Bearer token is present (no role check inside that function) -- see Q8.
Evidence
JWT app_metadata.role/clinic_id; admin-manage role matrix; RLS on clinic-scoped tables
OpenAI CONFIRMED
CareCall has three internal roles.
Admin -- a platform-wide administrator can: view data across clinics; create and configure clinics; select clinic context when multi-clinic mode is enabled; create Admin, Provider/Clinic Admin, and Staff accounts; change roles and clinic assignments; reset, deactivate, and reactivate users; view platform-wide reporting and audit data; and perform all Clinic Admin functions.
Clinic Admin, displayed as "Provider" -- the internal role is clinic_admin, but the UI label is "Provider" (session.tsx:80). Within their clinic this role can: view patients, campaigns, results, call history, and appointments; add and modify patients; create and edit campaigns; assign patients to campaigns; start, pause, and resume campaigns; configure clinicians and availability; configure clinic calling hours, timezone, callback/SMS settings, and assistant mappings; create Staff accounts; reset/deactivate/reactivate own-clinic Staff accounts; and review audit activity. It cannot create Admin or peer Clinic Admin accounts or modify other clinics.
Staff can generally: view their clinic's dashboard; view patients and patient details; view campaigns and results; view call history, summaries, and transcripts; use the human review queue; mark flagged cases resolved; requeue flagged cases; remove a patient from a campaign; and mark a patient do-not-call. Staff are restricted by RLS from editing patients, providers, clinic settings, and campaigns.
One significant exception is campaign initiation: the campaign detail UI shows "Start calling" without checking canManage, and start-campaign has defective server authorization. Therefore Staff -- and potentially unauthenticated parties -- may be able to initiate an already-active campaign despite the intended role model.
Evidence
web/src/lib/session.tsx:80
05 Telnyx vs. CareCall Configuration
Which parts of the calling workflow are configured in Telnyx, and which are configured within CareCall? Explain the boundary between the two systems.
Claude Code CONFIRMED
Telnyx owns call transport and AI voice execution -- the AI Assistant's registered tool contracts (names, URLs, parameters, the shared-secret header) are configured in telnyx/tools.json, and call routing/AMD detection is a Telnyx Call Control feature CareCall merely reacts to via webhooks. CareCall owns everything downstream of that: the campaign/dial-queue logic (start-campaign), what happens once a call connects (telnyx-call-events' event switch -- human-class vs. machine-class handling, SMS, hangup finalization), the tool implementations themselves that Telnyx's assistant calls back into (assistant-tools -- DOB verification, slot lookup, booking, outcome recording), and the public self-service booking flow (booking-api).
In short: Telnyx is configured with where to call (via webhook targets) and what tools exist (via tools.json); CareCall's Edge Functions hold all of the actual business logic, state, and data. The exact assistant prompt/personality configuration lives in telnyx/assistant-instructions.md, referenced as existing but whose content and deployment path (dashboard vs. API-managed) was not directly inspected.
Classification: CONFIRMED for the functional boundary; UNKNOWN for exactly how/where the Telnyx-side assistant configuration is deployed/versioned.
Evidence
telnyx/tools.json; supabase/functions/telnyx-call-events/index.ts; supabase/functions/assistant-tools/index.ts
Grok CONFIRMED
Configured in Telnyx: phone number, Call Control Application, and the webhook URL target (telnyx-call-events); the AI Assistant resource(s), voice, and assistant instructions (pasted from telnyx/assistant-instructions.md); tool webhook registration pointing at assistant-tools plus the shared secret header; the messaging profile (for SMS transport); the Insights/transcript webhook target (same events URL).
Configured in CareCall: campaign definitions, patient lists, DNC/active status, clinic calling hours; the premium AMD setting requested on the dial path (CareCall requests answering_machine_detection: "premium" when dialing); the queue/status machine, pre-call SMS lead time, self-booking flags, slot length, and provider calendars; which patients get SMS (consent), SMS copy assembly, and booking link minting; storage of results on call_logs and portal display.
Boundary: Telnyx owns telephony, AMD classification, AI runtime, and message transport. CareCall owns who to call, when, business rules, verification/booking truth, and persistence/reporting. CareCall starts the assistant only after AMD human-class results and supplies limited dynamic variables.
Evidence
telnyx/assistant-instructions.md; telnyx/tools.json; telnyx-call-events (webhook target); dial-path AMD setting; call_logs storage
OpenAI CONFIRMED
Configured in Telnyx: purchased telephone number; Call Control Application; Call Control webhook destination; AI Assistant definitions; assistant voice/model settings; the assistant's system instructions/personality; registered webhook tools; tool shared-secret header configuration; Messaging Profile; post-call Insights/transcript webhook configuration. The repository includes instructions and tool definitions that an operator must copy/register in Telnyx (tools.json:1).
Configured in CareCall: clinics and users; patients, phone numbers, consent, and do-not-call status; providers and weekly availability; campaign membership; appointment type, slot length, provider, schedule, and greeting context; calling hours and timezone; pre-call SMS lead time and fallback behavior; appointment-type-to-Telnyx-assistant ID mapping; per-campaign snapshot of the selected assistant ID; call queue state and retry timing; identity-verification result; slot generation and appointment creation; reporting, review, and audit records.
At call time, CareCall asks Telnyx to place the call with premium AMD. When Telnyx reports a live human, CareCall tells Telnyx which assistant to start and provides dynamic variables. Telnyx runs the conversation; its tools call back into CareCall for verification, slots, booking, and outcomes.
Evidence
telnyx/tools.json:1
06 Conversational AI Personality and Instructions
Where and how are the conversational AI's personality, behavior, instructions or prompt configured? Can this vary by client or campaign?
Claude Code UNKNOWN
A file telnyx/assistant-instructions.md is referenced as the source for the AI assistant's behavior/instructions, but its actual contents were not reproduced or examined in any of the five artifacts -- only that the file exists and is treated as the tool/behavior source.
There is no evidence in these artifacts of a per-client or per-campaign variation mechanism (e.g. a database column or template substitution feeding different instruction text per clinic/campaign into Telnyx) -- the tool contracts in tools.json are static and global, and nothing in the discovered Edge Function code dynamically constructs or selects assistant instruction text. Dynamic variables are passed to the assistant at call start on premium AMD human-class results, which could plausibly carry per-clinic or per-campaign context into the conversation, but whether that includes personality/instruction changes versus just data variables (patient name, clinic name) is UNKNOWN from this evidence.
Classification: UNKNOWN -- the instructions file's existence and use as the tool source is CONFIRMED, but its content and whether/how it varies by client or campaign is not established by any of the five artifacts.
Evidence
telnyx/assistant-instructions.md (referenced, not inspected); supabase/functions/telnyx-call-events/index.ts (dynamic variables passed at assistant start)
Grok CONFIRMED
Primary config location: telnyx/assistant-instructions.md -- pasted into the Telnyx AI Assistant instructions in the Telnyx console (not executed from the repo at runtime). Tools: telnyx/tools.json defines webhook tools registered on that assistant.
Per-call runtime injection: patient_first_name, campaign_context, clinic_name via dynamic_variables when starting the assistant.
Per-campaign assistant override: campaigns can store telnyx_assistant_id; the dial path prefers that over the global TELNYX_ASSISTANT_ID. The schema also supports an appointment-type -> assistant mapping (appointment_type_assistants migration).
So personality/prompt can vary by clinic/campaign if you configure multiple Telnyx assistants and point campaigns at them. Variation is operational (Telnyx + campaign fields), not a free-form prompt editor inside the portal in the code reviewed.
Evidence
telnyx/assistant-instructions.md; telnyx/tools.json; dynamic_variables at assistant start; campaigns.telnyx_assistant_id; appointment_type_assistants migration
OpenAI CONFIRMED
The primary prompt is assistant-instructions.md:1. It defines the assistant as "Sarah," with a warm, professional, concise scheduling style. It specifies greeting behavior; mandatory identity verification; progressive slot presentation; explicit booking confirmation; non-pressuring decline handling; human escalation; privacy restrictions; and call-duration guidance.
This file is not loaded dynamically by CareCall. Its instructions say to paste the content into the Telnyx Assistant's Instructions field.
Variation is possible in two ways: a clinic can map each appointment type to a different Telnyx Assistant ID; and a campaign snapshots the matching assistant ID when it is created and supplies a campaign-specific greeting_context.
Therefore personality and full prompt behavior can vary by clinic and appointment type if different Telnyx assistants are configured. A campaign can vary the reason/context, but CareCall has no editor for the assistant's complete prompt or personality. Campaign-specific prompt changes beyond greeting_context require selecting or changing a Telnyx assistant.
Evidence
telnyx/assistant-instructions.md:1
07 Telnyx Webhook Security
CareCall receives webhook/event data from Telnyx during or after calls. How are those endpoints authenticated or verified? What prevents an unauthorized party from submitting fabricated webhook events?
Claude Code CONFIRMED
telnyx-call-events has no signature verification and no authentication of any kind -- confirmed by the absence of any signature/secret check across the entire file, and the endpoint always returns HTTP 200 regardless of payload validity, specifically to avoid Telnyx retry storms. This means any party who can reach the public URL and send a POST with a plausible Telnyx-shaped JSON body can inject fabricated call events -- there is nothing in code preventing it. The gateway's verify_jwt is also false for this function, so there is no platform-level JWT gate either.
The README is cited as already documenting this as a known pre-production hardening gap, meaning the team is aware of it but it has not been remediated as of this analysis.
Classification: CONFIRMED -- the single most consistently and strongly evidenced concern across all five artifacts, verified by absence across the full file rather than inference.
Evidence
supabase/functions/telnyx-call-events/index.ts (entire file, esp. L231 'always 200'); supabase/config.toml (verify_jwt=false); README hardening section (referenced)
Grok CONFIRMED
Current implementation: endpoint POST .../functions/v1/telnyx-call-events; gateway verify_jwt = false; the function performs no Telnyx signature verification and always returns { ok: true } (200) so Telnyx does not retry forever. The README explicitly lists webhook signature verification (telnyx-signature-ed25519) as pre-production hardening.
What prevents fabricated events today? Essentially obscurity of the URL and any network controls added outside the app. An unauthorized client that can POST to that URL can inject events. This is a confirmed gap. (Contrast: assistant-tools requires x-carecall-secret.)
Evidence
supabase/functions/telnyx-call-events/index.ts; supabase/config.toml (verify_jwt=false); README hardening note; contrast with assistant-tools x-carecall-secret
OpenAI CONFIRMED
There are two different security postures.
AI tool webhooks: assistant-tools requires an x-carecall-secret header equal to the TOOL_WEBHOOK_SECRET environment value -- a static shared secret (_shared/lib.ts:35). It provides basic authentication but has no visible cryptographic request signature, timestamp/replay protection, per-assistant credentials, rotation workflow, rate limit, or constant-time comparison.
Call-event and Insights webhook: telnyx-call-events performs no authentication or signature verification. It parses the submitted JSON and processes it (telnyx-call-events/index.ts:49). Nothing in the implementation prevents an unauthorized party from submitting fabricated events. Depending on the payload and known identifiers, forged submissions could change call logs or patient campaign states; inject transcripts or summaries; trigger assistant start, speech, SMS, or hangup actions; or cause incorrect no-answer/voicemail results.
The README itself identifies Telnyx Ed25519 signature verification as required hardening. This is a confirmed security gap.
Evidence
supabase/functions/_shared/lib.ts:35; supabase/functions/telnyx-call-events/index.ts:49
08 Role-Based Access Control
Is RBAC actually implemented, or are roles primarily a UI concept? Identify where authorization is enforced—in application code, middleware, database/RLS policies or elsewhere—and highlight areas where enforcement appears incomplete.
Claude Code CONFIRMED
RBAC is a mix of real enforcement and gaps, not purely a UI concept, but it's also not uniformly enforced. Where it is enforced: admin-manage has an explicit, code-level role matrix checked server-side per action -- genuine authorization, not merely hidden UI buttons.
Where it is not enforced: start-campaign, despite being invoked from the portal UI, performs no role or clinic check at all -- any request with an Authorization header whose value starts with a Bearer prefix is accepted. This is a concrete example of a capability that a role-gated UI exposes but which the underlying API does not actually re-enforce -- exactly the "RBAC is a UI concept" failure mode, present on at least one surface.
Database-layer RLS is asserted to encode role/clinic logic (portal-roles migration, is_admin()/jwt_clinic_id() helpers) but the full policy set was not independently re-audited row-by-row in this pass.
Classification: CONFIRMED that RBAC is real (not purely UI) for admin-manage; CONFIRMED (as a gap) that start-campaign lacks equivalent enforcement despite portal UI implying role-gating; INFERRED/not fully audited for the database RLS layer.
Evidence
supabase/functions/admin-manage/index.ts L113-290; supabase/functions/start-campaign/index.ts L88-130; supabase/migrations/20260705000000_portal_roles.sql (referenced, not fully re-audited)
Grok CONFIRMED
RBAC is real, multi-layered -- with one notable hole.
- Postgres RLS -- clinic isolation and many write rules via is_admin() / jwt_clinic_id() / is_clinic_admin().
- admin-manage application code -- fine-grained admin actions (create user, set role, password reset, deactivate, create clinic).
- UI -- hides controls the role should not see.
Incomplete enforcement: start-campaign requires Authorization: Bearer … but does not call getCaller or check role. Any bearer that passes the function's presence check can trigger dialing (including intended cron/service-role use) -- this is weaker than portal role expectations. Edge Functions that use the service role bypass RLS by design; correctness depends on those functions applying clinic/patient filters themselves (they generally do for dial/tools/booking).
So: RBAC is implemented, not merely cosmetic, but not uniformly applied to every Edge Function.
Evidence
Postgres RLS (is_admin()/jwt_clinic_id()/is_clinic_admin()); admin-manage application code; start-campaign (Bearer presence only, no getCaller/role check)
OpenAI CONFIRMED
RBAC is real and is not only a UI concept. Enforcement exists at several layers: (1) UI route and control guards -- roleAtLeast hides or redirects management screens; (2) database RLS -- PostgreSQL policies use JWT app_metadata.role and clinic_id to control table operations; (3) admin-manage handler -- revalidates the JWT using Supabase Auth and applies action-specific role and target-scope checks; (4) Supabase gateway -- JWT verification is enabled for admin-manage; (5) service-layer checks -- assistant tools require verification before slot discovery or booking.
Incomplete or concerning areas include: start-campaign only checks that the header begins with "Bearer "; Telnyx event ingestion is unauthenticated; Staff can write campaign_patients by design, including deletion and status changes; appointments_write RLS does not require Clinic Admin -- any authenticated user with provider-scope access could write through PostgREST, although the current frontend does not do so; all authenticated users can read all clinic rows under the latest single-clinic migration; Avatar Storage write policies check the bucket but not ownership/path; UI controls cannot compensate for the privileged endpoint defects because Edge Functions use service-role access and bypass RLS.
Evidence
supabase/functions/admin-manage/index.ts; supabase/functions/start-campaign/index.ts
09 Retrying Unsuccessful Campaign Cases
After reviewing campaign results, can a user identify unsuccessful patients/cases, change relevant configuration or data, and rerun the campaign for only those cases without repeating successful ones? Describe the supported workflow and limitations.
Claude Code UNKNOWN
None of the five artifacts document a workflow for selectively identifying failed/unsuccessful patients within a campaign, editing just those cases, and re-running only that subset. What is confirmed is the underlying mechanics a retry feature would need: mark_outcome records a small enum of terminal outcomes per patient (declined, callback_requested, wrong_number, needs_human -- L239-257), and campaign_patients status is updated per-patient during dialing.
start-campaign's "sweep" mode processes all still-eligible active patients across active campaigns in a batch, but nothing in the discovered code filters that batch down to "previously unsuccessful patients only" -- sweep/campaign_id dialing appears to operate on the current queue/filter state (DNC/active filters), not on an outcome-based retry selection. Whether the portal UI offers a "retry failed only" affordance on top of these primitives is UNKNOWN -- that would live in frontend page logic these artifacts reference for the "start campaign" button but did not inspect for retry-specific controls.
Classification: UNKNOWN -- the outcome-tracking data model exists (CONFIRMED), but a specific retry-only-failed-cases workflow was not found or ruled out in the evidence gathered.
Evidence
supabase/functions/assistant-tools/index.ts L239-257 (mark_outcome); supabase/functions/start-campaign/index.ts (sweep/batch logic); web/src/pages/CampaignDetail.tsx (referenced, not inspected for retry UI)
Grok CONFIRMED
Supported by the status machine more than by a dedicated "rerun failed only" wizard.
Each patient in a campaign has a campaign_patients.status (pending, notified, calling, booked, declined, callback_requested, no_answer, voicemail, wrong_number, verification_failed, needs_human, resolved, ...). The dialer selects pending and callback_requested (when callback_after has passed), excludes DNC/inactive, and respects calling hours. Successful booked patients are not redialed by that queue logic. Unreached outcomes (no_answer, voicemail) can be moved back into the queue by staff (e.g. status/callback adjustments in review/patient flows -- exact UI edit paths depend on page implementations). callback_requested intentionally re-enters after callback_after. A campaign can remain active or be restarted, and auto-completes when the in-flight queue empties.
Limitations: no single documented "select all failures and retry" API distinct from editing statuses and keeping the campaign active / invoking start-campaign again. Attempt counters / stale-notified recovery can force needs_human after repeated failures. Changing slots/providers/hours affects future availability; already-booked appointments are protected by DB constraints (idempotency / exclusion / one-active-per-campaign).
Evidence
campaign_patients.status machine; dialer selection logic (pending/callback_requested, DNC/inactive exclusion, calling hours); callback_after re-entry
OpenAI CONFIRMED
There is a supported selective retry workflow, but it is incomplete.
For needs_human and verification_failed cases, the Review Queue lets a user: inspect the patient, campaign, and reason; edit patient information through the patient page if their role permits it; requeue only that patient by changing their status to pending; mark the issue resolved; remove the patient from the campaign; mark the patient do-not-call and remove them from active campaigns. The requeue implementation affects only the selected campaign-patient row -- patients already marked booked, declined, or otherwise successful are not reset, so they are not called again (Review.tsx:40). Callbacks also re-enter the dial queue automatically after callback_after.
Limitations: the Review Queue includes only needs_human and verification_failed; no_answer, voicemail, wrong_number, and general errors are not surfaced there for one-click requeue. There is no bulk "retry all unsuccessful" filter/action. Requeue does not reset the attempts counter. No explicit maximum-attempt policy is enforced for ordinary retries. If the campaign has auto-completed, a Clinic Admin must reactivate it before the requeued patient can be processed. Staff can requeue cases but normally cannot change campaign/provider/clinic configuration. Editing the assistant mapping does not automatically change an existing campaign's snapshotted assistant ID. There is no transactional workflow combining "fix data, requeue, and restart" -- those are separate actions.
Evidence
web/src/pages/Review.tsx:40
10 Appointment-Slot Presentation and Selection
How does CareCall determine available appointment slots, and how are those slots presented to the patient during booking? Describe the path from availability retrieval through conversational presentation to final appointment selection.
Claude Code CONFIRMED
Two independent callers reach the same underlying RPC. Inside a live call, assistant-tools' get_appointment_slots requires the patient to be verified first (403 otherwise -- L115-118), then discloses availability progressively rather than dumping a full calendar: a "days" granularity call returns up to 3 candidate days, and a "times" granularity call (for a chosen day) returns up to 3 candidate times, each formatted as a speakable string suitable for the voice assistant to read aloud (L137-168).
Selection is finalized by create_appointment, which takes a single slot_start and books it idempotently -- a repeat request for the same call_control_id + slot_start is treated as the same booking rather than a duplicate, and unique/exclusion constraint violations (Postgres 23P01/23505) are handled as "slot already taken" / "already booked" rather than raw errors (L175-233).
The public booking API (booking-api?action=slots / action=book) calls the same get_available_slots RPC but is documented as offering a wider day range than the voice path, and its book action handles the identical 23P01/23505 race conditions (L216-361). So: one shared source of truth for availability, two presentation layers (speech-constrained progressive disclosure for voice; a fuller web UI) built on the same booking/idempotency guarantees.
Classification: CONFIRMED.
Evidence
supabase/functions/assistant-tools/index.ts L115-233; supabase/functions/booking-api/index.ts L216-361; carecall-openapi.yaml GetSlotsArgs/CreateAppointmentArgs schemas
Grok CONFIRMED
Path (shared truth for voice and web): availability source is the Postgres RPC get_available_slots (provider schedule + existing appointments + clinic timezone p_tz).
Voice (AI tools): requires verify_patient success first (else 403). get_appointment_slots with granularity: "days" returns up to 3 days with spoken labels. Then granularity: "times" + on_date returns up to 3 times with spoken strings (e.g. "Tuesday, July 7th at 10:00 AM"). The patient confirms; the AI calls create_appointment with the exact slot_start. The server inserts the appointment with an idempotency key and handles slot-taken (23P01) and already-booked (23505).
Web self-booking (booking-api): after token + DOB verify, slots can show up to 10 days and all times for a chosen day (screen, not speech). book uses the same RPC rules and constraints; success flips campaign_patients to booked (removes from dialer).
Presentation principle: the model/UI should only offer slots returned by the server; progressive disclosure on voice reduces invention risk.
Evidence
get_available_slots RPC (provider schedule + existing appointments + p_tz); assistant-tools get_appointment_slots/create_appointment; booking-api slots/book
OpenAI CONFIRMED
The intended path: provider weekly availability is stored in provider_availability; CareCall's get_available_slots PostgreSQL function generates candidate slots for upcoming dates; it removes times overlapping existing booked or confirmed appointments; the provider comes from the campaign, falling back to the patient's assigned provider; after identity verification, the AI calls get_appointment_slots; for voice calls, CareCall returns at most three available days; after the patient chooses a day, it returns at most three concrete times with speech-ready text; the prompt requires the AI to present only returned times and obtain explicit confirmation; the AI sends the exact slot_start to create_appointment; CareCall creates the appointment and updates the campaign-patient and call-log results to booked.
The public booking-link path uses the same conceptual slot engine but presents up to ten days and all available times for the selected day.
Concurrency protections include: idempotency keys for retrying the same booking request; a PostgreSQL exclusion constraint preventing provider time overlap; a unique constraint preventing one patient from holding multiple active appointments for the same campaign; conflict responses that cause the AI/UI to fetch fresh options.
Two implementation concerns are material: the Edge Functions pass p_tz, but the only checked-in SQL definition of get_available_slots does not accept that parameter -- a referenced timezone migration is missing, so slot retrieval may fail unless the deployed database contains an untracked overload; and create_appointment accepts a submitted slot_start but does not independently verify that it was one of the generated availability options -- database constraints prevent overlap, but they do not prove the time falls within configured provider availability.
Evidence
get_available_slots (PostgreSQL function, referenced); assistant-tools get_appointment_slots / create_appointment