Platform services

Supabase

Four capabilities, four clients.

Client File Use

Server

lib/supabase/server-client.ts

Server components and route handlers; reads session cookies

Browser

lib/supabase/browser-client.ts

Client components

Route handler

lib/supabase/route-handler-client.ts

Sign-in and sign-out, where cookies must be written on the response

Admin

lib/supabase/admin-client.ts

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_targets RPC, 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_stats and agent_daily_performance_stats rollup tables with their refresh functions and pg_cron jobs, scripted in lib/supabase/sql/;

  • the handle_justcall_call_initiated() trigger function;

  • pg_cron jobs 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.

Edge Functions

lib/supabase/functions/ holds Deno functions deployed to Supabase, not to Vercel:

Function Purpose

event-logger

Webhook receipt at the edge

campaign-scheduler

Broadcasts a no_available_target Realtime event for a campaign

hello-world

Scaffolding

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

CallAnalysisService

Grade a completed call

CoverageRecommendationService

Suggest area codes to acquire

EmailVariationService

Rephrase an email variation

PromptSuggestionService

Propose a prompt revision from observed issues

TestCaseRunnerService

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

sentry.client.config.ts

Browser

sentry.server.config.ts

Node

sentry.edge.config.ts

Edge

instrumentation.ts, instrumentation-client.ts

Next.js instrumentation hooks

lib/sentry.ts

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.