What We Built
A running log of every step completed on Ringo so far.
01
Created Sofia in Vapi
- → Built AI voice agent named Sofia
- → Assigned warm female voice (Savannah)
- → Wrote system prompt: greet → lookup caller → collect info → book
- → Added 3 tools: check_availability, lookup_caller, book_appointment
02
Hardcoded Test Business
- → Created lib/data/business.ts with ABC Plumbing data
- → Name, owner, phone, city, timezone, services, available slots
- → Used as test data before real DB was ready
03
Supabase + Prisma Setup
- → Created 3 tables: businesses, contacts, bookings
- → Connected Prisma v7 with @prisma/adapter-pg driver
- → Generated Prisma client at lib/generated/prisma
- → Created lib/prisma.ts singleton client
- → Seeded ABC Plumbing row in businesses table
04
Vapi Webhook Route
- → Built app/api/vapi/route.ts
- → check_availability → returns open slots from business.ts
- → lookup_caller → queries contacts table by phone
- → book_appointment → upserts contact + creates booking row in Supabase
- → Phone number normalization to E.164 (+1XXXXXXXXXX)
05
Twilio SMS
- → Installed Twilio SDK, created lib/twilio.ts
- → On booking: texts plumber with job details + address
- → On booking: texts customer with confirmation + slot
- → Bought local Richmond VA number: +18043465715
- → A2P 10DLC registration deferred until production domain is ready
06
End-to-End Real Call Test
- → Connected Twilio number +18043465715 to Vapi
- → Set call forwarding on backup phone → Twilio → Sofia
- → Made real call → Sofia answered → booked job → row in Supabase
- → Fixed webhook URL missing /api/vapi path
- → Fixed toolIds being wiped on Vapi assistant updates
07
Caller Recognition
- → lookup_caller now returns rich instructions to Sofia
- → Returning callers: Sofia greets by name + confirms address
- → New callers: Sofia asks for name and address
- → Injected {{customer.number}} into Sofia's prompt — she never asks for phone
- → Cleaned up duplicate phone number formats in DB
08
Landing Page
- → Built app/page.tsx — dark theme using shadcn CSS variables
- → Hero: 'Your best employee. Never calls in sick.'
- → Stat bar: 24/7, $0 missed calls, 60s booking, 100% captured
- → 6 feature cards: answers calls, books, reminders, reviews, client DB, returning callers
- → 3-tier pricing: Basic $49 / Pro $99 / Premium $999
- → $149 one-time setup fee, $3k/mo receptionist comparison
- → Updated Navbar: Ringo branding + Dashboard link + Dev Log link
09
Appointment Management Tools
- → get_appointments — caller asks 'when is my appointment?' → returns upcoming bookings
- → reschedule_appointment — deletes old booking, creates new one with new slot, texts both parties
- → cancel_appointment — marks booking as cancelled, texts plumber + customer
- → Updated Sofia prompt with instructions for all 3 new tools
- → Sofia now has 6 total tools registered in Vapi
10
Database Enums
- → Added Industry enum: PLUMBING | HVAC | PAINTING
- → Added AgentType enum: SHARED | CUSTOM
- → Added Plan enum: BASIC | PRO | PREMIUM (replaces old string field)
- → Applied migration to Supabase, regenerated Prisma client
11
Plumber Dashboard — Calendar UI
- → Installed react-big-calendar + luxon for timezone-aware date handling
- → Built app/dashboard/page.tsx with day view calendar (default) + week view toggle
- → Clamped visible hours to 8am–5pm — no midnight scrolling
- → Dark theme CSS overrides for react-big-calendar (calendar-overrides.css)
- → Fake hardcoded events to test rendering (Mike, Sarah, Tom)
- → Click empty slot → modal opens with exact time selected
- → Modal form: customer name, phone, problem dropdown
- → Book button logs booking to console — API wiring comes next
- → Timezone strategy locked: store UTC in DB, display in business.timezone (America/New_York)
- → DST handled automatically via timestamptz + luxon timezone strings
12
Real Bookings — Schema, API & Dashboard CRUD
- → Schema migration: bookings.slot String → DateTime (UTC timestamptz in Postgres)
- → Added workdayStart, workdayEnd, slotDurationMins, workDays to Business model
- → lib/availability.ts: getAvailableSlots() — generates 30 days of slots, 2hr buffer, skips booked
- → lib/createBooking.ts: shared helper — upserts contact, creates booking, sends SMS to both parties
- → GET /api/bookings — returns upcoming bookings with contact info for calendar
- → POST /api/bookings — validates slot is free, calls createBooking()
- → PATCH /api/bookings/[id] — reschedule: conflict check, updates slot, sends SMS
- → DELETE /api/bookings/[id] — cancel: marks cancelled, sends SMS
- → Dashboard wired to real DB — fake events removed, real bookings load on mount
- → Click booking → detail modal: customer name, tap-to-call phone, Google Maps address link
- → Cancel from dashboard with confirmation dialog
- → Reschedule with datetime-local picker, min=now blocks past dates
- → All times displayed in business timezone (America/New_York) via luxon
13
Vapi — Real Dates & Improved Sofia Prompt
- → Wired check_availability to getAvailableSlots() — real dated slots like 'Monday Mar 9 at 9:00 AM'
- → Replaced inline book_appointment logic with createBooking() shared helper
- → get_appointments, reschedule_appointment, cancel_appointment now format DateTime with luxon
- → Removed hardcoded business.ts import from Vapi route — everything loads from DB
- → Phone normalization added to POST /api/bookings — strips spaces, dashes, parens, adds +1
- → Sofia prompt rewritten: one question at a time, filler phrases while tools run
- → Sofia asks for best contact number before booking instead of assuming caller ID
- → Sofia confirms details back before calling book_appointment
- → lookup_caller now fires only when caller hints at existing appointment — faster new booking calls
- → Added urgency detection: emergency keywords trigger priority booking flow
- → firstMessage set: 'Hi, thanks for calling ABC Plumbing, this is Sofia! How can I help you today?'
14
Auth — Clerk + Organizations
- → Installed @clerk/nextjs, wrapped app with ClerkProvider in layout.tsx
- → Created proxy.ts (Next.js 15 middleware) — protects /dashboard and /api/bookings/*
- → Built /sign-in and /sign-up pages using Clerk's pre-built UI components
- → Enabled Clerk Organizations — each business is an org, membership required
- → Roles planned: OWNER / DISPATCHER / TECH (industry terms, work across all trades)
- → Added clerkOrgId to Business model — links Clerk org to DB row
- → Seeded ABC Plumbing with org_3ARNhVrwvmuCTP4Pbm6cANAzhMi
- → GET/POST /api/bookings — replaced hardcoded businessId with auth() → orgId lookup
- → PATCH/DELETE /api/bookings/[id] — added auth check, returns 401 if not logged in
- → Dashboard no longer passes businessId — data scoped entirely by Clerk session
- → Vapi route stays public and hardcoded — intentional, explained in CLAUDE.md
15
Dashboard Polish — Modals, Toasts, Slot Picker, Calendar
- → Extracted BookingModal and EventDetailModal into separate components
- → EventDetailModal has 3 modes: view → reschedule → edit contact
- → Reschedule: scrollable slot list from /api/availability — no datetime picker, guaranteed free slots
- → Edit contact: inline name/phone/address editing → PATCH /api/contacts/[id]
- → AlertDialog for cancel confirmation — shows customer name + slot, no browser confirm()
- → Sonner toasts for all actions: success, error, warning
- → Calendar amber event colors, bolder text, red current time indicator
- → Calendar hours expanded: 7am–6pm
- → Slot validation: clicks on weekends/off-hours/booked slots show toast instead of opening modal
- → Dashboard header: org logo (Clerk), business name, user name, sign out button
- → Sign out button (red) for switching between business accounts during testing
16
Dynamic Slot Duration — Calendar Grid + Event Sizing
- → GET /api/availability now returns slotDurationMins alongside slots
- → Dashboard reads slotDurationMins from API — no hardcoded 60 minutes
- → Calendar event blocks sized correctly: 2hr slot = 2hr block on calendar
- → Calendar grid lines driven by slotDurationMins: 60min = lines every hour, 120min = every 2hrs
- → useRef for booking logic (no re-render), useState for calendar display (triggers re-render)
- → Load order fixed: availability loads before bookings so duration is set before events map
- → Change slot duration in Supabase → calendar updates on next page load, no code change
17
Add Business Page + Multi-tenant Test
- → Built /add-business — public form to create a new business with all schema fields
- → POST /api/businesses — saves to DB, returns created business
- → Smart defaults: only name + clerkOrgId need to change for testing
- → Industry, plan, timezone, work hours, work days, slot duration all configurable
- → Installed shadcn Checkbox component for work days selection
- → Tested multi-tenant isolation: Rivera HVAC org sees only its own calendar
- → SMS failures now non-fatal: booking/reschedule/cancel always save even if Twilio fails
- → smsFailures[] returned from createBooking — warning toast shows which SMS failed + customer phone
- → Same fix applied to PATCH and DELETE routes for reschedule and cancel