Platform services
Supabase
Four capabilities, four clients.
| Client | File | Use |
|---|---|---|
Server |
|
Server components and route handlers; reads session cookies |
Browser |
|
Client components |
Route handler |
|
Sign-in and sign-out, where cookies must be written on the response |
Admin |
|
Service-role operations; never reachable from the client |
lib/supabase/anon-env.ts resolves the public URL and anon key, returning null when they are absent so
middleware can allow requests through rather than crashing on a misconfigured environment.
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
DATABASE_URL= # pooled connection, used at runtime
DIRECT_URL= # direct connection, used by migrations
SUPABASE_GOOGLE_CLIENT_ID=
SUPABASE_GOOGLE_CLIENT_SECRET=
PostgreSQL
Accessed exclusively through Prisma (lib/prisma.ts), and only from lib/repositories. Runtime traffic
goes through the pooler (DATABASE_URL); migrations use DIRECT_URL.
@prisma/extension-accelerate is available for query caching where a workload justifies it.
Objects managed in Supabase rather than in this repository:
-
the
get_and_lease_due_targetsRPC, which atomically leases due call targets; -
reporting views —
vm_main_campaign_reports,main_required_summaries,main_sankey_chart_view,vm_main_daily_agent_performance_report,phone_number_pickup_rate,v_email_campaign_daily_engagement; -
the
call_daily_statsandagent_daily_performance_statsrollup tables with their refresh functions andpg_cronjobs, scripted inlib/supabase/sql/; -
the
handle_justcall_call_initiated()trigger function; -
pg_cronjobs that call the broadcast endpoints.
Most views prefixed vm_ are materialised — check their refresh schedule before treating stale numbers
as a bug. vm_main_campaign_reports and vm_main_daily_agent_performance_report are the exceptions:
they keep the prefix for compatibility but read from incrementally maintained rollup tables. See
Daily report rollups.
Auth
Supabase Auth owns identity and sessions. Sessions are HTTP-only cookies, refreshed automatically. Signup, sign-in and sign-out go through the app’s own API so the server sets the cookies. See Authentication.
prisma/sql/auth_hook_before_user_created_payability.sql is a Supabase auth hook enforcing the
Payability email domain at the point of user creation — a second line of defence behind
isAllowedPayabilityEmail() in middleware.
Realtime
RetellCallEventHandler.broadcastEvent(event, campaignId, payload) posts to
<SUPABASE_URL>/realtime/v1/api/broadcast on the topic campaign-<campaignId>, letting the dashboard
react to call events live. Skipped with a warning when the URL or key is missing.
Client-side subscription lives in lib/hooks/use-realtime.ts.
Vercel AI Gateway
One endpoint fronting several model providers, so switching models is a string change rather than a new SDK.
AI_GATEWAY_BASE_URL=https://ai-gateway.vercel.sh/v1 # default
AI_GATEWAY_API_KEY=
- Config
-
lib/ai/config.ts - Base class
-
lib/ai/base-ai-service.ts—BaseAIService, using@ai-sdk/openai-compatible
Services extending it:
| Service | Purpose |
|---|---|
|
Grade a completed call |
|
Suggest area codes to acquire |
|
Rephrase an email variation |
|
Propose a prompt revision from observed issues |
|
Predict outcomes for regression test cases |
Model selection
AI_GATEWAY_MODELS lists selectable models with a provider tag:
-
OpenAI —
openai/gpt-5,gpt-5-mini,gpt-4o,gpt-4o-mini,gpt-4-turbo -
Anthropic —
anthropic/claude-sonnet-5,claude-sonnet-4.6,claude-sonnet-4,claude-haiku-4.5,claude-3-5-sonnet,claude-3-5-haiku -
xAI —
xai/grok-4.5 -
Google —
google/gemini-3.5-flash
getModelSlug(modelId, llmModelType) prefers an explicit AiAgent.modelId, falling back to the legacy
llmModelType default (gpt → openai/gpt-4o, claude → anthropic/claude-sonnet-4).
getModelLabel(slug) and getProviderFromSlug(slug) support the UI.
The legacy gpt/claude enum predates per-model selection; new agents should always set modelId.
@payability/pyb-llm provides shared LLM helpers, including a HubSpot-aware agent used by the dial
pipeline.
Google BigQuery
A read-only mirror of CRM, call and engagement data, queried by sequence data gates.
- Client
-
lib/bigquery/client.ts
BIGQUERY_PROJECT_ID=bigqueryexport-183608 # default in lib/constants.ts
BIGQUERY_PRIVATE_KEY_ID=
BIGQUERY_PRIVATE_KEY= # PEM with escaped \n; restored to real newlines at runtime
BIGQUERY_CLIENT_ID=
The service-account email is hardcoded as
vercel-bigquery-agent@bigqueryexport-183608.iam.gserviceaccount.com. The client is lazily created and
memoised.
Injection safety
View, field and identifier names are SQL identifiers and cannot be passed as query parameters, so they are validated against a strict allowlist before interpolation. The identifier value is always bound as a parameter.
queryGateConditions({ view, identifierColumn, identifierValue, matchMode, conditions }) evaluates
conditions against a single contact-scoped row. Operators: is_not_null, is_null, equals,
not_equals, gt, lt, is_in, is_not_in. matchMode is all (AND) or any (OR).
The gate view must expose a hubspotcontactid column, and a contact without a hubspotContactId fails
the gate closed. See Data gates.
queryUnsyncedHubspotContacts(limit) runs a fixed SELECT against hubspot4.v_contacts_latest,
excluding HubSpot ids already present in outreach_app.contacts. LIMIT is bound (@limit,
default and ceiling 1000). SQL is documented in lib/bigquery/hubspot4/unsynced-contacts.sql.
Used by contacts/hubspot-import.
queryHubspotCallsSince(since, limit) runs a fixed SELECT against hubspot4.v_calls_latest for
rows with updated > TIMESTAMP(@since). @since is a bound STRING (RFC 3339 Configs value) and
@limit is INT64 (default and ceiling 1000). SQL is documented in lib/bigquery/hubspot4/calls-since.sql.
Used by calls/hubspot-import. The watermark lives in Configs key hubspot_call_sync_cursor
(ISO datetime) on the HUBSPOT_CONTACT_SYNC_SUBSCRIPTION_ID workspace; a missing or invalid
cursor skips the run.
querySuppliersUpdatedSince(cursor, limit) runs a fixed keyset SELECT against
PayabilitySheets.v_supplier_summary_extended for rows after (updated, supplier_key).
@sinceDate is a bound STRING (YYYY-MM-DD), @sinceKey is STRING, and @limit is INT64
(default and ceiling 1000). SQL is documented in
lib/bigquery/payability-sheets/v-supplier-summary-since.sql. Used by
suppliers/payability-import. The watermark lives in Configs key supplier_sync_cursor
({ "updated": "YYYY-MM-DD", "supplier_key": "<uuid>" }) on the
HUBSPOT_CONTACT_SYNC_SUBSCRIPTION_ID workspace; a missing cursor starts at 1970-01-01
with an empty key.
querySuppliersByKeys(keys) re-reads current view rows for known supplier_key`s
(`IN UNNEST(@keys), capped at 1000). Used after the incremental batch so status from
other tables in the view still overwrites local rows and stamps lastSyncedAt. SQL is
documented in lib/bigquery/payability-sheets/v-supplier-summary-by-keys.sql.
scripts/test-bigquery.ts is a connectivity check.
AWS SQS
Fire-and-forget hand-off to downstream Payability services.
- Client
-
lib/plugins/sqs-client.ts(@aws-sdk/client-sqs)
AWS_REGION=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
CRM_QUEUE_URL=
INCOMING_EMAIL_SQS_QUEUE_URL= # defaults to the morpheus-sam-email-agent queue
EMAIL_VARIATION_QUEUE_URL=
Publishers return a boolean rather than throwing, so a missing queue URL degrades to "not published"
instead of failing the caller. messageGroupId provides FIFO ordering per workspace or per contact. See
CRM queue and HubSpot events.
Redis
Holds the per-campaign in-flight call counter — the one piece of shared mutable state outside PostgreSQL.
- Client
-
lib/plugins/redis-client.ts - Counter
-
lib/redis/call-target-counter.ts—getCallTargetRuns,incrementCallTargetRuns,decrementCallTargetRuns
REDIS_URL=
The counter exists because a call is in-flight from the moment an Inngest event is sent until the
provider webhook lands, which the database cannot see cheaply. The broadcaster reads it, callTarget
decrements on every exit path, and callEndedAnalysis increments before a double tap. See
Asynchronous runtime.
Sentry
Error and performance monitoring across all three runtimes.
| File | Runtime |
|---|---|
|
Browser |
|
Node |
|
Edge |
|
Next.js instrumentation hooks |
|
Shared helpers |
NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_SENTRY_ENVIRONMENT=
.env.sentry-build-plugin holds the build-time upload token.
resolveSubscription() sets the Sentry user and a subscriptionId tag, so any error later in the request
is already attributed to a tenant. @inngest/middleware-sentry attaches workflow context to step
failures.
GET /api/sentry-example-api and /sentry-example-page verify the wiring.