OpenAPI Discovery
draft5/5 deliverables complete
Summary
Completed the independent API-discovery assessment. Existing application code was not modified.
Results:
- 30 capabilities: 28 CONFIRMED, 2 INFERRED, 0 UNKNOWN
- 22 logical API operations: 21 CONFIRMED, 1 INFERRED, 0 UNKNOWN
- 6 concrete OpenAPI path operations
- Authentication found: Supabase JWT, a presence-only bearer check, a static tool shared secret, no authentication at all (Telnyx webhook), and an opaque body-embedded token with DOB verification
- Parser-based OpenAPI validation: completed with
@redocly/cli lint— 0 errors, 2 warnings after fixing a YAML syntax error and 10 OpenAPI 3.1nullable-keyword errors. Both remaining warnings were left unresolved deliberately (no license for an internal artifact; the webhook genuinely has no 4xx response in code)
Major concerns:
start-campaignaccepts any bearer-prefixed value — no real authentication- Telnyx event webhook has no signature verification
- Checked-in slot RPC lacks the
p_tzparameter used by two callers - Public booking CORS headers don't allow the headers its own client sends
- Every Edge Function runs as service-role, so these authentication gaps bypass RLS entirely
- Audit records may duplicate complete patient rows (PHI) with no evident retention/redaction
start-campaignhas no top-level exception handling, unlike its four sibling functions
Repository scope
CareCall (external repository -- analyzed in a separate Claude Code session; never cloned into or accessed from KB Sandbox)
Guardrail
Safe Legacy Modernization — read-only repository analysis.
The external practitioner/AI must not modify application code, not modify infrastructure, not access production systems, and not expose secrets; must not infer unsupported endpoints; must explicitly identify uncertainty; must distinguish evidence from inference; and must preserve references to implementation evidence where practical.
Deliverables
- Capability Inventory (carecall-capability-inventory.md)
- Endpoint Inventory (carecall-endpoint-inventory.md)
- OpenAPI 3.1 Specification (carecall-openapi.yaml)
- Validation / Findings Report (carecall-openapi-findings.md)
- Evidence Map (carecall-evidence-map.md)
System Understanding
CareCall System Understanding
Version 1 · 10 questions
Evaluate whether an engineering method understands CareCall's architecture, security, configuration and operational workflows after independently examining the repository.
- Claude CodeCompleted
- GrokCompleted
- OpenAICompleted
Artifacts
Evidence Map
Evidence Map
via Claude Code · 8/16/2026, 2:52:12 AM
CareCall — Evidence Map
KB Sandbox artifact_type: evidence_map
All references are repository-relative and refer to the checked-out files inspected during discovery.
Capability evidence
| Capability | Evidence | What evidence establishes | Confidence |
|---|---|---|---|
| C1 Staff authentication/session | web/src/pages/Login.tsx:21-77; web/src/lib/session.tsx:20-77 | Email/password session, password update, session metadata | CONFIRMED |
| C2 Forced password change | web/src/pages/Login.tsx:51-77; supabase/functions/admin-manage/index.ts:207-232 | Forced-change flag handling and self-clear action | CONFIRMED |
| C3 User administration | supabase/functions/admin-manage/index.ts:31-56,91-268 | Eight action dispatch and role/target rules | CONFIRMED |
| C4 Clinic administration | supabase/functions/admin-manage/index.ts:272-290; web/src/pages/admin/Clinics.tsx:12-32 | Create, list, and activation toggle paths | CONFIRMED |
| C5 Clinic settings | web/src/pages/settings/ClinicSettings.tsx:20-86; supabase/migrations/20260705000000_portal_roles.sql:11-38 | Settings reads/writes and schema | CONFIRMED |
| C6 Appointment-type assistant routing | web/src/pages/settings/ClinicSettings.tsx:28-60; supabase/migrations/20260706000000_appointment_type_assistants.sql:16-45 | Mapping table, RLS, campaign snapshot | CONFIRMED |
| C7 Providers/availability | web/src/pages/Clinicians.tsx:17-60; supabase/migrations/20260704000000_init.sql:26-32,70-77 | Provider and weekly availability CRUD/schema | CONFIRMED |
| C8 Patient roster/import | web/src/pages/Patients.tsx:33-100; supabase/migrations/20260704000000_init.sql:34-45 | Direct roster CRUD, CSV processing, core schema | CONFIRMED |
| C9 Patient compliance flags | web/src/pages/PatientDetail.tsx:37-43; web/src/pages/Review.tsx:71-90; supabase/migrations/20260706120000_precall_sms_and_spec_v12.sql:35-40 | DNC/active/SMS-consent mutation | CONFIRMED |
| C10 Campaign authoring/lifecycle | web/src/pages/CampaignNew.tsx:22-91; web/src/pages/CampaignDetail.tsx:34-76; supabase/migrations/20260705000000_portal_roles.sql:72-95 | Create/edit/status transitions and status enum | CONFIRMED |
| C11 Campaign queue assignment | web/src/pages/Patients.tsx:93-100; web/src/pages/CampaignNew.tsx:80-91 | Patient-to-campaign insert/upsert | CONFIRMED |
| C12 Outbound execution | supabase/functions/start-campaign/index.ts:88-237,353-398; supabase/migrations/20260705000100_cron_scheduling.sql:20-55 | Sweep/single execution, gates, SMS/dial, cron | CONFIRMED |
| C13 Call orchestration | supabase/functions/telnyx-call-events/index.ts:49-190 | AMD, assistant start, voicemail/SMS, hangup | CONFIRMED |
| C14 Insights ingestion | supabase/functions/telnyx-call-events/index.ts:199-278 | Shape-sniffed summary/transcript/outcome updates | INFERRED |
| C15 Voice verification | supabase/functions/assistant-tools/index.ts:49-91 | Call-bound DOB comparison and lockout | CONFIRMED |
| C16 Voice slot discovery | supabase/functions/assistant-tools/index.ts:99-153 | Verified-call gate and 3-day/time disclosure; RPC mismatch noted | CONFIRMED |
| C17 Voice appointment booking | supabase/functions/assistant-tools/index.ts:159-230; supabase/migrations/20260706130000_self_booking_link.sql:49-57 | Idempotency, insert, conflict handling | CONFIRMED |
| C18 Voice outcome recording | supabase/functions/assistant-tools/index.ts:235-254 | Allowed outcomes and side effects | CONFIRMED |
| C19 Slot engine | supabase/migrations/20260704000000_init.sql:120-161 | Four-argument availability-minus-bookings SQL function | CONFIRMED |
| C20 Public booking | supabase/functions/booking-api/index.ts:112-132,138-428; web/src/lib/booking.ts:55-83 | Five actions and patient-facing client | CONFIRMED |
| C21 Double-booking prevention | supabase/migrations/20260704000000_init.sql:79-94; supabase/migrations/20260706130000_self_booking_link.sql:49-57 | Exclusion and partial unique constraints | CONFIRMED |
| C22 Human review queue | web/src/pages/Review.tsx:16-91 | Resolve, requeue, remove, DNC flows | CONFIRMED |
| C23 Call history/transcripts | web/src/pages/CallHistory.tsx:17-42; web/src/pages/PatientDetail.tsx:24-30 | Log/transcript reads | CONFIRMED |
| C24 Dashboard/reporting | web/src/pages/Dashboard.tsx:54-203; supabase/migrations/20260706120000_precall_sms_and_spec_v12.sql:59-79 | Counts, feed, search, slots, stats view | CONFIRMED |
| C25 Audit trail | supabase/functions/_shared/lib.ts:69-87; supabase/migrations/20260705000000_portal_roles.sql:271-309 | Explicit admin audit plus row triggers | CONFIRMED |
| C26 Profile self-service | web/src/pages/settings/Profile.tsx:16-48; supabase/migrations/20260705000000_portal_roles.sql:368-388 | Metadata/password/avatar operations | CONFIRMED |
| C27 RBAC/clinic scope | supabase/functions/_shared/lib.ts:44-67; supabase/migrations/20260705000000_portal_roles.sql:120-269 | Claims, roles, RLS policies | CONFIRMED |
| C28 Direct data access | web/src/pages/**/*.tsx; supabase/migrations/20260705000000_portal_roles.sql:151-269 | SPA uses generated PostgREST surface under RLS | CONFIRMED |
| C29 AI conversational behavior | telnyx/assistant-instructions.md:1-159; telnyx/tools.json:1-153 | Intended prompt/tool behavior, not hosted runtime | INFERRED |
| C30 Booking abuse controls | supabase/functions/booking-api/index.ts:38-42,86-110,156-209; supabase/migrations/20260706130000_self_booking_link.sql:77-108 | Generic invalid response, token lockout, IP counter | CONFIRMED |
OpenAPI operation evidence
| Logical operation | Evidence | What evidence establishes | Confidence |
|---|---|---|---|
admin-manage/list_users | supabase/functions/admin-manage/index.ts:91-110 | Role gate, scope, response fields | CONFIRMED |
admin-manage/create_user | supabase/functions/admin-manage/index.ts:113-149 | Validation, role rules, temp password, audit | CONFIRMED |
admin-manage/set_role | supabase/functions/admin-manage/index.ts:152-180 | Admin-only role/clinic change | CONFIRMED |
admin-manage/reset_password | supabase/functions/admin-manage/index.ts:184-204 | Scoped reset and response | CONFIRMED |
admin-manage/clear_force_password_change | supabase/functions/admin-manage/index.ts:207-232 | Self-only idempotent flag clear | CONFIRMED |
admin-manage/deactivate_user | supabase/functions/admin-manage/index.ts:235-260 | Ban, no-self, target scope | CONFIRMED |
admin-manage/reactivate_user | supabase/functions/admin-manage/index.ts:235-260 | Unban and target scope | CONFIRMED |
admin-manage/create_clinic | supabase/functions/admin-manage/index.ts:272-290 | Admin-only create and response | CONFIRMED |
start-campaign/single | supabase/functions/start-campaign/index.ts:88-130,132-237 | Header check, campaign lookup, queue execution | CONFIRMED |
start-campaign/sweep | supabase/functions/start-campaign/index.ts:97-130; supabase/migrations/20260705000100_cron_scheduling.sql:40-47 | All-active selection and cron request | CONFIRMED |
telnyx-call-events/call-control | supabase/functions/telnyx-call-events/index.ts:49-174 | Event families, side effects, always-200 | CONFIRMED |
telnyx-call-events/insights | supabase/functions/telnyx-call-events/index.ts:199-278 | Tolerant payload parsing and writes | INFERRED |
assistant-tools/verify_patient | supabase/functions/assistant-tools/index.ts:25-47,66-91; telnyx/tools.json:1-31 | Selector, secret, args, responses | CONFIRMED |
assistant-tools/get_appointment_slots | supabase/functions/assistant-tools/index.ts:99-153; telnyx/tools.json:32-75 | Verification gate, args, response limits | CONFIRMED |
assistant-tools/create_appointment | supabase/functions/assistant-tools/index.ts:159-230; telnyx/tools.json:76-103 | Required slot, insert, conflicts, response | CONFIRMED |
assistant-tools/mark_outcome | supabase/functions/assistant-tools/index.ts:235-254; telnyx/tools.json:104-153 | Outcome enum and state/log writes | CONFIRMED |
booking-api/context | supabase/functions/booking-api/index.ts:134-149 | PHI-minimal context/state | CONFIRMED |
booking-api/verify | supabase/functions/booking-api/index.ts:151-209 | Rate limit, DOB, attempts, lock | CONFIRMED |
booking-api/slots | supabase/functions/booking-api/index.ts:211-276 | Ready-state gate and day/time shapes | CONFIRMED |
booking-api/book | supabase/functions/booking-api/index.ts:278-403 | Validation, idempotency, insert/conflicts | CONFIRMED |
booking-api/decline | supabase/functions/booking-api/index.ts:405-428 | Callback requeue mutation | CONFIRMED |
rpc/get_available_slots | supabase/migrations/20260704000000_init.sql:124-161; web/src/pages/Dashboard.tsx:135 | Four-argument SQL signature and direct call | CONFIRMED |
Cross-cutting security evidence
| Concern | Evidence | What evidence establishes | Confidence |
|---|---|---|---|
| Edge gateway configuration | supabase/config.toml:9-39 | JWT enabled only for admin-manage | CONFIRMED |
| Portal JWT resolution | supabase/functions/_shared/lib.ts:50-67 | Bearer validation and app-metadata extraction | CONFIRMED |
| Start-campaign auth bypass | supabase/functions/start-campaign/index.ts:88-113 | Only bearer prefix is tested | CONFIRMED |
| Unsigned Telnyx webhook | supabase/functions/telnyx-call-events/index.ts:49-174; README.md:172-176 | No verification and explicit hardening TODO | CONFIRMED |
| Tool shared secret | supabase/functions/_shared/lib.ts:35-38; supabase/functions/assistant-tools/index.ts:25-27 | Static header equality | CONFIRMED |
| Clinic/role RLS | supabase/migrations/20260705000000_portal_roles.sql:120-269 | Per-table read/write policies | CONFIRMED |
| Broad clinic reads | supabase/migrations/20260706140000_single_clinic_read.sql:1-21 | Latest policy is using (true) | CONFIRMED |
| Missing timezone migration | supabase/functions/assistant-tools/index.ts:112-118; supabase/functions/booking-api/index.ts:231-238; supabase/migrations/20260704000000_init.sql:124-129; supabase/migrations/20260706130000_self_booking_link.sql:59-62 | Callers pass absent parameter; comment names absent file | CONFIRMED inconsistency / UNKNOWN deployment |
Validation / Findings Report
Findings
via Claude Code · 8/16/2026, 2:52:12 AM
CareCall — OpenAPI Discovery Findings
KB Sandbox artifact_type: findings
Basis: static, read-only analysis of the checked-out repository. No deployed service or production system was contacted.
1. Executive summary
CareCall is a React/Vite staff portal backed by Supabase Auth, PostgREST, Storage, PostgreSQL/RLS, five Deno Edge Functions, and Telnyx voice/SMS services. Discovery found 30 externally meaningful capabilities (28 CONFIRMED, 2 INFERRED, 0 UNKNOWN as capability classifications) and 22 logical custom API operations exposed through six HTTP route templates: five Edge Function URLs plus one PostgREST RPC. The OpenAPI document models the six concrete POST operations and uses discriminated oneOf bodies/query selectors for their multiplexed logical actions.
The resulting contract is a traceable hypothesis, not an approved contract. The highest-risk findings are an effectively unauthenticated campaign executor, an unsigned Telnyx webhook that performs privileged writes and outbound actions, and a missing SQL migration/signature mismatch that may break slot discovery in both patient booking channels.
2. Architecture observations
- Frontend:
web/is a React/Vite SPA. It authenticates with Supabase email/password and directly usessupabase-jsfor Auth, PostgREST table/view access, RPC, and avatar Storage. - Custom HTTP API:
supabase/functions/contains five Deno handlers.admin-manageandstart-campaignare portal/cron-facing;assistant-toolsandtelnyx-call-eventsare Telnyx-facing;booking-apiis patient-facing. - Data/business layer: migrations define the tables, constraints, slot RPC, status enums, audit triggers, RLS policies, booking-link controls, and cron job. Edge Functions use a service-role client and bypass RLS.
- External dependencies: Supabase Auth/Database/Storage/Edge Runtime/Vault/cron/net and Telnyx Call Control, Messaging, and AI Assistant APIs.
- Asynchrony:
pg_croninvokes campaign sweep every minute; Telnyx webhooks drive AMD, voicemail, hangup, and post-call insight processing. - Tests: no automated API, unit, integration, or contract tests were found. The README contains a manual smoke test only.
3. API discovery observations
- The custom API is heavily multiplexed: eight
admin-manageactions, twostart-campaignmodes, two Telnyx webhook payload families, four assistant tools, and five booking actions. Together with the direct slot RPC these are 22 logical operations over six concretePOSTpaths. - The SPA also invokes Supabase's generated Auth, PostgREST, and Storage APIs directly. Exhaustive vendor-generated CRUD paths are deliberately not copied into the OpenAPI file; their observed CareCall table-level surface is recorded in the capability inventory.
- Booking links use 128-bit opaque URL tokens whose SHA-256 hashes are stored. Context is PHI-minimal; DOB comparison is server-side; two failures lock the link/call.
- Appointment creation is idempotent and protected by provider-overlap and one-active-appointment-per-campaign database constraints.
- Webhook insights parsing is best-effort and shape-tolerant, so that payload family remains INFERRED even though its receiver is CONFIRMED.
4. Authentication and authorization observations
| Surface | Authentication | Authorization / scope | Assessment |
|---|---|---|---|
admin-manage | Supabase JWT at gateway and auth.getUser in handler | admin, clinic_admin, staff; action and target-clinic checks | Strongest custom surface; CONFIRMED |
start-campaign | Gateway JWT disabled; handler checks only that header starts with Bearer | None; service-role queries accept arbitrary campaign ID or global sweep | Critical gap: any bearer text passes |
assistant-tools | Static x-carecall-secret equality check | Call context derives patient/campaign from call_control_id; verification gates slots/book, but not mark_outcome | Shared-secret boundary; no replay/rotation evidence |
telnyx-call-events | None | None; trusts event payload and decoded client state | Critical gap: no Telnyx signature verification |
booking-api | Opaque token in JSON body; gateway JWT off | Link state, DOB verification, lockout; verify alone has per-IP rate limiting | Capability token; possession plus DOB unlocks booking |
| PostgREST/Storage | Supabase user JWT/anon key as applicable | PostgreSQL RLS; app metadata supplies role/clinic | Generally clinic-scoped, with exceptions below |
The last migration intentionally permits every authenticated user to read every clinics row for the single-clinic build. appointments_write permits all authenticated users whose target provider is in scope; it does not require Clinic Admin. campaign_patients_write likewise permits all clinic users because Staff operate the review queue.
5. OpenAPI validation findings
carecall-openapi.yamlis OpenAPI 3.1.0 and documents six concretePOSToperations. Multiplexed logical actions are modeled throughoneOf, discriminators, and selector enums.- Parser validation was performed with
@redocly/cli lint(v2, run vianpx, no project dependency added) against the checked-in file. First pass failed to parse: a YAML block-scalar plain string began with a backtick (`), which YAML reserves as an indicator character and cannot begin a plain scalar — fixed by rewording the one offendingdescription(an HTTP 404 description forstart-campaign). Second pass parsed but reported 10 schema errors, allnullable: trueused intype/formatshorthand objects — invalid in OpenAPI 3.1, whose schemas are JSON Schema 2020-12 and usetype: [X, "null"]instead of the 3.0-eranullablekeyword. All 10 were corrected. A missingsecurity: []on the publicbooking-apioperation was also flagged and added explicitly (it has no bearer/apiKey scheme — auth is an opaque token in the body — but Redocly'ssecurity-definedrule expects the absence to be stated, not implied). - Final result: the document lints clean — 0 errors, 2 warnings. Both remaining warnings were deliberately left unresolved because "fixing" them would misrepresent the implementation, not the spec: (1)
info.licenseis absent — there is no license for this internal discovery artifact; (2)telnyx-call-eventshas no documented4XXresponse — confirmed by code (index.ts:173comment) that the handler always returns200, even on internal errors, specifically so Telnyx does not retry. Manufacturing a 4xx response to satisfy the linter would contradict the evidence. - Syntactic/structural validity is not the same as contractual correctness. Passing lint proves the document is well-formed OpenAPI 3.1; it does not prove any operation matches runtime behavior, since no live endpoint was called (see §9).
- No server URL is asserted as factual. The templated Supabase origin remains deployment-specific and UNKNOWN.
- The checked-in RPC contract is limited to four parameters (
p_provider_id,p_from,p_days,p_limit). The OpenAPI intentionally does not invent ap_tzfifth parameter, despiteassistant-toolsandbooking-apipassing one — see §6/§7 item 1.
6. Missing or ambiguous information
- UNKNOWN deployed slot RPC: a comment references
20260705000000_slot_tz.sql, but that file is absent. The deployed database may or may not contain a timezone-aware overload. - UNKNOWN deployment/configuration: deployed migration state, environment variable presence, Telnyx-side registered tools/prompts, webhook configuration, and runtime secrets cannot be established statically.
- UNKNOWN Supabase gateway nuances: the exact behavior/version-specific requirements for anon-key headers were not exercised.
- INFERRED insight payload: no captured Telnyx Insights example or authoritative schema is stored locally.
- UNKNOWN operational policy: consent basis, retention, BAA status, recording enablement, and incident controls are not represented in executable repository evidence.
7. Potential defects and inconsistencies (implementation findings)
- Slot RPC mismatch — high.
assistant-toolsandbooking-apipassp_tz, but the repository's only SQL definition has no such parameter. On a database built solely from these migrations, PostgREST should reject those calls, making voice/link slot discovery fail. - Public booking CORS mismatch — high for browser use. The client sends
apikeyandAuthorization;booking-apipreflight allows onlycontent-type. Cross-origin browser requests may therefore be blocked. - Admin scope errors become 500 — medium.
requireTargetInScopethrows for not-found/forbidden targets and the outer catch returns 500, rather than 403/404. start-campaignbatch validation — medium.batch_sizeis accepted without type/range validation before being passed to query limits.- Arbitrary booking time — medium. booking handlers accept
slot_startand rely on overlap/uniqueness constraints; they do not prove the submitted time was one returned byget_available_slotsor fits configured availability. - Documentation drift — medium. README text says webhook/tool endpoints are protected by a shared-secret/always-200 pattern, but
telnyx-call-eventshas no secret or signature check. README also describes older cron/config details in places. - Clinic read policy drift — low/intentional. the latest migration broadens clinic reads beyond the earlier role specification.
start-campaignhas no top-level exception handling — medium. Every other Edge Function wraps its handler body intry/catchand returns a JSON{error}on failure.start-campaign/index.tsdoes not (confirmed by grep: only inner helpers likedialPatientcatch locally). An exception anywhere in the main request path — e.g. theclinic_within_calling_hoursRPC — becomes an unhandled rejection, so the caller gets the Deno/Supabase Edge Runtime's generic 500 instead of the application's normal error shape, which is inconsistent with the other four functions and could confuse callers (includingpg_cron, whose retry/alerting behavior on a generic 500 is UNKNOWN).
8. Security concerns (implementation findings)
- Critical: campaign execution authorization bypass. Because
verify_jwt=falseand only the bearer prefix is checked, an unauthenticated party can trigger single-campaign execution or a global sweep, causing PHI-related outbound calls/SMS and privileged database writes through the service role. - Critical: unsigned webhook. Any caller can forge call events/insights, mutate call logs and campaign state, initiate Telnyx assistant/speak/hangup/message actions for known call IDs, and inject transcript/summary material. The README explicitly flags signature verification as unfinished.
- High: service-role blast radius. Every Edge Function uses a service-role client; defects in request authentication/validation bypass RLS completely.
- High: sensitive audit data. the generic audit trigger stores complete row JSON for patient changes, potentially duplicating PHI into
audit_log; retention/redaction is not evident. - Medium: temporary passwords returned in plaintext. This is intentional one-time UI behavior, but secure delivery, display lifetime, and logging protections are not established.
- Medium: shared-secret limitations. Assistant tools compare one static value with ordinary equality; no request signing, replay prevention, rotation scheme, or per-assistant scoping is evident.
- Medium: permissive CORS. custom functions generally return
Access-Control-Allow-Origin: *; authorization is the primary boundary. - Medium: avatar bucket policy. any authenticated user may insert/update any object in the public bucket subject only to
bucket_id; the SQL policy does not constrain path ownership.
9. Limitations of the analysis (AI discovery limitations)
- Static analysis cannot confirm deployment state, vendor behavior, database grants, network controls, or real responses.
- No requests were made because no safe local API stack/test harness was present and production access was prohibited.
- Generated Supabase Auth/PostgREST/Storage contracts are not exhaustively expanded; only observed table operations and the bespoke RPC are inventoried.
- TypeScript casts and permissive JSON parsing mean some external payload fields cannot be proven required merely from runtime code.
- Absence of a file proves only absence from this checkout, not absence from an independently modified deployment.
These limitations are separate from the implementation defects in §§7–8.
10. Recommended human-review areas
- Compare the six concrete paths and 22 logical operations against the human benchmark.
- Inspect the deployed
get_available_slotssignature and recover/approve the missing timezone migration. - Contract-test every selector/action, status code, and response variant with synthetic data.
- Review
start-campaignauthentication and Telnyx signature verification before any real use. - Validate RLS with a role/tenant matrix, especially appointments, campaign-patient mutation, clinic reads, and avatar paths.
- Confirm Telnyx webhook/tool payload schemas and retry semantics.
- Perform healthcare privacy, consent, retention, vendor-BAA, and audit-log reviews.
11. MCP readiness
Reasonable read-oriented MCP candidates after contract approval are: list campaign status/statistics, retrieve available slots, summarize call outcomes, and surface a de-identified review queue. Mutating tools such as create appointment, update callback outcome, or start a single campaign require explicit user confirmation, narrow clinic/resource scopes, idempotency, and durable audit records.
Do not directly expose global sweep execution, user/role/password administration, deactivation, raw patient/DOB records, raw transcripts, arbitrary PostgREST CRUD, or webhook ingestion to a general AI. Before MCP work, resolve the slot RPC mismatch, strengthen campaign/webhook authentication, define minimum-necessary PHI projections, establish per-tool roles/tenant scoping, and specify confirmation/replay/rate-limit/error behavior.
OpenAPI 3.1 Specification
OpenAPI Spec
via Claude Code · 8/16/2026, 2:52:11 AM
openapi: 3.1.0 info: title: CareCall Bespoke API (Discovered) version: "0.1.0-discovery" summary: >- Evidence-backed reconstruction of CareCall's custom Supabase Edge Function API, produced by static analysis. This is a HYPOTHESIS to be validated by a human reviewer and/or contract testing, not an authored, approved contract. description: | Scope and provenance (see carecall-openapi-findings.md for full detail):
* This document covers ONLY the five bespoke Deno Edge Functions under
`supabase/functions/` (`admin-manage`, `start-campaign`,
`telnyx-call-events`, `assistant-tools`, `booking-api`) plus one
directly-client-called bespoke RPC (`get_available_slots`). These are
CareCall's own business logic.
* It deliberately does NOT enumerate the generic PostgREST CRUD surface
(`/rest/v1/{table}`) that Supabase auto-generates over ~10 clinic-scoped
tables, nor Supabase's own `/auth/v1/*` endpoints. Both are real,
externally reachable, and used directly by the web client — see
carecall-capability-inventory.md §2 and carecall-openapi-findings.md §5
for what they are and why they are out of scope here.
* Every schema/operation below traces to source evidence in
carecall-evidence-map.md. Fields whose type could not be fully
confirmed are annotated inline.
* Several operations multiplex several distinct logical actions behind
one URL via a query parameter (`assistant-tools`, `booking-api`) or a
JSON body field (`admin-manage`, `start-campaign`). Request/response
bodies are modeled with `oneOf` to represent this faithfully; consult
carecall-endpoint-inventory.md for the prose per-action breakdown.
contact: name: CareCall OpenAPI Discovery Workstream externalDocs: description: Endpoint-level narrative detail, evidence map, and findings url: ./carecall-endpoint-inventory.md
No server URL is asserted: SUPABASE_URL is project-specific and only ever
referenced via Deno.env.get("SUPABASE_URL") in code / env files, never as a
literal in the repository. Asserting one here would be fabrication.
servers:
- url: "{SUPABASE_URL}/functions/v1" description: >- Supabase project's Edge Functions gateway. SUPABASE_URL is project-specific and not present as a literal anywhere in this repository (UNKNOWN — see findings §6). variables: SUPABASE_URL: default: "https://.supabase.co"
tags:
- name: admin-manage description: Portal user & clinic administration (staff-facing, JWT-authenticated)
- name: start-campaign description: Outbound dialer / campaign queue execution
- name: telnyx-call-events description: Telnyx Call Control + AI Assistant Insights webhook
- name: assistant-tools description: Telnyx AI Assistant tool-calling backend (voice channel)
- name: booking-api description: Public self-service booking (SMS-delivered link, patient-facing)
- name: slots description: Shared slot-generation RPC
paths:
/admin-manage:
post:
tags: [admin-manage]
operationId: adminManage
summary: Multiplexed portal user & clinic administration actions
description: >-
Single endpoint; the action field in the request body selects the
operation. Every action re-derives the caller's role/clinic from
their Supabase JWT server-side (never trusts client-asserted role).
Evidence: supabase/functions/admin-manage/index.ts:31-56.
security:
- portalBearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/AdminManageListUsersRequest'
- $ref: '#/components/schemas/AdminManageCreateUserRequest'
- $ref: '#/components/schemas/AdminManageSetRoleRequest'
- $ref: '#/components/schemas/AdminManageResetPasswordRequest'
- $ref: '#/components/schemas/AdminManageClearForcePasswordChangeRequest'
- $ref: '#/components/schemas/AdminManageDeactivateUserRequest'
- $ref: '#/components/schemas/AdminManageReactivateUserRequest'
- $ref: '#/components/schemas/AdminManageCreateClinicRequest'
discriminator:
propertyName: action
mapping:
list_users: '#/components/schemas/AdminManageListUsersRequest'
create_user: '#/components/schemas/AdminManageCreateUserRequest'
set_role: '#/components/schemas/AdminManageSetRoleRequest'
reset_password: '#/components/schemas/AdminManageResetPasswordRequest'
clear_force_password_change: '#/components/schemas/AdminManageClearForcePasswordChangeRequest'
deactivate_user: '#/components/schemas/AdminManageDeactivateUserRequest'
reactivate_user: '#/components/schemas/AdminManageReactivateUserRequest'
create_clinic: '#/components/schemas/AdminManageCreateClinicRequest'
responses:
"200":
description: >-
Action-specific success payload. See
carecall-endpoint-inventory.md "Detail: admin-manage" table for
the per-action response shape; not modeled as a single oneOf here
because each action's 200 response has a materially different
shape and OpenAPI cannot key a response schema off a request-body
discriminator.
content:
application/json:
schema:
$ref: '#/components/schemas/AdminManageGenericSuccess'
"400":
description: Unknown action, or missing/invalid required fields.
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
"401":
description: Missing or invalid Supabase session (no resolvable caller/role).
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
"403":
description: Caller's role does not permit this action or target.
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
"500":
description: Unhandled server error.
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
/start-campaign:
post:
tags: [start-campaign]
operationId: startCampaign
summary: Advance one campaign's, or every active campaign's, dial/notify queue
description: |
SECURITY NOTE (CONFIRMED finding, see carecall-openapi-findings.md §4):
the gateway-level Supabase JWT check is disabled for this function
(verify_jwt=false in supabase/config.toml, to allow the pg_cron
service-role invocation), and the function's own check only verifies
that an Authorization: Bearer <anything> header is PRESENT — it
never validates the token's signature, expiry, or the caller's
identity against Supabase Auth or the service-role key. Evidence:
supabase/functions/start-campaign/index.ts:91-92.
security:
- presenceOnlyBearer: []
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/StartCampaignSingleRequest'
- $ref: '#/components/schemas/StartCampaignSweepRequest'
responses:
"200":
description: Batch advanced (single-campaign or sweep-mode response).
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/StartCampaignResult'
- $ref: '#/components/schemas/StartCampaignSweepResult'
- $ref: '#/components/schemas/StartCampaignNotActiveResult'
"400":
description: Neither campaign_id nor sweep:true provided.
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
"401":
description: No Authorization header, or it does not start with "Bearer ".
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
"404":
description: The given campaign_id does not exist.
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
"500":
description: >-
UNLIKE the other four functions, start-campaign has no top-level
try/catch around its handler body (evidence: absence of any such
block wrapping index.ts:88-130, confirmed by grep for try/catch
in the file — only inner helper functions like dialPatient() and
clinicLeadSeconds() catch their own errors). An unhandled
exception elsewhere (e.g. the clinic_within_calling_hours RPC
call) surfaces as the Deno/Supabase Edge Runtime's generic 500,
NOT the application's {error: "..."} JSON shape used elsewhere
in this repo. Response body shape here is therefore INFERRED
(platform default), not application-controlled.
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
/telnyx-call-events: post: tags: [telnyx-call-events] operationId: telnyxCallEvents summary: Telnyx Call Control webhook + AI Assistant Insights webhook (shared URL) description: | Receives two logically distinct payload families on one URL, distinguished by shape-sniffing (looksLikeInsights(), evidence: supabase/functions/telnyx-call-events/index.ts:206-211): (1) Call Control lifecycle events (call.answered, call.machine.detection.ended, call.machine.greeting.ended, call.speak.ended, call.hangup), and (2) the AI Assistant's post-call Insights payload (transcript/summary /outcome). Always responds 200 to avoid Telnyx retry storms, even on internal processing errors (evidence: index.ts:169-174).
SECURITY NOTE (CONFIRMED finding): no authentication of any kind is
enforced on this endpoint — no shared secret, no Telnyx webhook
signature check. Confirmed by the absence of any such check in the
handler and self-acknowledged in README.md ("Hardening before
production" section) as an outstanding task. See
carecall-openapi-findings.md §4.
security: []
requestBody:
required: true
content:
application/json:
schema:
description: >-
Untyped: request shape is entirely Telnyx-defined and the
handler itself pattern-matches rather than validating against
a schema. Modeled here only at the "has a data object, or
looks like insights" level actually relied upon by the code.
type: object
responses:
"200":
description: >-
Always `{ "ok": true }`, including on internal errors (the
handler swallows exceptions per-event-type to guarantee Telnyx
never sees a non-200 and retries indefinitely).
content:
application/json:
schema:
type: object
properties:
ok: { type: boolean, const: true }
required: [ok]
/assistant-tools:
post:
tags: [assistant-tools]
operationId: assistantTools
summary: Telnyx AI Assistant tool-calling backend (voice channel)
description: >-
The tool query parameter selects the tool. Caller identity is never
taken from the request body — the AI never supplies a patient ID; the
call context (patient/campaign/clinic) is resolved server-side from
call_logs via the Telnyx-supplied call_control_id, which Telnyx
includes automatically in every tool-call payload. Evidence:
supabase/functions/assistant-tools/index.ts:25-59; parameter shapes
cross-confirmed by telnyx/tools.json.
security:
- toolSharedSecret: []
parameters:
- name: tool
in: query
required: true
schema:
type: string
enum: [verify_patient, get_appointment_slots, create_appointment, mark_outcome]
requestBody:
required: true
content:
application/json:
schema:
description: >-
Telnyx wraps tool arguments as data.payload.arguments (or,
per a defensive fallback in code, arguments or the raw
body). Evidence: index.ts:29-33. The effective argument
object per tool value is one of:
oneOf:
- $ref: '#/components/schemas/VerifyPatientArgs'
- $ref: '#/components/schemas/GetAppointmentSlotsArgs'
- $ref: '#/components/schemas/CreateAppointmentArgs'
- $ref: '#/components/schemas/MarkOutcomeArgs'
responses:
"200":
description: Tool-specific success payload.
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/VerifyPatientResponse'
- $ref: '#/components/schemas/GetAppointmentSlotsResponse'
- $ref: '#/components/schemas/CreateAppointmentResponse'
- $ref: '#/components/schemas/MarkOutcomeResponse'
"400":
description: Unknown tool, missing required argument, or invalid outcome.
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
"401":
description: Missing/incorrect x-carecall-secret header.
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
"403":
description: Call is not yet identity-verified (get_appointment_slots, create_appointment).
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
"500":
description: >-
Internal error. Body includes a say field with patient-safe
spoken text for the AI to read aloud (evidence: index.ts:45).
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
/booking-api:
post:
tags: [booking-api]
operationId: bookingApi
summary: Public self-service booking API (patient-facing, no portal login)
description: >-
The action query parameter selects the operation. Auth is an opaque
per-(campaign,patient) token carried in the JSON body (never a
header) and matched server-side against a stored SHA-256 hash — this
does not map to a standard OpenAPI securityScheme location
(header/query/cookie), so it is modeled as a required body field
token on every action instead of a security requirement. Evidence:
supabase/functions/booking-api/index.ts:87-101,
supabase/functions/_shared/lib.ts:111-127.
security: []
parameters:
- name: action
in: query
required: true
schema:
type: string
enum: [context, verify, slots, book, decline]
requestBody:
required: true
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/BookingContextRequest'
- $ref: '#/components/schemas/BookingVerifyRequest'
- $ref: '#/components/schemas/BookingSlotsRequest'
- $ref: '#/components/schemas/BookingBookRequest'
- $ref: '#/components/schemas/BookingDeclineRequest'
responses:
"200":
description: Action-specific success/state payload.
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/BookingContextResponse'
- $ref: '#/components/schemas/BookingVerifyResponse'
- $ref: '#/components/schemas/BookingSlotsResponse'
- $ref: '#/components/schemas/BookingBookResponse'
- $ref: '#/components/schemas/BookingDeclineResponse'
"400":
description: Unknown action, or missing slot_start for book.
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
"403":
description: Link exists but is not yet verified/ready for slots/book.
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
"405":
description: Non-POST method.
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
"429":
description: Per-IP verify rate limit exceeded (>10/minute).
content:
application/json:
schema: { $ref: '#/components/schemas/BookingVerifyResponse' }
"500":
description: Internal error.
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
/rest/v1/rpc/get_available_slots:
post:
tags: [slots]
operationId: getAvailableSlots
summary: Bespoke slot-generation function (PostgREST RPC), called directly by the staff dashboard
description: >-
This is the one PostgREST RPC included in this document because it is
bespoke business logic (not generic table CRUD) and is called
directly by the web client, not only server-side. It is NOT
security definer, so Row-Level Security on the underlying
provider_availability/appointments tables applies using the
caller's own JWT. The checked-in function accepts exactly four
arguments; a referenced timezone-aware migration is absent. Evidence:
supabase/migrations/20260704000000_init.sql:124-161 (definition),
web/src/pages/Dashboard.tsx:135 (direct client call).
security:
- portalBearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [p_provider_id]
properties:
p_provider_id: { type: string, format: uuid }
p_from:
type: string
format: date
description: Defaults to current_date + 1 server-side if omitted.
p_days: { type: integer, default: 14 }
p_limit: { type: integer, default: 30 }
responses:
"200":
description: Available slots for the provider, ordered by start time.
content:
application/json:
schema:
type: array
items:
type: object
properties:
slot_start: { type: string, format: date-time }
slot_end: { type: string, format: date-time }
"400":
description: >-
Malformed arguments (e.g. non-UUID p_provider_id). Standard
PostgREST RPC error behavior (INFERRED — not exercised against a
live instance; PostgREST's documented convention, not CareCall
code).
content:
application/json:
schema: { $ref: '#/components/schemas/ErrorResponse' }
components:
securitySchemes:
portalBearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: >-
Supabase Auth user session JWT. Verified both at the Supabase
gateway (verify_jwt=true) and again inside the function via
supabase.auth.getUser(token) to read app_metadata.role/clinic_id.
toolSharedSecret:
type: apiKey
in: header
name: x-carecall-secret
description: >-
Static shared secret, compared with === (not constant-time) against
the TOOL_WEBHOOK_SECRET environment variable. Evidence:
supabase/functions/_shared/lib.ts:36-38.
presenceOnlyBearer:
type: http
scheme: bearer
description: >-
NON-STANDARD / WEAK: the server only checks that this header is
present and prefixed "Bearer " — the token value itself is never
validated. Documented as a distinct scheme (rather than reusing
portalBearerAuth) specifically to avoid implying it provides real
authentication. See operation description and
carecall-openapi-findings.md §4.
schemas: ErrorResponse: type: object required: [error] properties: error: { type: string } say: type: string description: Present only on assistant-tools error responses (spoken text for the AI).
# ---------------- admin-manage ----------------
AdminManageListUsersRequest:
type: object
required: [action]
properties:
action: { type: string, const: list_users }
AdminManageCreateUserRequest:
type: object
required: [action, email]
properties:
action: { type: string, const: create_user }
email: { type: string, format: email }
role: { type: string, enum: [admin, clinic_admin, staff], default: staff }
clinic_id:
type: string
format: uuid
description: >-
Ignored/overridden to the caller's own clinic unless caller is
Admin. Evidence: admin-manage/index.ts:126-129.
AdminManageSetRoleRequest:
type: object
required: [action, user_id, role]
properties:
action: { type: string, const: set_role }
user_id: { type: string, format: uuid }
role: { type: string, enum: [admin, clinic_admin, staff] }
clinic_id: { type: [string, "null"], format: uuid }
AdminManageResetPasswordRequest:
type: object
required: [action, user_id]
properties:
action: { type: string, const: reset_password }
user_id: { type: string, format: uuid }
AdminManageClearForcePasswordChangeRequest:
type: object
required: [action]
properties:
action: { type: string, const: clear_force_password_change }
AdminManageDeactivateUserRequest:
type: object
required: [action, user_id]
properties:
action: { type: string, const: deactivate_user }
user_id: { type: string, format: uuid }
AdminManageReactivateUserRequest:
type: object
required: [action, user_id]
properties:
action: { type: string, const: reactivate_user }
user_id: { type: string, format: uuid }
AdminManageCreateClinicRequest:
type: object
required: [action, name]
properties:
action: { type: string, const: create_clinic }
name: { type: string }
timezone: { type: string, default: America/Chicago }
phone_callback: { type: [string, "null"] }
greeting_default: { type: [string, "null"] }
AdminManageGenericSuccess:
description: >-
Superset shape covering all admin-manage 200 responses; not every
field is present on every action (see endpoint inventory for the
exact per-action fields).
type: object
properties:
ok: { type: boolean }
users:
type: array
items:
type: object
properties:
id: { type: string, format: uuid }
email: { type: string }
role: { type: string }
clinic_id: { type: [string, "null"] }
deactivated: { type: boolean }
last_sign_in_at: { type: [string, "null"], format: date-time }
created_at: { type: string, format: date-time }
user_id: { type: string, format: uuid }
clinic_id: { type: string, format: uuid }
temporary_password:
type: string
description: Plaintext, shown once. See findings §8 for handling notes.
note: { type: string }
# ---------------- start-campaign ----------------
StartCampaignSingleRequest:
type: object
required: [campaign_id]
properties:
campaign_id: { type: string, format: uuid }
batch_size: { type: integer, default: 3 }
StartCampaignSweepRequest:
type: object
required: [sweep]
properties:
sweep: { type: boolean, const: true }
batch_size: { type: integer, default: 5, description: "Cron invokes with batch_size=5, evidence cron_scheduling.sql:46" }
StartCampaignResult:
type: object
properties:
notified: { type: integer }
started: { type: integer }
StartCampaignSweepResult:
allOf:
- $ref: '#/components/schemas/StartCampaignResult'
- type: object
properties:
campaigns: { type: integer }
skipped_outside_hours: { type: integer }
StartCampaignNotActiveResult:
type: object
properties:
started: { type: integer, const: 0 }
notified: { type: integer, const: 0 }
message: { type: string }
# ---------------- assistant-tools ----------------
VerifyPatientArgs:
type: object
required: [stated_date_of_birth]
properties:
stated_date_of_birth:
type: string
description: "Normalized YYYY-MM-DD per telnyx/tools.json description."
call_control_id:
type: string
description: Supplied automatically by Telnyx, not by the AI's tool-call arguments.
VerifyPatientResponse:
type: object
properties:
match: { type: boolean }
locked: { type: boolean }
remaining_attempts: { type: integer }
GetAppointmentSlotsArgs:
type: object
required: [granularity]
properties:
granularity: { type: string, enum: [days, times] }
on_date: { type: string, format: date, description: "Required by convention when granularity=times; not enforced server-side as required (evidence: index.ts:142-146 treats it as optional filter)." }
from_date: { type: string, format: date }
days: { type: string, description: "Numeric string; parsed with Number(). Default 14." }
GetAppointmentSlotsResponse:
type: object
properties:
days:
type: array
items:
type: object
properties:
date: { type: string, format: date }
spoken: { type: string }
times:
type: array
items:
type: object
properties:
slot_start: { type: string, format: date-time }
slot_end: { type: string, format: date-time }
spoken: { type: string }
say: { type: string }
CreateAppointmentArgs:
type: object
required: [slot_start]
properties:
slot_start: { type: string, format: date-time }
CreateAppointmentResponse:
type: object
properties:
booked: { type: boolean }
appointment_id: { type: [string, "null"], format: uuid }
spoken: { type: [string, "null"] }
already_booked: { type: boolean }
reason: { type: string, enum: [slot_taken] }
say: { type: string }
MarkOutcomeArgs:
type: object
required: [outcome]
properties:
outcome: { type: string, enum: [declined, callback_requested, wrong_number, needs_human] }
callback_after: { type: string, format: date-time }
note: { type: string }
MarkOutcomeResponse:
type: object
properties:
recorded: { type: boolean, const: true }
# ---------------- booking-api ----------------
BookingContextRequest:
type: object
required: [token]
properties:
token: { type: string, maxLength: 100 }
BookingContextResponse:
type: object
properties:
clinic_name: { type: string }
appointment_type_label: { type: string }
timezone: { type: string }
state: { $ref: '#/components/schemas/BookingLinkState' }
BookingVerifyRequest:
type: object
required: [token, stated_date_of_birth]
properties:
token: { type: string, maxLength: 100 }
stated_date_of_birth: { type: string, format: date }
BookingVerifyResponse:
type: object
properties:
verified: { type: boolean }
first_name: { type: string }
state: { $ref: '#/components/schemas/BookingLinkState' }
remaining_attempts: { type: integer }
error: { type: string }
BookingSlotsRequest:
type: object
required: [token]
properties:
token: { type: string, maxLength: 100 }
granularity: { type: string, enum: [days, times], default: days }
on_date: { type: string, format: date }
BookingSlotsResponse:
type: object
properties:
days:
type: array
items:
type: object
properties:
date: { type: string, format: date }
label: { type: string }
count: { type: integer }
times:
type: array
items:
type: object
properties:
slot_start: { type: string, format: date-time }
slot_end: { type: string, format: date-time }
label: { type: string }
timezone: { type: string }
state: { $ref: '#/components/schemas/BookingLinkState' }
BookingBookRequest:
type: object
required: [token, slot_start]
properties:
token: { type: string, maxLength: 100 }
slot_start: { type: string, format: date-time }
BookingBookResponse:
type: object
properties:
booked: { type: boolean }
already: { type: boolean }
reason: { type: string, enum: [slot_taken] }
appointment_id: { type: [string, "null"], format: uuid }
spoken: { type: [string, "null"] }
state: { $ref: '#/components/schemas/BookingLinkState' }
error: { type: string }
BookingDeclineRequest:
type: object
required: [token]
properties:
token: { type: string, maxLength: 100 }
callback: { type: boolean }
BookingDeclineResponse:
type: object
properties:
declined: { type: boolean, const: true }
BookingLinkState:
type: string
enum: [needs_verification, ready, booked, expired, locked]
description: >-
Derived server-side in linkState() (booking-api/index.ts:104-110),
never client-supplied.
Endpoint Inventory
Endpoint Inventory
via Claude Code · 8/15/2026, 10:15:07 AM
CareCall — Endpoint Inventory
Scope: the 5 bespoke Supabase Edge Functions under supabase/functions/. Each is one Deno HTTP handler reachable at ${SUPABASE_URL}/functions/v1/{function-name}, multiplexing several "actions"/"tools" via a query parameter or a JSON body field. All are invoked over HTTPS; method is POST for every action (plus a CORS-only OPTIONS preflight on four of the five). Gateway-level JWT enforcement (verify_jwt in supabase/config.toml) is noted per function — this runs before the function code and is a separate layer from the function's own auth logic.
Direct PostgREST/table access (not routed through these functions) is inventoried at the table level in carecall-capability-inventory.md §2, not repeated here.
Summary table
| Method | Route | Purpose | Auth | Evidence | Confidence |
|---|---|---|---|---|---|
| POST | /functions/v1/admin-manage (action=list_users) | List portal users (own clinic, or all for Admin) | Supabase user JWT (gateway-verified) + role check | supabase/functions/admin-manage/index.ts:91-110 | CONFIRMED |
| POST | /functions/v1/admin-manage (action=create_user) | Invite a new portal user with a temporary password | Supabase user JWT + role/scope check | supabase/functions/admin-manage/index.ts:113-149 | CONFIRMED |
| POST | /functions/v1/admin-manage (action=set_role) | Change a user's role/clinic | Supabase user JWT, Admin only | supabase/functions/admin-manage/index.ts:152-181 | CONFIRMED |
| POST | /functions/v1/admin-manage (action=reset_password) | Issue a new temporary password for a user | Supabase user JWT + scope check | supabase/functions/admin-manage/index.ts:184-204 | CONFIRMED |
| POST | /functions/v1/admin-manage (action=clear_force_password_change) | Caller clears their own forced-password-change flag | Supabase user JWT (self only) | supabase/functions/admin-manage/index.ts:211-232 | CONFIRMED |
| POST | /functions/v1/admin-manage (action=deactivate_user) | Ban a user (blocks token refresh) | Supabase user JWT + scope check, no self | supabase/functions/admin-manage/index.ts:235-260 | CONFIRMED |
| POST | /functions/v1/admin-manage (action=reactivate_user) | Lift a user ban | Supabase user JWT + scope check | supabase/functions/admin-manage/index.ts:235-260 | CONFIRMED |
| POST | /functions/v1/admin-manage (action=create_clinic) | Create a new clinic | Supabase user JWT, Admin only | supabase/functions/admin-manage/index.ts:272-290 | CONFIRMED |
| POST | /functions/v1/start-campaign | Advance one campaign's dial/notify queue by one batch | Presence-only bearer header check (see note) | supabase/functions/start-campaign/index.ts:88-130 | CONFIRMED |
| POST | /functions/v1/start-campaign (sweep:true) | Advance every active campaign (cron heartbeat) | Same as above | supabase/functions/start-campaign/index.ts:98-102 | CONFIRMED |
| POST | /functions/v1/telnyx-call-events | Telnyx Call Control webhook: AMD result, greeting-ended, speak-ended, hangup | None | supabase/functions/telnyx-call-events/index.ts:49-174 | CONFIRMED |
| POST | /functions/v1/telnyx-call-events | Telnyx AI Assistant "Insights" webhook (same URL, shape-sniffed) | None | supabase/functions/telnyx-call-events/index.ts:206-278 | INFERRED (payload contract) |
| POST | /functions/v1/assistant-tools?tool=verify_patient | Voice call: compare stated DOB server-side | Shared secret header x-carecall-secret | supabase/functions/assistant-tools/index.ts:66-91 | CONFIRMED |
| POST | /functions/v1/assistant-tools?tool=get_appointment_slots | Voice call: fetch available days/times | Shared secret header | supabase/functions/assistant-tools/index.ts:99-153 | CONFIRMED |
| POST | /functions/v1/assistant-tools?tool=create_appointment | Voice call: book the confirmed slot | Shared secret header | supabase/functions/assistant-tools/index.ts:159-230 | CONFIRMED |
| POST | /functions/v1/assistant-tools?tool=mark_outcome | Voice call: record non-booking outcome | Shared secret header | supabase/functions/assistant-tools/index.ts:235-254 | CONFIRMED |
| POST | /functions/v1/booking-api?action=context | Public: resolve a booking-link token to clinic name / state | Opaque token in request body | supabase/functions/booking-api/index.ts:138-149 | CONFIRMED |
| POST | /functions/v1/booking-api?action=verify | Public: DOB verification for a booking link | Opaque token + per-IP rate limit | supabase/functions/booking-api/index.ts:156-209 | CONFIRMED |
| POST | /functions/v1/booking-api?action=slots | Public: available days/times for a verified link | Opaque token, link must be ready | supabase/functions/booking-api/index.ts:216-276 | CONFIRMED |
| POST | /functions/v1/booking-api?action=book | Public: book the confirmed slot | Opaque token, link must be ready | supabase/functions/booking-api/index.ts:288-361 | CONFIRMED |
| POST | /functions/v1/booking-api?action=decline | Public: "call me instead" — requeue for callback | Opaque token | supabase/functions/booking-api/index.ts:409-428 | CONFIRMED |
| RPC (PostgREST) | POST /rest/v1/rpc/get_available_slots | Bespoke slot-generation function, called directly by the staff dashboard; the checked-in signature has four parameters | Supabase user JWT (RLS applies to underlying tables; function is not security definer) | supabase/migrations/20260704000000_init.sql:124-161, called from web/src/pages/Dashboard.tsx:135 | CONFIRMED |
Detail: admin-manage
Gateway auth: verify_jwt = true (supabase/config.toml:18-19) — Supabase rejects requests without a valid user JWT before the function runs.
Function-level auth: getCaller(req) (supabase/functions/_shared/lib.ts:55-67) re-derives role/clinic from app_metadata via supabase.auth.getUser(token). Every action additionally checks caller.role.
Request shape: { "action": "<name>", ...action-specific fields }.
Response shape: { ...result } on success, { "error": "<message>" } with a non-2xx status on failure. Every branch returns via the shared json() helper, which always sets Content-Type: application/json and CORS headers (supabase/functions/_shared/lib.ts:89-95).
| Action | Caller requirement | Notable request fields | Notable response fields | Side effects |
|---|---|---|---|---|
list_users | clinic_admin or admin | — | { users: [{id,email,role,clinic_id,deactivated,last_sign_in_at,created_at}] } | none (read) |
create_user | clinic_admin/admin; non-admin forced to role=staff and own clinic | email, role, clinic_id? | { ok, user_id, temporary_password } | creates auth.users row; writes audit_log (user_created) |
set_role | admin only; blocks self-demotion if caller is the last admin | user_id, role, clinic_id? | { ok, note } | updates app_metadata; writes audit_log (user_role_changed) |
reset_password | clinic_admin/admin; target must be in caller's scope (own-clinic Staff, unless caller is Admin) | user_id | { ok, temporary_password } | sets new password + force-change flag; writes audit_log (password_reset) |
clear_force_password_change | any authenticated caller, self only (no user_id param — always acts on caller) | — | { ok } | clears caller's own force-change flag; writes audit_log (password_change_completed) — idempotent no-op if already cleared |
deactivate_user | clinic_admin/admin; cannot target self; target must be in scope | user_id | { ok } | sets ban_duration: 876000h; writes audit_log (user_deactivated) |
reactivate_user | clinic_admin/admin; target must be in scope | user_id | { ok } | clears ban; writes audit_log (user_reactivated) |
create_clinic | admin only | name, timezone?, phone_callback?, greeting_default? | { ok, clinic_id } | inserts clinics row; writes audit_log (clinic_created) |
Errors: unknown action → 400 { error: "unknown action: <action>" } (line 50); missing/invalid caller → 401 { error: "unauthorized" } (line 35); permission failures → 403 { error: "forbidden..." } (multiple, e.g. lines 92, 125, 153); validation failures → 400 with a specific message; unexpected exceptions → 500 { error: "<message or 'internal error'>" } (lines 52-55).
Detail: start-campaign
Gateway auth: verify_jwt = false (supabase/config.toml:24-25) — deliberately, so the function can also be invoked by pg_cron using the service-role key (supabase/migrations/20260705000100_cron_scheduling.sql:40-47).
Function-level auth — CONFIRMED GAP: the handler checks only if (!auth.startsWith("Bearer ")) return json({ error: "unauthorized" }, 401); (supabase/functions/start-campaign/index.ts:91-92). It never calls supabase.auth.getUser() or otherwise validates the token's signature or the caller's identity/role. Any string prefixed Bearer satisfies this check. See carecall-openapi-findings.md §4 for the security assessment.
Request shape: either { "campaign_id": "<uuid>", "batch_size"?: number } (portal-triggered, one campaign) or { "sweep": true, "batch_size"?: number } (cron-triggered, all active campaigns).
Response shape: { notified, started, campaigns, skipped_outside_hours } (sweep) or { notified, started } per single-campaign call (index.ts:129, 132-237); { error } variants for 400/404/401.
Unhandled-exception gap (CONFIRMED): unlike the other four functions, start-campaign has no top-level try/catch around its handler body — only inner helpers (dialPatient, clinicLeadSeconds, clinicDisplayName, clinicSelfBooking) catch their own errors. An exception elsewhere (e.g. the clinic_within_calling_hours RPC call) is unhandled and surfaces as the Deno/Supabase Edge Runtime's generic 500, not the {error: "..."} JSON shape every other function returns.
Behavior (per campaign, evidence index.ts:132-238):
- Calling-hours gate via
clinic_within_calling_hoursRPC — outside window, campaign is skipped entirely (no SMS, no dial). - Phase B (runs first): dials any patient in
notifiedstatus whosedial_afterhas elapsed, excludingdo_not_call/inactive patients; anotifiedrow stuck past 30 minutes is flaggedneeds_humaninstead of retried indefinitely (staleness guard). - Phase A: for the next batch of
pending/due-callback_requestedpatients, sends a pre-call SMS (if the clinic hassms_precall_lead_seconds > 0,TELNYX_MESSAGING_PROFILE_IDis set, and the patient hassms_consent = true) and setsstatus = 'notified', or dials immediately if any of those conditions is false. - Optionally mints a self-booking link (
createBookingLink) and appends it to the pre-call SMS, gated per-clinic byclinics.self_booking_enabled. - Auto-completes the campaign (
status = 'completed') when no patients remain inpending/notified/calling/callback_requested. dialPatientcalls the Telnyx/callsAPI withanswering_machine_detection: "premium"and encodes campaign/patient context intoclient_state(base64 JSON) for the webhook to decode later.
Detail: telnyx-call-events
Gateway auth: verify_jwt = false (supabase/config.toml:28-29) — no Supabase JWT is present on Telnyx webhook calls.
Function-level auth — CONFIRMED GAP: no shared secret, signature, or any other check is present anywhere in supabase/functions/telnyx-call-events/index.ts. The handler parses req.json() and acts on any payload containing a data key. This is self-acknowledged in README.md under "Hardening before production": "Webhook signature verification: verify Telnyx's telnyx-signature-ed25519 header in telnyx-call-events instead of trusting any POST." — confirming the gap is known but (per the repository state analyzed) not yet closed.
Two logical sub-endpoints share one URL, distinguished by payload shape (looksLikeInsights(), index.ts:206-211):
- Call Control events (
event.data.event_type):call.answered→ logsstarted_at.call.machine.detection.ended→ onhuman/not_sure, starts the Telnyx AI Assistant on the call (ai_assistant_start) with per-call dynamic variables; onmachine, does nothing yet (waits for greeting end).call.machine.greeting.ended→ speaks a short PHI-free callback message.call.speak.ended→ marks the patientvoicemail, optionally sends an SMS fallback (with or without a self-booking link, gated by clinic settings and SMS consent), then hangs up.call.hangup→ finalizescall_logs(ended_at,duration_seconds); if noresultwas recorded, marksno_answer.- Response: always
{ ok: true }with HTTP 200, even on internal errors, so "Telnyx doesn't retry forever" (index.ts:173comment).
- AI Assistant Insights (heuristically detected): stores
transcript/summaryon the matchingcall_logsrow and — only as a fallback, never overwriting a result already set by the tool endpoints — infers a call outcome from the insight payload. The parser is explicitly tolerant of an unconfirmed payload shape (INFERRED, see capability inventory C14).
Detail: assistant-tools
Gateway auth: verify_jwt = false (supabase/config.toml:32-33).
Function-level auth: checkToolSecret(req) — exact string equality between the x-carecall-secret request header and the TOOL_WEBHOOK_SECRET environment secret (supabase/functions/_shared/lib.ts:36-38). Not a constant-time comparison (see findings §8). Matches telnyx/tools.json, which independently declares the same four tools with the same header requirement — this cross-source agreement raises confidence to CONFIRMED for the request parameter shapes.
Dispatch: ?tool= query parameter selects the handler; the call's identity (patient/campaign/clinic) is resolved server-side from call_logs by call_control_id, which Telnyx includes automatically in tool-call payloads — the tool arguments themselves carry no patient identifier, which is a deliberate anti-spoofing design (a caller cannot name an arbitrary patient to verify/book against; they can only act on the call context already established at dial time).
| Tool | Request params (from telnyx/tools.json, cross-checked against code) | Response (success) | Response (failure modes) |
|---|---|---|---|
verify_patient | stated_date_of_birth (string, required) | { match, locked, remaining_attempts } | after MAX_VERIFY_ATTEMPTS=2: { match:false, locked:true } and the patient is flagged verification_failed for human follow-up |
get_appointment_slots | granularity ("days"|"times", required), on_date?, from_date?, days? | { days:[{date,spoken}] } (max 3) or { times:[{slot_start,slot_end,spoken}] } (max 3) | 403 if not yet verified; { slots:[] } if no provider configured |
create_appointment | slot_start (ISO datetime, required) | { booked:true, appointment_id, spoken } | 403 unverified; 400 missing slot_start; {booked:false, reason:"slot_taken"} on exclusion-constraint conflict (Postgres 23P01); already-booked-elsewhere is turned into a success response, not an error (Postgres 23505) |
mark_outcome | outcome (enum: declined|callback_requested|wrong_number|needs_human, required), callback_after?, note? | { recorded:true } | 400 if outcome not in the allowed set |
All four require verification (except verify_patient itself) via ctx.verified, sourced from the call_logs row, not from the tool call arguments.
Detail: booking-api
Gateway auth: verify_jwt = false (supabase/config.toml:38-39) — intentionally public; the patient is not a portal user (booking-api/index.ts:1-4).
Function-level auth: possession of the opaque per-(campaign,patient) token embedded in the SMS-delivered URL (/book/<token>). The token is never stored in plaintext server-side — only its SHA-256 hash (resolveLink(), booking-api/index.ts:87-101; hashBookingToken, _shared/lib.ts:121-127). The web client additionally sends the Supabase project's anon key as apikey/Authorization: Bearer <anon key> to satisfy the Supabase gateway itself (web/src/lib/booking.ts:59-63) — this is platform-level plumbing, not application auth.
Abuse controls (CONFIRMED, code-evidenced):
- 2-attempt DOB verification lockout per link, mirroring the voice flow (
MAX_VERIFY_ATTEMPTS,booking-api/index.ts:20,182-208). - Per-IP rate limit of 10
verifycalls/minute via thebump_booking_ratePostgres function (booking-api/index.ts:159-165,supabase/migrations/20260706130000_self_booking_link.sql:91-108). - Invalid and expired tokens return the identical generic
{ state: "expired" }response (genericInvalid(),booking-api/index.ts:40-42) — deliberately no oracle distinguishing "wrong token" from "token expired."
| Action | Request body | Response (success) | Notes |
|---|---|---|---|
context | { token } | { clinic_name, appointment_type_label, timezone, state } | PHI-minimal; never returns patient name/DOB pre-verification |
verify | { token, stated_date_of_birth } | { verified:true, first_name, state:"ready" } | on failure: { verified:false, remaining_attempts } or {state:"locked"} after 2nd failure (also flags campaign_patients.status = needs_human); 429 on rate limit |
slots | { token, granularity:"days"|"times", on_date? } | { days:[{date,label,count}] } (≤10) or { times:[{slot_start,slot_end,label}] } | requires link state === "ready"; 403 otherwise |
book | { token, slot_start } | { booked:true, appointment_id, spoken, state:"booked" } | idempotent via idempotency_key = link:{id}:{slot_start}; slot-race → {booked:false, reason:"slot_taken"}; cross-channel race → returns the already-existing appointment as a success |
decline | { token, callback? } | { declined:true } | sets campaign_patients.status = 'callback_requested' |
Link lifecycle states (derived server-side, never client-supplied): needs_verification → ready → booked, or terminal expired/locked (linkState(), booking-api/index.ts:104-110).
Cross-cutting notes
- CORS:
admin-manage,start-campaign, andbooking-apiall setAccess-Control-Allow-Origin: *with method allow-lists ofPOST, OPTIONS(_shared/lib.ts:89-95,start-campaign/index.ts:37-43,booking-api/index.ts:26-32).assistant-toolsandtelnyx-call-eventsset no CORS headers at all (not needed — server-to-server webhook callers, not browser-originated). - Every function returns JSON with
Content-Type: application/json; no endpoint in this repo returns any other media type, and no endpoint accepts anything other thanapplication/jsonrequest bodies (or empty bodies forOPTIONS). - No endpoint in this repository implements pagination, aside from fixed
.limit(n)caps baked into individual queries (e.g.,list_userscaps at 1000 via the Supabase Admin API's ownperPage).
Capability Inventory
Capability Inventory
via Claude Code · 8/15/2026, 9:59:13 AM
CareCall — Capability Inventory
Workstream: OpenAPI Discovery (external engineering exercise)
Repository analyzed: CareCall (this repo), commit state as checked out at analysis time — see carecall-openapi-findings.md §11 for repo/commit identification caveats.
Analysis mode: Read-only. No application code, schema, or infrastructure was modified to produce this document.
0. Methodology
- Static discovery only. No deployed instance of CareCall was called; nothing here was verified by executing a live request. All claims trace to source files: React/TypeScript frontend (
web/src), Deno Supabase Edge Functions (supabase/functions/*), SQL migrations (supabase/migrations/*), and static integration artifacts (telnyx/tools.json,telnyx/assistant-instructions.md). - Two distinct API surfaces exist and are treated as separate capability categories:
- Bespoke Edge Functions — 5 hand-written Deno HTTP handlers under
supabase/functions/, each multiplexing several "actions" or "tools" behind one URL. This is CareCall's custom business-logic API and is the primary subject ofcarecall-openapi.yaml. - Direct PostgREST / Supabase-platform access — the React app also talks straight to Supabase's auto-generated
/rest/v1/{table}API and to Supabase Auth (/auth/v1/...) using the@supabase/supabase-jsclient, governed by Row-Level Security (RLS) policies rather than application code. This is real, externally-reachable API surface, but it is schema-generated (not bespoke CareCall logic) and is documented here at the capability/table level rather than as exhaustive per-table OpenAPI CRUD paths. Seecarecall-openapi-findings.md§5 for the scoping rationale.
- Bespoke Edge Functions — 5 hand-written Deno HTTP handlers under
- Capability boundary rule. Capabilities are grouped by externally meaningful outcome ("what can the system do"), not 1:1 with routes or table names, per workstream instructions. A capability may span multiple endpoints/tables; conversely, one endpoint's several actions may back several capabilities.
- Confidence classification (applied per capability):
- CONFIRMED — directly evidenced by reading the implementation (handler code, RLS policy, or schema) that performs the behavior.
- INFERRED — strongly suggested by evidence but not fully nailed down (e.g., behavior only encoded in a natural-language AI prompt, or a payload shape the code itself treats as variable/uncertain).
- UNKNOWN — cannot be reliably determined from this repository (e.g., production configuration, values only present in environment secrets, Telnyx-side behavior not observable from CareCall's code).
- Existing documentation was treated as a claim to verify, not ground truth.
docs/portal-ui-roles-spec_1.md,docs/self-booking-link-spec.md, anddocs/precall-sms-implementation-note.mdwere cross-checked against code; divergences are called out inline and in the findings report.
1. Capability Table
| # | Capability | Category | Confidence | Primary Evidence |
|---|---|---|---|---|
| C1 | Staff authentication & session lifecycle | Auth | CONFIRMED | web/src/pages/Login.tsx, web/src/lib/session.tsx |
| C2 | Forced password change / temporary-password flow | Auth | CONFIRMED | web/src/pages/Login.tsx:51-77, supabase/functions/admin-manage/index.ts:211-232 |
| C3 | Portal user administration (invite, role change, password reset, deactivate) | Admin | CONFIRMED | supabase/functions/admin-manage/index.ts |
| C4 | Clinic administration (create clinic; list/toggle clinics) | Admin | CONFIRMED | supabase/functions/admin-manage/index.ts:272-290, web/src/pages/admin/Clinics.tsx |
| C5 | Clinic settings configuration (hours, timezone, SMS toggles, greeting) | Configuration | CONFIRMED | web/src/pages/settings/ClinicSettings.tsx |
| C6 | Per-appointment-type AI assistant routing configuration | Configuration | CONFIRMED | web/src/pages/settings/ClinicSettings.tsx:38-60, supabase/migrations/20260706000000_appointment_type_assistants.sql |
| C7 | Clinician (provider) & weekly availability management | Scheduling config | CONFIRMED | web/src/pages/Clinicians.tsx |
| C8 | Patient roster management (manual add, CSV bulk import with dry-run) | Patient data | CONFIRMED | web/src/pages/Patients.tsx |
| C9 | Patient compliance flags (do-not-call, SMS consent, active/inactive) | Patient data / compliance | CONFIRMED | web/src/pages/PatientDetail.tsx:37-43, web/src/pages/Review.tsx:71-90 |
| C10 | Campaign authoring & lifecycle management (draft→scheduled→active→paused→completed) | Campaign | CONFIRMED | web/src/pages/CampaignNew.tsx, web/src/pages/CampaignDetail.tsx, supabase/migrations/20260705000000_portal_roles.sql:72-95 |
| C11 | Campaign patient queue assignment | Campaign | CONFIRMED | web/src/pages/Patients.tsx:93-100, web/src/pages/CampaignNew.tsx:80-91 |
| C12 | Outbound dialer execution (batch dial, pre-call SMS two-phase queue, calling-hours gate, DNC/consent gate, cron sweep) | Campaign execution | CONFIRMED | supabase/functions/start-campaign/index.ts |
| C13 | AI voice call orchestration (AMD gate, assistant start, voicemail handling, hangup finalize) | Voice / telephony | CONFIRMED | supabase/functions/telnyx-call-events/index.ts:49-174 |
| C14 | Post-call insights ingestion (transcript, summary, outcome fallback) | Voice / telephony | INFERRED | supabase/functions/telnyx-call-events/index.ts:199-278 — parser is deliberately "tolerant" of an unconfirmed Telnyx payload shape (see code comment at line 200-205) |
| C15 | Patient identity verification (voice channel) | AI tool backend | CONFIRMED | supabase/functions/assistant-tools/index.ts:66-91 |
| C16 | Appointment slot discovery, progressive disclosure (voice channel) | AI tool backend | CONFIRMED | supabase/functions/assistant-tools/index.ts:99-153 |
| C17 | Appointment booking, idempotent (voice channel) | AI tool backend | CONFIRMED | supabase/functions/assistant-tools/index.ts:159-230 |
| C18 | Call outcome recording (voice channel) | AI tool backend | CONFIRMED | supabase/functions/assistant-tools/index.ts:235-254 |
| C19 | Underlying slot-generation engine (shared by voice, web-booking, and staff dashboard) | Scheduling core | CONFIRMED | supabase/migrations/20260704000000_init.sql:124-161 (function get_available_slots) |
| C20 | Public self-service booking (context/verify/slots/book/decline via SMS-delivered opaque link) | Public / patient-facing | CONFIRMED | supabase/functions/booking-api/index.ts |
| C21 | Cross-channel double-booking prevention (voice vs. web-link vs. concurrent voice) | Scheduling integrity | CONFIRMED | supabase/migrations/20260706130000_self_booking_link.sql:49-57, handling of Postgres error codes 23P01/23505 in both assistant-tools/index.ts:189-220 and booking-api/index.ts:332-357 |
| C22 | Review-queue human escalation workflow (resolve / requeue / remove / do-not-call) | Human-in-the-loop | CONFIRMED | web/src/pages/Review.tsx |
| C23 | Call history & transcript review | Reporting | CONFIRMED | web/src/pages/CallHistory.tsx, web/src/pages/PatientDetail.tsx:85-100 |
| C24 | Operational dashboard & reporting aggregation (campaign stats, live feed, slot calendar, patient search) | Reporting | CONFIRMED | web/src/pages/Dashboard.tsx |
| C25 | Audit trail (automatic DB triggers + explicit admin-manage writes) | Compliance / observability | CONFIRMED | supabase/migrations/20260705000000_portal_roles.sql:109-119,275-309, supabase/functions/_shared/lib.ts:69-87 |
| C26 | User profile self-service (display name, avatar upload, own password change) | Account | CONFIRMED | web/src/pages/settings/Profile.tsx |
| C27 | Role-based access control enforcement (Admin / Clinic Admin / Staff, clinic-scoped) | Security | CONFIRMED | supabase/migrations/20260705000000_portal_roles.sql:122-269, web/src/lib/session.tsx:80-98 |
| C28 | Direct data-access layer (PostgREST CRUD over 10 clinic-scoped tables, governed by RLS) | Data access | CONFIRMED | see §2 below |
| C29 | AI conversational behavior (identity-verification-first, progressive slot disclosure, no-pressure decline handling) | AI behavior / prompt | INFERRED | telnyx/assistant-instructions.md — this is a natural-language prompt, not executable code; actual runtime behavior of the Telnyx-hosted LLM cannot be confirmed from this repository |
| C30 | Booking-link abuse mitigation (per-token verification lockout, per-IP rate limit, no invalid/expired oracle) | Security | CONFIRMED | supabase/functions/booking-api/index.ts:38-42,156-165, supabase/migrations/20260706130000_self_booking_link.sql:81-108 |
2. Capability C28 detail — Data Access Layer
The frontend performs direct supabase-js table operations (bypassing any bespoke handler) against the following tables. Evidence: web/src/** grep for .from("<table>"), cross-referenced against RLS policies in the migrations.
| Table | Operations observed in frontend | Governing RLS (evidence) |
|---|---|---|
clinics | select, update | supabase/migrations/20260706140000_single_clinic_read.sql (read: any authenticated user, in a deliberate single-clinic-build relaxation — see findings); write: Admin only (20260705000000_portal_roles.sql:173-177) |
patients | select, insert, update, upsert | 20260705000000_portal_roles.sql:182-186 — read: own clinic or Admin; write: Clinic Admin+ |
providers | select, insert, update | 20260705000000_portal_roles.sql:191-195 — Clinic Admin+ write, clinic-scoped read |
provider_availability | select, insert, update, delete | 20260705000000_portal_roles.sql:200-213 — scoped via parent provider's clinic |
campaigns | select, insert, update | 20260705000000_portal_roles.sql:218-222 — read: clinic-scoped; write: Clinic Admin+ (Staff explicitly excluded per spec D1) |
campaign_patients | select, insert/upsert, update, delete | 20260705000000_portal_roles.sql:227-241 — read/write scoped via parent campaign's clinic; notably not restricted to Clinic Admin+, so Staff can update/delete (used by the review queue) |
appointments | select | 20260705000000_portal_roles.sql:246-259 — scoped via provider's clinic; app never writes appointments directly from the frontend (writes are server-side, service-role, from assistant-tools/booking-api) |
call_logs | select | 20260705000000_portal_roles.sql:263-264 — read-only in RLS; clinic-scoped |
audit_log | select | 20260705000000_portal_roles.sql:267-269 — read-only; clinic-scoped or Admin |
appointment_type_assistants | select, upsert | 20260706000000_appointment_type_assistants.sql:33-39 — Clinic Admin+ write, clinic-scoped read |
storage.objects (avatars bucket) | insert (upload), public read | 20260705000000_portal_roles.sql:375-388 |
Not directly written by the frontend under any evidence found: booking_links (RLS read-only for staff; all writes are service-role from start-campaign/telnyx-call-events/booking-api), booking_rate_limits (server-only via RPC).
This table-level view is intentionally not expanded into per-table OpenAPI CRUD paths in carecall-openapi.yaml — see carecall-openapi-findings.md §5 ("Scope of the OpenAPI document") for why, and how to extend it later if full PostgREST documentation is wanted.
3. Dependencies & cross-cutting constraints
- Every clinic-scoped write ultimately depends on
app_metadata.role/app_metadata.clinic_id, which are set exclusively byadmin-manage(C3) and never client-writable (supabase/functions/admin-manage/index.ts:21-25comment, confirmed structurally by every other handler reading but never writing these fields). - C12 (dialer), C13 (call orchestration), C15-C18 (AI tools), and C20 (public booking) all share the single slot-generation function
get_available_slots(C19) and the same idempotency/exclusion-constraint pattern (C21) — a change to slot generation or the exclusion constraint affects all four. - C29 (AI conversational behavior) is a soft dependency: the tool contracts in C15-C18 are the only backstop; the Telnyx-hosted model's actual real-time behavior is not verifiable from source and is explicitly marked INFERRED.
- C6 (assistant routing) determines which Telnyx AI Assistant ID answers a call; this indirectly gates which system prompt (C29) applies, but the mapping is only "resolved" at campaign-creation time and snapshotted onto the campaign row (
campaigns.telnyx_assistant_id) — a later change to the mapping does not retroactively affect in-flight campaigns (supabase/migrations/20260706000000_appointment_type_assistants.sql:1-11comment).
4. Notable uncertainties surfaced during capability discovery
- C14 (insights ingestion): the code's own comments (
telnyx-call-events/index.ts:199-205) state the Telnyx Insights payload shape "varies by configuration" and the parser is deliberately tolerant/best-effort. No sample payload or schema was found in the repo. Marked INFERRED. - C29 (AI behavior): entirely prompt-driven; no code enforces steps 1, 3 (open-ended day question), or 5 (no-pressure callback offer) described in
telnyx/assistant-instructions.md. Only the tool calls the prompt is supposed to trigger (C15-C18) are code-enforced. - C4/clinic read RLS:
docs/portal-ui-roles-spec_1.md§4.6 documents a clinic-scoped read policy (is_admin() or id = jwt_clinic_id()), but a later migration (20260706140000_single_clinic_read.sql) intentionally broadensclinicsread to any authenticated user "for now" (single-clinic build). The spec document is therefore stale on this one point; code wins per workstream rules. Confirmed via direct migration diff, not inferred. - C16/C19/C20 timezone-aware slot generation:
assistant-toolsandbooking-apiboth passp_tz, but the only checked-inget_available_slotsdefinition accepts onlyp_provider_id,p_from,p_days, andp_limit(20260704000000_init.sql:124-129). A comment in20260706130000_self_booking_link.sql:59-62refers to20260705000000_slot_tz.sql, but that migration is absent. CONFIRMED repository inconsistency; whether the deployed database has the missing overload is UNKNOWN.