Cross-cutting concerns
Observability
- Sentry
-
Initialised in three places —
sentry.client.config.ts,sentry.server.config.ts,sentry.edge.config.ts— plusinstrumentation.tsandinstrumentation-client.ts.resolveSubscription()sets the user and asubscriptionIdtag on every authenticated request, and@inngest/middleware-sentryattaches 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
WebhookEventwith its raw payload before dispatch, so any event can be inspected or replayed after the fact. - Analysis audit trail
-
CallAiAgentAnalysisResultis 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:
-
Timestamps are stored in UTC. Scheduling columns use
@db.Timestamptz(6). -
Call windows are evaluated in the target’s timezone.
CampaignTarget.tzis derived from the number’s area code (lib/utils/area-code-timezones.ts,lib/utils/state-area-codes.ts) and defaults toAmerica/New_York.PhoneNumberCallingService.checkCallWindowconverts the current UTC instant into that zone usingIntlbefore comparing againstCampaign.windowStart/windowEnd. -
Report and filter date ranges are Eastern Time.
parseDateInNY(dateStr, endOfDay?)inlib/utils.tsturns aYYYY-MM-DDstring into the UTC instant of midnight or 23:59:59 inAmerica/New_York, DST-aware viaIntl.DateTimeFormatwith 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_URLis reserved for migrations.@prisma/extension-accelerateis available for query caching where a workload justifies it.
Performance
- Query shape
-
List endpoints share a
wherebuilder between the page query and its count query, so filters can never diverge between the two. Stats come fromgroupByaggregations 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 (
callTargetlimit 5,sequenceExecutelimit 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.