A voice AI webhook receiver with analytics dashboard. Receives webhooks from Retell AI, stores conversation data, and displays analytics.
π Watch Demo Video
Click the link above to watch the step-by-step demo of how the platform works.
π View on Portfolio
- Next.js 16 (App Router, React 19, TypeScript strict mode)
- Drizzle ORM + Supabase Postgres - Documentation
- shadcn/ui + Tailwind CSS v4
- Zod validation - Documentation
- Retell AI SDK with Conversation Flow
src/
βββ app/
β βββ api/webhooks/retell/route.ts β Webhook receiver
β βββ dashboard/page.tsx β Analytics dashboard
β βββ interview/[callId]/page.tsx β Call detail view
βββ components/
β βββ dashboard/ β Dashboard components
β βββ interview/ β Call detail components
β βββ ui/ β Reusable UI components
βββ db/
β βββ schema.ts β Drizzle schema + types
β βββ index.ts β DB connection
βββ lib/
β βββ types.ts β Centralized type exports
β βββ utils/ β Utility functions
β βββ validations/retell.ts β Zod schemas
docs/
βββ retell/
β βββ retell-integration.md β Retell AI integration docs
β βββ retell-agent-config.json β Agent configuration
βββ ngrok-setup.md β ngrok local testing docs
npm install
cp .env.example .env# Database (Supabase Postgres)
DATABASE_URL="postgres://..."
DATABASE_POSTGRES_URL_NON_POOLING="postgres://..."
# Retell AI
RETELL_API_KEY="your_retell_api_key"- Connect your GitHub repo to Vercel
- Connect Supabase integration in Vercel:
- Vercel Dashboard β Your Project β Storage β Connect Database
- Select Supabase β This auto-populates
DATABASE_POSTGRES_URL
- Add remaining environment variable:
RETELL_API_KEY
- Deploy
View data directly in:
- Supabase Dashboard β Table Editor β
interviewstable (or your configured table name)
Problem: Supabase Free Tier pauses projects after 7 days of inactivity, causing Tenant or user not found errors.
Solution: Health check cron job keeps database active.
How it works:
- Health check endpoint:
/api/health(src/app/api/health/route.ts)- Simple
SELECT 1query to keep DB connection alive - Returns status + timestamp
- Simple
- Vercel cron job: Configured in
vercel.json- Runs every 6 days (
0 0 */6 * *) - Prevents auto-pause by keeping project active
- Runs every 6 days (
Manual recovery (if paused):
- Go to Supabase Dashboard
- Click "Restore project" button
- Wait 1-2 minutes for database to restart
- Redeploy in Vercel (to reconnect)
Alternative: Upgrade to Supabase Pro ($25/month) for no auto-pause.
Create a Retell AI agent using Conversation Flow (not Single Prompt mode):
- Go to Retell Dashboard β Agents β Create Agent
- Select Conversation Flow as the agent type
- Configure the conversation flow with blocks:
- Greeting block - Welcome message
- Question blocks - Each conversation question
- Goodbye block - Thank you message
- Enable Extract Dynamic Variables on each question block to capture responses
- Register your webhook URL in Agent Settings
See docs/retell/retell-integration.md for detailed configuration.
src/db/schema.ts
Simple schema with Drizzle:
callIdas unique identifierparticipantIdtranscriptstored as JSON arraydurationin millisecondscompletionStatustimestamps
Drizzle gives us type inference with $inferSelect and $inferInsert types.
npm run db:generate # Generate migrations
npm run db:push # Push schema to database
npm run db:studio # Open Drizzle Studiosrc/lib/validations/retell.ts
The Zod schema matches Retell's nested structure:
eventtype at the top level (call_started,call_ended,call_analyzed)callobject with all the call datatranscript_objectas an array of messages
src/app/api/webhooks/retell/route.ts
The webhook endpoint does 4 things:
- Validates signature using Retell SDK (
Retell.verify()) - real validation, not mock - Parses and validates payload with Zod
- Calculates duration from timestamps (
end_timestamp - start_timestamp) - Saves to database with Drizzle (check if exists, then insert or update)
- Webhook Overview - Webhook events and payload structure
- Secure Webhook - Signature validation
- Register Webhook - Configure webhooks in dashboard
src/app/dashboard/page.tsx
The dashboard shows:
- Total calls count
- Average duration
- Completion rate
- Question-by-question analytics - each question, response count, sample answers
- Recent calls list
src/app/interview/[callId]/page.tsx
The detail page shows:
- Metadata at the top (call ID, duration, status)
- Complete transcript formatted as conversation between agent and user
npm run dev# Terminal 1: Start Next.js
npm run dev
# Terminal 2: Start ngrok
ngrok http 3000
# Register in Retell dashboard:
# https://abc123.ngrok-free.app/api/webhooks/retellPOST https://your-domain.vercel.app/api/webhooks/retell
After a webhook is received, check logs in:
- Vercel Dashboard β Your Project β Logs
- Look for:
β Call saved: [call_id]
- Drizzle ORM - SQL-like API with great TypeScript support
- Real signature validation - Using Retell SDK, not mocking
- Zod nested schema - Matches Retell's actual payload structure
- Check then insert/update - Check if exists first, then insert or update
- Server Components - Dashboard and detail pages fetch data directly
- Reusable components - Small, focused components with barrel exports
Instead of parsing transcripts with regex/AI to extract answers, this project uses Retell's Extract Dynamic Variables feature. This gives us structured data directly from the conversation flow.
Why: More reliable, no parsing errors, works regardless of how the user phrases their answer.
Trade-off: Requires using Conversation Flow with Blocks instead of Single Prompt mode.
See: docs/retell/retell-integration.md
Extracted variables are stored in a JSON column (extracted_variables) rather than separate columns.
Why: Flexible schema - can add new variables without migrations. Easy to extend for different conversation types.
Trade-off: Can't query individual fields with SQL WHERE clauses. For production, consider adding a GIN index or separate columns for frequently queried fields.
Dashboard and interview pages use Server Components with force-dynamic to always fetch fresh data.
Why: Simpler than client-side state management. No stale data issues.
Trade-off: Every page load hits the database. For high traffic, add caching or ISR.
Added a Refresh button instead of automatic polling.
Why: Data changes infrequently (when interviews complete). Polling wastes resources.
Trade-off: User must click to see new data. Could add Server-Sent Events for real-time updates in production.
Participant IDs are generated as participant-1, participant-2, etc. using COUNT(*).
Trade-off: Theoretical race condition with concurrent requests. Acceptable for MVP.
Production solution: Use database sequences or UUID-based IDs.
The project spec suggested "mock validation," but I implemented real signature validation using the Retell SDK (Retell.verify()).
Why: More secure and production-ready. Same amount of code.
Used Next.js 16 with React 19 instead of 14+ as specified.
Why: Latest stable version with improved performance. App Router API is the same.
- Webhook receiver for Retell AI call events
- Analytics dashboard
- Call detail view with transcript