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