About this app
UCSD Process Discovery is a voice-interview platform for finding repetitive administrative work, extracting evidence-backed process records, and turning those records into a ranked automation backlog for forward deployed engineering.
This page describes the production architecture, privacy boundaries, data flow, model routing, and safeguards used by the application. It reflects the current runtime configuration without exposing API keys, secrets, participant records, or admin console data.
System at a glance
Live audio and raw transcripts pass through vendor clouds (Vapi, Vercel, Neon). Analysis models run on campus through the Triton AI gateway; only the redaction stage sees raw text, and everything after it works on redacted text. Download this diagram (SVG).
Architecture
The web application is a Next.js 16 App Router deployment on Vercel, rendered with React 19. The user interface is Bootstrap 3 served through the UC San Diego Decorator v5 bundle, self-hosted with the app. The admin console manages templates, campaigns, invitations, consent documents, interviews, analysis results, and exports. The interview participant flow is token based: the participant opens a personal invitation link, accepts the consent notice, passes the microphone check, and starts the voice interview.
The realtime audio call does not run through Vercel. The participant browser connects directly to Vapi over WebRTC (Daily.co transport) using the Vapi browser SDK. The app assembles a transient Vapi assistant immediately before each call. That assistant includes the current interview template, consent script, participant context, resume state, configured voice, model choice, per-call duration cap, and tool definitions, but it does not include server secrets. Because the assistant configuration transits the participant's browser, the server keeps a hash of the prompt, tools, duration cap, recording settings, and metadata and compares it with what Vapi actually ran at the end of the call. A mismatch parks the interview for human review and it is never analyzed.
Vapi sends status updates, tool calls, transcripts, summaries, and end-of-call reports back to /api/vapi/webhook. The webhook endpoint verifies the org-level Vapi secret, stores every webhook behind an idempotency gate, resolves the call session by the Vapi call id bound by the token-authenticated browser, ignores tool calls and reports from superseded or never-connected sessions, updates the call session, and kicks off the finalization path. Cron sweeps reconcile missed or delayed events, so webhooks are the fast path and polling is the durability backstop.
Neon Postgres (with pgvector) is the source of truth, accessed through Drizzle. It stores campaigns, invitations, interview state, call sessions, stitched transcripts, redacted transcripts, analysis runs, process records, embeddings, clusters, themes, consent events, email events, and an LLM usage ledger. Postgres also acts as the queue for analysis work: each run has a stage cursor, attempt count, lease, cost ledger, model flags, and status. Admin sign-in uses Auth.js v5 email magic links; email goes through Resend; audio recordings are copied to Vercel Blob; scheduled jobs run on Vercel Cron.
Participant flow
- Invitation: Resend sends a magic link from the verified sender. Links are personal, expire by campaign policy, and rotate whenever they are resent or copied.
- Written consent: the participant accepts a versioned written consent notice before any call starts. The click is recorded as a consent event pinned to the document version.
- Voice session: the app mints a call session and transient Vapi assistant. The participant speaks with the voice agent in the browser. Only one call may be live per interview; starting again from another device takes over the interview and ends the superseded call.
- Spoken consent: the agent reads the consent script and calls the
confirm_consenttool with the participant's answer. The participant's own words are stored as evidence. Section completion is refused until a verbal yes is on record, so the agent cannot skip past consent. A verbal decline after content has been captured withdraws the interview and records a withdrawal event. - Tool calls: the agent marks sections complete and can pause at the participant's request. Duplicate tool-call deliveries replay the stored result instead of acting twice.
- Pause and resume: a pause requested by voice or by the Pause button stays a pause, as does a dropped connection or a call that hits the duration cap. Every pause extends the link's resume window by seven days. The next call receives a progress summary and resumes at the next unfinished section.
- Completion: when the agent concludes the call with spoken consent on record, the transcript is stitched across sessions, the invitation is marked complete, a receipt can be sent, and analysis is queued. If the agent concludes a call without spoken consent on record, the interview is paused for re-confirmation rather than completed, and the same check blocks finalization and analysis.
Voice stack
| Layer | Current implementation | Purpose |
|---|---|---|
| Realtime call transport | Vapi browser SDK over WebRTC (Daily.co) | Keeps low-latency audio off the Vercel request path. Campaigns can set the hard call cap up to 43,200 seconds. |
| Conversation model | Vapi-native Anthropic, default shown to signed-in admins | Runs the live voice interview and adapts the template questions into natural conversation. |
| Optional conversation override | A campaign can point the live conversation at an OpenAI-compatible endpoint | Allows an OpenAI-compatible endpoint, such as the Triton gateway, to power the live interview after latency testing. The endpoint key lives in Vapi's credential store, never in the assistant config. |
| Transcription | Deepgram nova-3 through Vapi | Produces the call transcript; template key terms are passed through to improve campus jargon recognition. |
| Voice | Campaign voice provider/voice id, default Vapi Elliot | Speaks the interviewer side of the conversation. |
| Operational Vapi analysis | Minimal summary and coverage JSON inside Vapi | Provides lightweight call-quality signals only. Authoritative extraction happens server-side after redaction. |
Analysis pipeline
The analysis pipeline is deliberately staged so each expensive or risky task has a narrow job. A run can resume from its last completed stage and can be re-run without changing prior successful runs until the new run succeeds. Pipeline version: 1.0.
| Stage | What it does | Output |
|---|---|---|
| Redact | Applies deterministic regex rules (identifiers, contact details, and titled personal names such as Prof. or Dr. followed by a name), then asks two on-campus models in parallel to list the remaining names and personal identifiers and unions their answers. An empty answer on a transcript that looks like it contains identifiers triggers a second opinion from another on-campus model. This is the only stage that sees raw transcript text, and it fails closed: if no on-campus redaction model answers, nothing downstream runs. | Versioned redacted transcript. |
| Extract | Inventories recurring work processes, extracts one structured process record per process, and captures non-process theme mentions. Evidence quotes are required for factual process claims. A deterministic grounding validator then checks every quote against the redacted transcript: fuzzy matches are accepted only when every number or number word in the quote appears exactly. A numeric field is kept when a verified quote is labelled for it (tolerating misspelled labels) or plainly states the number, including phrases such as one in five, once a month, or six hours; otherwise it is nulled and added to the record's unknowns. The pain quote is dropped when it cannot be found, and a record with fewer than six verified quotes and no verified volume quote is dropped rather than entering the backlog. | Process records, theme mentions, verified evidence, unknowns. |
| Score | Computes effort, pain, feasibility, confidence, and routing signals from extracted facets. Pure math, no model call. | Priority fields used by the backlog. |
| Embed | Embeds redacted transcript chunks, process records, and theme mentions. | pgvector-searchable corpus for clustering and Ask. |
| Cluster | Joins similar process records across interviews, with LLM adjudication in the ambiguous range. Human curation (must-link, cannot-link, rename, merge, split) is keyed on a stable identity made from the interview, the normalized process name, and the department, so it survives re-analysis with new prompts or models. Renames apply on every recluster and carry through merges. | Canonical process clusters and membership. |
| Rollup | Aggregates clusters, department heatmaps, systems, and campaign-level theme synthesis. Only succeeded runs count, and an interview's older runs are excluded as soon as a new run starts. Theme synthesis is incremental (only unassigned mentions are sent), chunked so the model's output cap cannot truncate a grouping, wrapped in one transaction, and serialized with a per-campaign advisory lock so the nightly job, admin recompute, and a finishing run never interleave. | Backlog, heatmap, systems, themes, and analytics views. |
Retries distinguish real errors from interruptions. An error attempt is counted against the run; a lease reclaim (a crash, a deploy, or the sweep's time budget expiring mid-stage) re-queues the run from its stage cursor without consuming an attempt, and is capped separately. Every model call has its own wall-clock timeout, and a run that trips the cost ceiling is failed permanently rather than retried.
Each run has a hard cost circuit breaker of shown to signed-in admins. On-prem Triton stages are recorded as zero marginal LLM cost; direct/cloud model usage is estimated from token counts.
Model routing
Analysis calls use model specs in the form onprem:<id>, cloud:<id>, or anthropic:<id>. Bare model IDs are treated as direct Anthropic models for backward compatibility. The current environment reports:
| Purpose | Current primary model | Fallbacks | Why this role uses it |
|---|---|---|---|
| Live voice conversation | shown to signed-in admins via Vapi-native Anthropic, unless a campaign overrides it | Campaign-specific override | Optimized for low-latency turn-taking and natural interviewing rather than bulk analysis. |
| Redaction | shown to signed-in admins | shown to signed-in admins | Fast entity listing after regex redaction; this is the only analysis model stage that sees raw transcript text, so its chain is restricted to on-campus models. |
| Process extraction | shown to signed-in admins | shown to signed-in admins | Produces strict structured output for recurring processes, evidence, unknowns, systems, time, volume, and route signals. Chosen because it is the on-prem model that returns the full process-record schema reliably. |
| Cluster adjudication | shown to signed-in admins | shown to signed-in admins | Resolves ambiguous semantic matches between process records when vector similarity alone is not decisive. |
| Themes | shown to signed-in admins | shown to signed-in admins | Consolidates raw theme mentions into campaign-level findings with descriptions, source breadth, requirement implications, recommended follow-up, and cited evidence. Falls back to embedding-similarity grouping if every model in the chain fails. |
| Dossiers | shown to signed-in admins | shown to signed-in admins | Produces deeper opportunity write-ups for FDE review after a backlog item is selected. Generated on demand from the backlog. |
| Ask the interviews | shown to signed-in admins | None by default for chat synthesis. | Uses retrieval over analyzed records and redacted transcript chunks, then returns cited answers grounded in the campaign corpus. |
| Embeddings | shown to signed-in admins (1024 dimensions) | Configurable after vector schema validation. | Powers semantic search, clustering, Ask retrieval, and theme/process similarity. Only redacted text is embedded. |
| QA harness | shown to signed-in admins | Development-only | Drives synthetic test interviews where roleplay quality matters more than production cost. Synthetic transcripts contain no real data. |
Fallback policy. When a structured stage's primary model fails or returns invalid output, the run tries the on-campus fallbacks first and a cloud model only when a cloud key is configured. Redaction never falls back to a cloud model unless the operator explicitly opts in; without an on-campus answer it fails closed. Every fallback or off-campus call is recorded on the analysis run and shown on the admin interview page, and a complete usage ledger records model, tokens, cost, and on-campus status for every pipeline stage, dossier, Ask chat turn, and nightly adjudication call.
Prompt versions: redact v2, extract v2, cluster v1, themes v2, dossier v1.
Privacy and data boundaries
- Raw transcript text is stored so administrators can audit interviews, but the analysis pipeline redacts before downstream extraction, embeddings, clustering, and Ask retrieval.
- Deterministic regex rules remove common identifiers such as SSNs, UCSD PID-shaped IDs, emails, phone numbers, dates, and titled personal names before the redaction models run.
- Two on-campus redaction models are each instructed to remove third-party names and personal identifiers while preserving systems, departments, job roles, buildings, and process vocabulary; their answers are unioned. Redaction runs only on campus infrastructure unless the operator explicitly opts into a cloud model.
- Campaigns can anonymize reports so FDE-facing views refer to people by role and department rather than name or email. Viewers never see or export participant names or emails on any campaign.
- Theme synthesis and Ask use active successful analysis outputs; failed and superseded analysis runs are not counted in current campaign themes.
- Audio recording is campaign-controlled. If a retention period is configured, the daily retention job deletes expired recordings, strips raw transcript and recording fields from stored webhook payloads, and removes the raw per-call transcript from analyzed call sessions. The redacted transcript in the analysis tables is the durable copy; rows are never deleted.
- Consent is versioned and captured twice: click-through consent before the call and spoken confirmation during the call. Both are enforced server-side, and the participant's spoken words are stored as evidence.
Operations and reliability
The app uses Vercel Cron and Postgres leases instead of a separate queue service. The call sweep reconciles Vapi calls whose end-of-call report never arrived, retries recording fetches that failed, and closes sessions that were minted but never bound to a call. The analysis sweep resumes queued or lease-expired analysis runs, reminders send pending follow-ups, the retention job expires overdue invitations, auto-finalizes paused interviews after 48 hours of inactivity, and enforces recording and transcript retention, and the nightly recluster job keeps campaign rollups current.
All important external inputs are made idempotent. Vapi webhooks are stored before processing and duplicate tool calls replay their stored result, email sends are logged as email events, analysis stages are wipe-and-rewrite or versioned where appropriate, and successful re-analysis supersedes prior successful runs only after the new run completes.
Access and security
- Admin sign-in uses emailed magic links with an allowlist; there are no passwords. A disabled admin loses access on their next request because the session is re-checked against the user record, and the last active owner cannot be demoted or disabled.
- Roles are owner, admin, fde, and viewer. Viewers see aggregate views and exports only; person-level and dossier exports are refused for viewers on non-anonymized campaigns, and the Service Owner report is name-masked for them.
- Every admin server action validates its input with zod (ids, bounded integers, model-id allowlist, real email addresses, bounded invite metadata). Magic-link requests, invitation sends, reminders, invite-link minting, and report emails are rate limited.
- CSV exports neutralize spreadsheet formula prefixes. Exports and API responses are marked non-cacheable.
- In production the server validates its required configuration on the first request that needs it and fails with a clear message rather than falling back to a localhost default.
- Every page carries Strict-Transport-Security, nosniff, frame denial, a strict referrer policy, a Permissions-Policy that allows the microphone only from this origin, and an enforced Content Security Policy with a per-request script nonce. Invitation and sign-in tokens are stored only as SHA-256 hashes. Vapi webhooks are checked against the org-level shared secret with a constant-time comparison; Resend webhooks are signature-verified.
Verification
Continuous integration runs the typecheck, lint, the unit-test suite, a production build, a dependency audit, a prompt-version consistency check, and a schema-drift check that fails if the database schema changed without a committed migration.
Two live-lifecycle regressions drive the real interview routes and webhook handler against a running dev server with synthetic Vapi events, then let the analysis pipeline run on the on-prem models. The happy path covers consent, sections, end of call, and analysis. The integrity-gates run covers section completion refused before spoken consent, a voice pause staying a pause, duplicate tool-call replay, device takeover neutralizing the old call, an agent-concluded call without consent being paused rather than completed, a verbal decline abandoning the interview, and the analysis run recording no off-campus use. A golden-fixture suite runs the whole pipeline on four synthetic personas with planted identifiers and known ground truth.
None of these replace a real voice call. Audio, turn-taking, and the agent actually calling the tools are only proven live.
Release Notes
2026-09-02: Hardening pass: consent, interview integrity, model policy, access
- Spoken consent is now enforced server-side: the agent cannot mark a section complete until confirm_consent records a verbal yes, the participant's words are stored as evidence, and finalization and analysis are blocked without it. A verbal decline after content withdraws the interview; an agent-concluded call with no spoken consent is paused for re-confirmation, never completed.
- A voice-initiated pause stays a pause with a seven-day resume extension. Taking over an interview from a second device ends the superseded call, and the webhook ignores tool calls and reports from superseded sessions.
- The assistant config hash now covers the prompt, tools, duration cap, recording settings, and metadata; a mismatch parks the interview for human review instead of analyzing it. Recording fetch failures and never-bound sessions are reconciled by the call sweep.
- Model routing re-verified against the Triton gateway. On-prem fallbacks run before any cloud model, redaction never leaves campus unless explicitly enabled, and every fallback or off-campus call is recorded on the run and shown on the interview page. A complete usage ledger now covers pipeline stages, dossiers, Ask chat, and nightly adjudication.
- Curation overrides key on a stable process identity and survive re-analysis; renames carry through merges. Grounding requires exact numeric tokens, nulls numeric fields with no verified evidence, and verifies the pain quote. Theme synthesis is transactional, incremental, chunked, and serialized per campaign; rollups count only succeeded runs.
- Access hardening: disabled admins lose their session on the next request, the last active owner cannot be demoted or disabled, viewers never see or export participant names or emails, every server action validates its input, outbound email is rate limited, CSV exports neutralize formula prefixes, production configuration is validated at first request, and security headers plus an enforced Content Security Policy with per-request nonces are set on every page.
- Retention now strips raw transcript text from stored webhook payloads and from analyzed call sessions once the campaign retention period passes; the redacted transcript in the analysis tables is the durable copy.
- Two live-lifecycle regressions and a golden-fixture pipeline suite were added alongside the CI checks. The golden suite drove further deterministic hardening: two on-campus models are unioned for redaction with a titled-name regex rule, grounding recovers numbers stated in verified quotes (misspelled labels, one in five, once a month, six hours), and unsubstantiated process records are dropped.
- Content Security Policy is enforced with per-request nonces from a proxy, the UC San Diego Decorator bundle is deferred on interviewee pages, the template builder and email templates were split into modules, and the sign-in page no longer reveals allowlist membership.
2026-06-30: Public About page, release notes, and richer theme synthesis
- Made this About page publicly accessible at /admin/about so stakeholders can review the platform architecture without console access.
- Made the How to use guide publicly accessible at /admin/guide with the same public navigation and sign-in affordance.
- Added the public About and How to use sections to signed-out and signed-in application navigation.
- Added release notes and refreshed the architecture, analysis, privacy, operations, and model-routing documentation.
- Upgraded Themes from raw one-off observations into campaign-level synthesized themes with source breadth, evidence links, requirement implications, and recommended follow-up actions.
- Theme consolidation now ignores failed or superseded analysis runs so re-analysis does not double-count stale theme mentions.
2026-06-29: Higher runtime caps and resilient analysis budgets
- Raised the Vapi per-call hard cap to 43,200 seconds, matching Vapi's documented 12-hour maximum, while keeping the default target interview length concise.
- Made the per-run analysis cost ceiling configurable with a $5 default instead of a hardcoded $0.75 breaker.
- Re-ran the Atlassian EOL interview that had failed on the old analysis ceiling; it now completes analysis successfully.
2026-06-24: Campaign cleanup and stale-rollup protection
- Added campaign-admin participant removal with explicit confirmation and cleanup of related interview, call-session, transcript, recording, and analysis data.
- Refreshing campaign clusters, themes, and dossier freshness after deletion prevents aggregate views from retaining removed participant data.
2026-06-11: Email delivery observability
- Added Resend webhook ingestion and email event tracking so invitation status can distinguish sent mail from delivered, bounced, complained, or failed mail.
- Kept the existing verified sending domain while improving delivery-state visibility.