← CareCall — Can AI Reconstruct an API Contract From Legacy Code?

OpenAPI Discovery

draft

5/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.1 nullable-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-campaign accepts any bearer-prefixed value — no real authentication
  • Telnyx event webhook has no signature verification
  • Checked-in slot RPC lacks the p_tz parameter 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-campaign has 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

View Assessment

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

CapabilityEvidenceWhat evidence establishesConfidence
C1 Staff authentication/sessionweb/src/pages/Login.tsx:21-77; web/src/lib/session.tsx:20-77Email/password session, password update, session metadataCONFIRMED
C2 Forced password changeweb/src/pages/Login.tsx:51-77; supabase/functions/admin-manage/index.ts:207-232Forced-change flag handling and self-clear actionCONFIRMED
C3 User administrationsupabase/functions/admin-manage/index.ts:31-56,91-268Eight action dispatch and role/target rulesCONFIRMED
C4 Clinic administrationsupabase/functions/admin-manage/index.ts:272-290; web/src/pages/admin/Clinics.tsx:12-32Create, list, and activation toggle pathsCONFIRMED
C5 Clinic settingsweb/src/pages/settings/ClinicSettings.tsx:20-86; supabase/migrations/20260705000000_portal_roles.sql:11-38Settings reads/writes and schemaCONFIRMED
C6 Appointment-type assistant routingweb/src/pages/settings/ClinicSettings.tsx:28-60; supabase/migrations/20260706000000_appointment_type_assistants.sql:16-45Mapping table, RLS, campaign snapshotCONFIRMED
C7 Providers/availabilityweb/src/pages/Clinicians.tsx:17-60; supabase/migrations/20260704000000_init.sql:26-32,70-77Provider and weekly availability CRUD/schemaCONFIRMED
C8 Patient roster/importweb/src/pages/Patients.tsx:33-100; supabase/migrations/20260704000000_init.sql:34-45Direct roster CRUD, CSV processing, core schemaCONFIRMED
C9 Patient compliance flagsweb/src/pages/PatientDetail.tsx:37-43; web/src/pages/Review.tsx:71-90; supabase/migrations/20260706120000_precall_sms_and_spec_v12.sql:35-40DNC/active/SMS-consent mutationCONFIRMED
C10 Campaign authoring/lifecycleweb/src/pages/CampaignNew.tsx:22-91; web/src/pages/CampaignDetail.tsx:34-76; supabase/migrations/20260705000000_portal_roles.sql:72-95Create/edit/status transitions and status enumCONFIRMED
C11 Campaign queue assignmentweb/src/pages/Patients.tsx:93-100; web/src/pages/CampaignNew.tsx:80-91Patient-to-campaign insert/upsertCONFIRMED
C12 Outbound executionsupabase/functions/start-campaign/index.ts:88-237,353-398; supabase/migrations/20260705000100_cron_scheduling.sql:20-55Sweep/single execution, gates, SMS/dial, cronCONFIRMED
C13 Call orchestrationsupabase/functions/telnyx-call-events/index.ts:49-190AMD, assistant start, voicemail/SMS, hangupCONFIRMED
C14 Insights ingestionsupabase/functions/telnyx-call-events/index.ts:199-278Shape-sniffed summary/transcript/outcome updatesINFERRED
C15 Voice verificationsupabase/functions/assistant-tools/index.ts:49-91Call-bound DOB comparison and lockoutCONFIRMED
C16 Voice slot discoverysupabase/functions/assistant-tools/index.ts:99-153Verified-call gate and 3-day/time disclosure; RPC mismatch notedCONFIRMED
C17 Voice appointment bookingsupabase/functions/assistant-tools/index.ts:159-230; supabase/migrations/20260706130000_self_booking_link.sql:49-57Idempotency, insert, conflict handlingCONFIRMED
C18 Voice outcome recordingsupabase/functions/assistant-tools/index.ts:235-254Allowed outcomes and side effectsCONFIRMED
C19 Slot enginesupabase/migrations/20260704000000_init.sql:120-161Four-argument availability-minus-bookings SQL functionCONFIRMED
C20 Public bookingsupabase/functions/booking-api/index.ts:112-132,138-428; web/src/lib/booking.ts:55-83Five actions and patient-facing clientCONFIRMED
C21 Double-booking preventionsupabase/migrations/20260704000000_init.sql:79-94; supabase/migrations/20260706130000_self_booking_link.sql:49-57Exclusion and partial unique constraintsCONFIRMED
C22 Human review queueweb/src/pages/Review.tsx:16-91Resolve, requeue, remove, DNC flowsCONFIRMED
C23 Call history/transcriptsweb/src/pages/CallHistory.tsx:17-42; web/src/pages/PatientDetail.tsx:24-30Log/transcript readsCONFIRMED
C24 Dashboard/reportingweb/src/pages/Dashboard.tsx:54-203; supabase/migrations/20260706120000_precall_sms_and_spec_v12.sql:59-79Counts, feed, search, slots, stats viewCONFIRMED
C25 Audit trailsupabase/functions/_shared/lib.ts:69-87; supabase/migrations/20260705000000_portal_roles.sql:271-309Explicit admin audit plus row triggersCONFIRMED
C26 Profile self-serviceweb/src/pages/settings/Profile.tsx:16-48; supabase/migrations/20260705000000_portal_roles.sql:368-388Metadata/password/avatar operationsCONFIRMED
C27 RBAC/clinic scopesupabase/functions/_shared/lib.ts:44-67; supabase/migrations/20260705000000_portal_roles.sql:120-269Claims, roles, RLS policiesCONFIRMED
C28 Direct data accessweb/src/pages/**/*.tsx; supabase/migrations/20260705000000_portal_roles.sql:151-269SPA uses generated PostgREST surface under RLSCONFIRMED
C29 AI conversational behaviortelnyx/assistant-instructions.md:1-159; telnyx/tools.json:1-153Intended prompt/tool behavior, not hosted runtimeINFERRED
C30 Booking abuse controlssupabase/functions/booking-api/index.ts:38-42,86-110,156-209; supabase/migrations/20260706130000_self_booking_link.sql:77-108Generic invalid response, token lockout, IP counterCONFIRMED

OpenAPI operation evidence

Logical operationEvidenceWhat evidence establishesConfidence
admin-manage/list_userssupabase/functions/admin-manage/index.ts:91-110Role gate, scope, response fieldsCONFIRMED
admin-manage/create_usersupabase/functions/admin-manage/index.ts:113-149Validation, role rules, temp password, auditCONFIRMED
admin-manage/set_rolesupabase/functions/admin-manage/index.ts:152-180Admin-only role/clinic changeCONFIRMED
admin-manage/reset_passwordsupabase/functions/admin-manage/index.ts:184-204Scoped reset and responseCONFIRMED
admin-manage/clear_force_password_changesupabase/functions/admin-manage/index.ts:207-232Self-only idempotent flag clearCONFIRMED
admin-manage/deactivate_usersupabase/functions/admin-manage/index.ts:235-260Ban, no-self, target scopeCONFIRMED
admin-manage/reactivate_usersupabase/functions/admin-manage/index.ts:235-260Unban and target scopeCONFIRMED
admin-manage/create_clinicsupabase/functions/admin-manage/index.ts:272-290Admin-only create and responseCONFIRMED
start-campaign/singlesupabase/functions/start-campaign/index.ts:88-130,132-237Header check, campaign lookup, queue executionCONFIRMED
start-campaign/sweepsupabase/functions/start-campaign/index.ts:97-130; supabase/migrations/20260705000100_cron_scheduling.sql:40-47All-active selection and cron requestCONFIRMED
telnyx-call-events/call-controlsupabase/functions/telnyx-call-events/index.ts:49-174Event families, side effects, always-200CONFIRMED
telnyx-call-events/insightssupabase/functions/telnyx-call-events/index.ts:199-278Tolerant payload parsing and writesINFERRED
assistant-tools/verify_patientsupabase/functions/assistant-tools/index.ts:25-47,66-91; telnyx/tools.json:1-31Selector, secret, args, responsesCONFIRMED
assistant-tools/get_appointment_slotssupabase/functions/assistant-tools/index.ts:99-153; telnyx/tools.json:32-75Verification gate, args, response limitsCONFIRMED
assistant-tools/create_appointmentsupabase/functions/assistant-tools/index.ts:159-230; telnyx/tools.json:76-103Required slot, insert, conflicts, responseCONFIRMED
assistant-tools/mark_outcomesupabase/functions/assistant-tools/index.ts:235-254; telnyx/tools.json:104-153Outcome enum and state/log writesCONFIRMED
booking-api/contextsupabase/functions/booking-api/index.ts:134-149PHI-minimal context/stateCONFIRMED
booking-api/verifysupabase/functions/booking-api/index.ts:151-209Rate limit, DOB, attempts, lockCONFIRMED
booking-api/slotssupabase/functions/booking-api/index.ts:211-276Ready-state gate and day/time shapesCONFIRMED
booking-api/booksupabase/functions/booking-api/index.ts:278-403Validation, idempotency, insert/conflictsCONFIRMED
booking-api/declinesupabase/functions/booking-api/index.ts:405-428Callback requeue mutationCONFIRMED
rpc/get_available_slotssupabase/migrations/20260704000000_init.sql:124-161; web/src/pages/Dashboard.tsx:135Four-argument SQL signature and direct callCONFIRMED

Cross-cutting security evidence

ConcernEvidenceWhat evidence establishesConfidence
Edge gateway configurationsupabase/config.toml:9-39JWT enabled only for admin-manageCONFIRMED
Portal JWT resolutionsupabase/functions/_shared/lib.ts:50-67Bearer validation and app-metadata extractionCONFIRMED
Start-campaign auth bypasssupabase/functions/start-campaign/index.ts:88-113Only bearer prefix is testedCONFIRMED
Unsigned Telnyx webhooksupabase/functions/telnyx-call-events/index.ts:49-174; README.md:172-176No verification and explicit hardening TODOCONFIRMED
Tool shared secretsupabase/functions/_shared/lib.ts:35-38; supabase/functions/assistant-tools/index.ts:25-27Static header equalityCONFIRMED
Clinic/role RLSsupabase/migrations/20260705000000_portal_roles.sql:120-269Per-table read/write policiesCONFIRMED
Broad clinic readssupabase/migrations/20260706140000_single_clinic_read.sql:1-21Latest policy is using (true)CONFIRMED
Missing timezone migrationsupabase/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-62Callers pass absent parameter; comment names absent fileCONFIRMED 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 uses supabase-js for Auth, PostgREST table/view access, RPC, and avatar Storage.
  • Custom HTTP API: supabase/functions/ contains five Deno handlers. admin-manage and start-campaign are portal/cron-facing; assistant-tools and telnyx-call-events are Telnyx-facing; booking-api is 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_cron invokes 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-manage actions, two start-campaign modes, 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 concrete POST paths.
  • 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

SurfaceAuthenticationAuthorization / scopeAssessment
admin-manageSupabase JWT at gateway and auth.getUser in handleradmin, clinic_admin, staff; action and target-clinic checksStrongest custom surface; CONFIRMED
start-campaignGateway JWT disabled; handler checks only that header starts with Bearer None; service-role queries accept arbitrary campaign ID or global sweepCritical gap: any bearer text passes
assistant-toolsStatic x-carecall-secret equality checkCall context derives patient/campaign from call_control_id; verification gates slots/book, but not mark_outcomeShared-secret boundary; no replay/rotation evidence
telnyx-call-eventsNoneNone; trusts event payload and decoded client stateCritical gap: no Telnyx signature verification
booking-apiOpaque token in JSON body; gateway JWT offLink state, DOB verification, lockout; verify alone has per-IP rate limitingCapability token; possession plus DOB unlocks booking
PostgREST/StorageSupabase user JWT/anon key as applicablePostgreSQL RLS; app metadata supplies role/clinicGenerally 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.yaml is OpenAPI 3.1.0 and documents six concrete POST operations. Multiplexed logical actions are modeled through oneOf, discriminators, and selector enums.
  • Parser validation was performed with @redocly/cli lint (v2, run via npx, 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 offending description (an HTTP 404 description for start-campaign). Second pass parsed but reported 10 schema errors, all nullable: true used in type/format shorthand objects — invalid in OpenAPI 3.1, whose schemas are JSON Schema 2020-12 and use type: [X, "null"] instead of the 3.0-era nullable keyword. All 10 were corrected. A missing security: [] on the public booking-api operation was also flagged and added explicitly (it has no bearer/apiKey scheme — auth is an opaque token in the body — but Redocly's security-defined rule 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.license is absent — there is no license for this internal discovery artifact; (2) telnyx-call-events has no documented 4XX response — confirmed by code (index.ts:173 comment) that the handler always returns 200, 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 a p_tz fifth parameter, despite assistant-tools and booking-api passing 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)

  1. Slot RPC mismatch — high. assistant-tools and booking-api pass p_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.
  2. Public booking CORS mismatch — high for browser use. The client sends apikey and Authorization; booking-api preflight allows only content-type. Cross-origin browser requests may therefore be blocked.
  3. Admin scope errors become 500 — medium. requireTargetInScope throws for not-found/forbidden targets and the outer catch returns 500, rather than 403/404.
  4. start-campaign batch validation — medium. batch_size is accepted without type/range validation before being passed to query limits.
  5. Arbitrary booking time — medium. booking handlers accept slot_start and rely on overlap/uniqueness constraints; they do not prove the submitted time was one returned by get_available_slots or fits configured availability.
  6. Documentation drift — medium. README text says webhook/tool endpoints are protected by a shared-secret/always-200 pattern, but telnyx-call-events has no secret or signature check. README also describes older cron/config details in places.
  7. Clinic read policy drift — low/intentional. the latest migration broadens clinic reads beyond the earlier role specification.
  8. start-campaign has no top-level exception handling — medium. Every other Edge Function wraps its handler body in try/catch and returns a JSON {error} on failure. start-campaign/index.ts does not (confirmed by grep: only inner helpers like dialPatient catch locally). An exception anywhere in the main request path — e.g. the clinic_within_calling_hours RPC — 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 (including pg_cron, whose retry/alerting behavior on a generic 500 is UNKNOWN).

8. Security concerns (implementation findings)

  • Critical: campaign execution authorization bypass. Because verify_jwt=false and 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

  1. Compare the six concrete paths and 22 logical operations against the human benchmark.
  2. Inspect the deployed get_available_slots signature and recover/approve the missing timezone migration.
  3. Contract-test every selector/action, status code, and response variant with synthetic data.
  4. Review start-campaign authentication and Telnyx signature verification before any real use.
  5. Validate RLS with a role/tenant matrix, especially appointments, campaign-patient mutation, clinic reads, and avatar paths.
  6. Confirm Telnyx webhook/tool payload schemas and retry semantics.
  7. 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

MethodRoutePurposeAuthEvidenceConfidence
POST/functions/v1/admin-manage (action=list_users)List portal users (own clinic, or all for Admin)Supabase user JWT (gateway-verified) + role checksupabase/functions/admin-manage/index.ts:91-110CONFIRMED
POST/functions/v1/admin-manage (action=create_user)Invite a new portal user with a temporary passwordSupabase user JWT + role/scope checksupabase/functions/admin-manage/index.ts:113-149CONFIRMED
POST/functions/v1/admin-manage (action=set_role)Change a user's role/clinicSupabase user JWT, Admin onlysupabase/functions/admin-manage/index.ts:152-181CONFIRMED
POST/functions/v1/admin-manage (action=reset_password)Issue a new temporary password for a userSupabase user JWT + scope checksupabase/functions/admin-manage/index.ts:184-204CONFIRMED
POST/functions/v1/admin-manage (action=clear_force_password_change)Caller clears their own forced-password-change flagSupabase user JWT (self only)supabase/functions/admin-manage/index.ts:211-232CONFIRMED
POST/functions/v1/admin-manage (action=deactivate_user)Ban a user (blocks token refresh)Supabase user JWT + scope check, no selfsupabase/functions/admin-manage/index.ts:235-260CONFIRMED
POST/functions/v1/admin-manage (action=reactivate_user)Lift a user banSupabase user JWT + scope checksupabase/functions/admin-manage/index.ts:235-260CONFIRMED
POST/functions/v1/admin-manage (action=create_clinic)Create a new clinicSupabase user JWT, Admin onlysupabase/functions/admin-manage/index.ts:272-290CONFIRMED
POST/functions/v1/start-campaignAdvance one campaign's dial/notify queue by one batchPresence-only bearer header check (see note)supabase/functions/start-campaign/index.ts:88-130CONFIRMED
POST/functions/v1/start-campaign (sweep:true)Advance every active campaign (cron heartbeat)Same as abovesupabase/functions/start-campaign/index.ts:98-102CONFIRMED
POST/functions/v1/telnyx-call-eventsTelnyx Call Control webhook: AMD result, greeting-ended, speak-ended, hangupNonesupabase/functions/telnyx-call-events/index.ts:49-174CONFIRMED
POST/functions/v1/telnyx-call-eventsTelnyx AI Assistant "Insights" webhook (same URL, shape-sniffed)Nonesupabase/functions/telnyx-call-events/index.ts:206-278INFERRED (payload contract)
POST/functions/v1/assistant-tools?tool=verify_patientVoice call: compare stated DOB server-sideShared secret header x-carecall-secretsupabase/functions/assistant-tools/index.ts:66-91CONFIRMED
POST/functions/v1/assistant-tools?tool=get_appointment_slotsVoice call: fetch available days/timesShared secret headersupabase/functions/assistant-tools/index.ts:99-153CONFIRMED
POST/functions/v1/assistant-tools?tool=create_appointmentVoice call: book the confirmed slotShared secret headersupabase/functions/assistant-tools/index.ts:159-230CONFIRMED
POST/functions/v1/assistant-tools?tool=mark_outcomeVoice call: record non-booking outcomeShared secret headersupabase/functions/assistant-tools/index.ts:235-254CONFIRMED
POST/functions/v1/booking-api?action=contextPublic: resolve a booking-link token to clinic name / stateOpaque token in request bodysupabase/functions/booking-api/index.ts:138-149CONFIRMED
POST/functions/v1/booking-api?action=verifyPublic: DOB verification for a booking linkOpaque token + per-IP rate limitsupabase/functions/booking-api/index.ts:156-209CONFIRMED
POST/functions/v1/booking-api?action=slotsPublic: available days/times for a verified linkOpaque token, link must be readysupabase/functions/booking-api/index.ts:216-276CONFIRMED
POST/functions/v1/booking-api?action=bookPublic: book the confirmed slotOpaque token, link must be readysupabase/functions/booking-api/index.ts:288-361CONFIRMED
POST/functions/v1/booking-api?action=declinePublic: "call me instead" — requeue for callbackOpaque tokensupabase/functions/booking-api/index.ts:409-428CONFIRMED
RPC (PostgREST)POST /rest/v1/rpc/get_available_slotsBespoke slot-generation function, called directly by the staff dashboard; the checked-in signature has four parametersSupabase 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:135CONFIRMED

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).

ActionCaller requirementNotable request fieldsNotable response fieldsSide effects
list_usersclinic_admin or admin—{ users: [{id,email,role,clinic_id,deactivated,last_sign_in_at,created_at}] }none (read)
create_userclinic_admin/admin; non-admin forced to role=staff and own clinicemail, role, clinic_id?{ ok, user_id, temporary_password }creates auth.users row; writes audit_log (user_created)
set_roleadmin only; blocks self-demotion if caller is the last adminuser_id, role, clinic_id?{ ok, note }updates app_metadata; writes audit_log (user_role_changed)
reset_passwordclinic_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_changeany 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_userclinic_admin/admin; cannot target self; target must be in scopeuser_id{ ok }sets ban_duration: 876000h; writes audit_log (user_deactivated)
reactivate_userclinic_admin/admin; target must be in scopeuser_id{ ok }clears ban; writes audit_log (user_reactivated)
create_clinicadmin onlyname, 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):

  1. Calling-hours gate via clinic_within_calling_hours RPC — outside window, campaign is skipped entirely (no SMS, no dial).
  2. Phase B (runs first): dials any patient in notified status whose dial_after has elapsed, excluding do_not_call/inactive patients; a notified row stuck past 30 minutes is flagged needs_human instead of retried indefinitely (staleness guard).
  3. Phase A: for the next batch of pending/due-callback_requested patients, sends a pre-call SMS (if the clinic has sms_precall_lead_seconds > 0, TELNYX_MESSAGING_PROFILE_ID is set, and the patient has sms_consent = true) and sets status = 'notified', or dials immediately if any of those conditions is false.
  4. Optionally mints a self-booking link (createBookingLink) and appends it to the pre-call SMS, gated per-clinic by clinics.self_booking_enabled.
  5. Auto-completes the campaign (status = 'completed') when no patients remain in pending/notified/calling/callback_requested.
  6. dialPatient calls the Telnyx /calls API with answering_machine_detection: "premium" and encodes campaign/patient context into client_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):

  1. Call Control events (event.data.event_type):
    • call.answered → logs started_at.
    • call.machine.detection.ended → on human/not_sure, starts the Telnyx AI Assistant on the call (ai_assistant_start) with per-call dynamic variables; on machine, does nothing yet (waits for greeting end).
    • call.machine.greeting.ended → speaks a short PHI-free callback message.
    • call.speak.ended → marks the patient voicemail, optionally sends an SMS fallback (with or without a self-booking link, gated by clinic settings and SMS consent), then hangs up.
    • call.hangup → finalizes call_logs (ended_at, duration_seconds); if no result was recorded, marks no_answer.
    • Response: always { ok: true } with HTTP 200, even on internal errors, so "Telnyx doesn't retry forever" (index.ts:173 comment).
  2. AI Assistant Insights (heuristically detected): stores transcript/summary on the matching call_logs row 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).

ToolRequest params (from telnyx/tools.json, cross-checked against code)Response (success)Response (failure modes)
verify_patientstated_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_slotsgranularity ("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_appointmentslot_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_outcomeoutcome (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 verify calls/minute via the bump_booking_rate Postgres 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."
ActionRequest bodyResponse (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, and booking-api all set Access-Control-Allow-Origin: * with method allow-lists of POST, OPTIONS (_shared/lib.ts:89-95, start-campaign/index.ts:37-43, booking-api/index.ts:26-32). assistant-tools and telnyx-call-events set 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 than application/json request bodies (or empty bodies for OPTIONS).
  • No endpoint in this repository implements pagination, aside from fixed .limit(n) caps baked into individual queries (e.g., list_users caps at 1000 via the Supabase Admin API's own perPage).

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

  1. 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).
  2. 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 of carecall-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-js client, 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. See carecall-openapi-findings.md §5 for the scoping rationale.
  3. 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.
  4. 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).
  5. 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, and docs/precall-sms-implementation-note.md were cross-checked against code; divergences are called out inline and in the findings report.

1. Capability Table

#CapabilityCategoryConfidencePrimary Evidence
C1Staff authentication & session lifecycleAuthCONFIRMEDweb/src/pages/Login.tsx, web/src/lib/session.tsx
C2Forced password change / temporary-password flowAuthCONFIRMEDweb/src/pages/Login.tsx:51-77, supabase/functions/admin-manage/index.ts:211-232
C3Portal user administration (invite, role change, password reset, deactivate)AdminCONFIRMEDsupabase/functions/admin-manage/index.ts
C4Clinic administration (create clinic; list/toggle clinics)AdminCONFIRMEDsupabase/functions/admin-manage/index.ts:272-290, web/src/pages/admin/Clinics.tsx
C5Clinic settings configuration (hours, timezone, SMS toggles, greeting)ConfigurationCONFIRMEDweb/src/pages/settings/ClinicSettings.tsx
C6Per-appointment-type AI assistant routing configurationConfigurationCONFIRMEDweb/src/pages/settings/ClinicSettings.tsx:38-60, supabase/migrations/20260706000000_appointment_type_assistants.sql
C7Clinician (provider) & weekly availability managementScheduling configCONFIRMEDweb/src/pages/Clinicians.tsx
C8Patient roster management (manual add, CSV bulk import with dry-run)Patient dataCONFIRMEDweb/src/pages/Patients.tsx
C9Patient compliance flags (do-not-call, SMS consent, active/inactive)Patient data / complianceCONFIRMEDweb/src/pages/PatientDetail.tsx:37-43, web/src/pages/Review.tsx:71-90
C10Campaign authoring & lifecycle management (draft→scheduled→active→paused→completed)CampaignCONFIRMEDweb/src/pages/CampaignNew.tsx, web/src/pages/CampaignDetail.tsx, supabase/migrations/20260705000000_portal_roles.sql:72-95
C11Campaign patient queue assignmentCampaignCONFIRMEDweb/src/pages/Patients.tsx:93-100, web/src/pages/CampaignNew.tsx:80-91
C12Outbound dialer execution (batch dial, pre-call SMS two-phase queue, calling-hours gate, DNC/consent gate, cron sweep)Campaign executionCONFIRMEDsupabase/functions/start-campaign/index.ts
C13AI voice call orchestration (AMD gate, assistant start, voicemail handling, hangup finalize)Voice / telephonyCONFIRMEDsupabase/functions/telnyx-call-events/index.ts:49-174
C14Post-call insights ingestion (transcript, summary, outcome fallback)Voice / telephonyINFERREDsupabase/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)
C15Patient identity verification (voice channel)AI tool backendCONFIRMEDsupabase/functions/assistant-tools/index.ts:66-91
C16Appointment slot discovery, progressive disclosure (voice channel)AI tool backendCONFIRMEDsupabase/functions/assistant-tools/index.ts:99-153
C17Appointment booking, idempotent (voice channel)AI tool backendCONFIRMEDsupabase/functions/assistant-tools/index.ts:159-230
C18Call outcome recording (voice channel)AI tool backendCONFIRMEDsupabase/functions/assistant-tools/index.ts:235-254
C19Underlying slot-generation engine (shared by voice, web-booking, and staff dashboard)Scheduling coreCONFIRMEDsupabase/migrations/20260704000000_init.sql:124-161 (function get_available_slots)
C20Public self-service booking (context/verify/slots/book/decline via SMS-delivered opaque link)Public / patient-facingCONFIRMEDsupabase/functions/booking-api/index.ts
C21Cross-channel double-booking prevention (voice vs. web-link vs. concurrent voice)Scheduling integrityCONFIRMEDsupabase/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
C22Review-queue human escalation workflow (resolve / requeue / remove / do-not-call)Human-in-the-loopCONFIRMEDweb/src/pages/Review.tsx
C23Call history & transcript reviewReportingCONFIRMEDweb/src/pages/CallHistory.tsx, web/src/pages/PatientDetail.tsx:85-100
C24Operational dashboard & reporting aggregation (campaign stats, live feed, slot calendar, patient search)ReportingCONFIRMEDweb/src/pages/Dashboard.tsx
C25Audit trail (automatic DB triggers + explicit admin-manage writes)Compliance / observabilityCONFIRMEDsupabase/migrations/20260705000000_portal_roles.sql:109-119,275-309, supabase/functions/_shared/lib.ts:69-87
C26User profile self-service (display name, avatar upload, own password change)AccountCONFIRMEDweb/src/pages/settings/Profile.tsx
C27Role-based access control enforcement (Admin / Clinic Admin / Staff, clinic-scoped)SecurityCONFIRMEDsupabase/migrations/20260705000000_portal_roles.sql:122-269, web/src/lib/session.tsx:80-98
C28Direct data-access layer (PostgREST CRUD over 10 clinic-scoped tables, governed by RLS)Data accessCONFIRMEDsee §2 below
C29AI conversational behavior (identity-verification-first, progressive slot disclosure, no-pressure decline handling)AI behavior / promptINFERREDtelnyx/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
C30Booking-link abuse mitigation (per-token verification lockout, per-IP rate limit, no invalid/expired oracle)SecurityCONFIRMEDsupabase/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.

TableOperations observed in frontendGoverning RLS (evidence)
clinicsselect, updatesupabase/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)
patientsselect, insert, update, upsert20260705000000_portal_roles.sql:182-186 — read: own clinic or Admin; write: Clinic Admin+
providersselect, insert, update20260705000000_portal_roles.sql:191-195 — Clinic Admin+ write, clinic-scoped read
provider_availabilityselect, insert, update, delete20260705000000_portal_roles.sql:200-213 — scoped via parent provider's clinic
campaignsselect, insert, update20260705000000_portal_roles.sql:218-222 — read: clinic-scoped; write: Clinic Admin+ (Staff explicitly excluded per spec D1)
campaign_patientsselect, insert/upsert, update, delete20260705000000_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)
appointmentsselect20260705000000_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_logsselect20260705000000_portal_roles.sql:263-264 — read-only in RLS; clinic-scoped
audit_logselect20260705000000_portal_roles.sql:267-269 — read-only; clinic-scoped or Admin
appointment_type_assistantsselect, upsert20260706000000_appointment_type_assistants.sql:33-39 — Clinic Admin+ write, clinic-scoped read
storage.objects (avatars bucket)insert (upload), public read20260705000000_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 by admin-manage (C3) and never client-writable (supabase/functions/admin-manage/index.ts:21-25 comment, 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-11 comment).

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 broadens clinics read 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-tools and booking-api both pass p_tz, but the only checked-in get_available_slots definition accepts only p_provider_id, p_from, p_days, and p_limit (20260704000000_init.sql:124-129). A comment in 20260706130000_self_booking_link.sql:59-62 refers to 20260705000000_slot_tz.sql, but that migration is absent. CONFIRMED repository inconsistency; whether the deployed database has the missing overload is UNKNOWN.