Cross-cutting concerns

Observability

Sentry

Initialised in three places — sentry.client.config.ts, sentry.server.config.ts, sentry.edge.config.ts — plus instrumentation.ts and instrumentation-client.ts. resolveSubscription() sets the user and a subscriptionId tag on every authenticated request, and @inngest/middleware-sentry attaches workflow context to step failures.

Structured console logging

Handlers and workflows log with a bracketed source prefix, for example 📡 [Nooks] call.logged — eventId=… or [Phone Number Optimizer] Released 3 numbers. Vercel log search relies on these prefixes.

Webhook audit trail

Every inbound webhook is persisted to WebhookEvent with its raw payload before dispatch, so any event can be inspected or replayed after the fact.

Analysis audit trail

CallAiAgentAnalysisResult is unique on (callId, aiAgentId, aiAgentVersionId), which makes it a record of which agent version produced which verdict — the basis of the per-agent performance report.

Time handling

Time is the most common source of subtle bugs in this system. Three rules:

  1. Timestamps are stored in UTC. Scheduling columns use @db.Timestamptz(6).

  2. Call windows are evaluated in the target’s timezone. CampaignTarget.tz is derived from the number’s area code (lib/utils/area-code-timezones.ts, lib/utils/state-area-codes.ts) and defaults to America/New_York. PhoneNumberCallingService.checkCallWindow converts the current UTC instant into that zone using Intl before comparing against Campaign.windowStart/windowEnd.

  3. Report and filter date ranges are Eastern Time. parseDateInNY(dateStr, endOfDay?) in lib/utils.ts turns a YYYY-MM-DD string into the UTC instant of midnight or 23:59:59 in America/New_York, DST-aware via Intl.DateTimeFormat with no external library. Call list timestamps are rendered in Eastern Time with the zone abbreviation so the display matches the filter semantics.

TIMEZONES in lib/constants.ts is the allowlist offered in the UI.

Caching

  • React Query is the client cache. Query keys follow a stable convention (["campaigns"], ["campaign", id], ["campaignTargets", campaignId, params]) and every mutation invalidates the related keys. See Data layer.

  • Fetches use cache: "no-store" so Next.js does not add a second, invisible cache layer in front of React Query.

  • Prisma connects through Supabase’s pooler; DIRECT_URL is reserved for migrations. @prisma/extension-accelerate is available for query caching where a workload justifies it.

Performance

Query shape

List endpoints share a where builder between the page query and its count query, so filters can never diverge between the two. Stats come from groupBy aggregations rather than loading rows into memory.

Indexes

Composite indexes exist for the hot paths: CampaignTarget(campaignId, state), CampaignTarget(nextEarliestAttemptAt), CampaignTarget(leaseExpiresAt), Call(campaignId, status), Call(targetPhoneNumber, campaignId, createdAt), Call(targetPhoneNumber, disposition), Emails(subscriptionId, createdAt DESC), ContactSequence(status, nextStepAt).

Leasing instead of locking

Target selection uses a lease timestamp, not a database lock, so a crashed worker releases its claim by expiry rather than requiring cleanup.

Pagination everywhere

Every list endpoint returns { data, total, page, pageSize } and enforces a maximum page size (commonly 100 or 500).

Scalability

  • Route handlers are stateless and horizontally scalable; the only shared mutable state outside PostgreSQL is the Redis counter.

  • Broadcast loops are pull-based, so adding campaigns adds ticks rather than long-running processes.

  • Inngest applies its own concurrency limits per function (callTarget limit 5, sequenceExecute limit 50), which caps downstream provider load independently of how many events are enqueued.

  • Sleeping Inngest runs do not hold concurrency slots, so a sequence with a 7-day wait costs nothing while waiting.

Known constraints

  • No automated test suite exists. Write pure functions and inject dependencies so that logic can be tested, but do not add test infrastructure unless it is explicitly requested.

  • lib/mock-data.ts still backs a few placeholder screens.