Foyer 🛠️

Hackathon: H0: The Hackathon Zero

Live Demo: foyerai.vercel.app


Inspiration

HVAC, plumbing, electrical, and roofing shops lose four-figure jobs every time a homeowner's call goes to voicemail. The market reality: 30%+ of inbound service calls go unanswered during business hours because the dispatcher is on another line or out on a job, and another 40% land after hours and never get returned. The lost-revenue math is brutal — a single missed emergency burst-pipe call is a $1,500 job that walks down the street to whoever picks up next.

The deeper problem isn't just answering — it's coordinating. Most shops have a website chat widget, an office phone, and an on-call tech with a personal cell. All three can promise the same Tuesday-at-2 slot to different homeowners, and the conflict only surfaces when two trucks show up at two driveways for the same crew. The contractor world calls this "double-booking purgatory" and it eats more margin than missed calls do.

The bet: a Claude-powered voice receptionist that books the appointment in the same breath as answering the call, backed by a database that physically cannot let two writes collide on the same crew + time slot. Aurora DSQL's multi-region strong consistency turns that bet from a hope into a guarantee.


What It Does

A homeowner calls the shop's number, gets greeted by Claude in the shop's brand voice, walks through the issue conversationally, and hangs up with a confirmed appointment — all in 60-90 seconds.

  • 24/7 call answering — Claude picks up every call, captures the issue, address, urgency, and caller name through natural conversation. No phone tree, no "press 1 for plumbing."
  • Atomic crew booking — every appointment write hits a UNIQUE(crew_id, start_time) index in Aurora DSQL. If the office line and the website chat both try to promise Mike's 2 PM slot at the same instant, the second writer gets a clean rejection and Claude rolls the homeowner to the next available window.
  • Instant quote ballparks — Claude reads the shop's pricing playbook and gives the homeowner a service-call ballpark before hanging up, so there's no surprise when the truck shows up.
  • SMS confirmations + reminders — auto-text with crew name, ETA window, and a "running late" link.
  • Owner dashboard — every call transcript, drafted quote, booking, and crew schedule in one place across multiple shops/locations. Per-shop branding, dispatch tone, and service catalog.
  • Stripe subscriptions — $299/month per shop or $2,990/year, managed through Stripe Billing.

Beyond the single inbound call, Foyer gives you:

  • A calls dashboard of every conversation Claude has had, with full transcripts, the booking it produced, and the quote ballpark it offered
  • Multi-shop + multi-location support so a parent operator with 3 HVAC shops in 3 cities runs them all from one dashboard
  • Crew availability boards showing each truck's day at a glance, with conflict highlights when two appointments are too tight to be physically possible
  • 7 sample call scenarios — emergency burst pipe, AC not cooling, no hot water, panel upgrade, drain clog, shingle damage, after-hours service call — so you can simulate a call without buying a number

The promise: the receptionist seat ($35,000/yr) becomes a $299/mo line item, and the appointment book becomes a database with no race conditions.


How I Built It

Architecture

Homeowner (voice call)
    │
    ▼
Twilio → /api/twilio/voice (Next.js on Vercel)
    │  Gather speech transcript per turn
    ▼
Claude Sonnet 4.6
    - shop context: services, pricing playbook, hours, crews
    - tools: quote_ballpark, book_appointment
    - emits BOOKING_JSON when enough captured
    │
    ▼
Amazon Aurora DSQL (Postgres-compatible, multi-region strong consistency)
    ├── foyer_shops          — multi-shop profiles + dispatch tone
    ├── foyer_locations      — service areas per shop
    ├── foyer_crews          — crew + trade + truck assignment
    ├── foyer_appointments   — UNIQUE(crew_id, start_time) ASYNC INDEX
    ├── foyer_customers      — homeowner records (phone-keyed)
    ├── foyer_conversations  — per-call message history (CallSid keyed)
    ├── foyer_calls          — call metadata + outcomes
    └── foyer_quotes         — ballpark history
    │
    ▼
TwiML response → next Gather OR confirmation + SMS

The headline architectural bet is the atomic crew booking guarantee. When Claude calls the book_appointment tool, the API route writes a single INSERT into foyer_appointments with (crew_id, start_time) constrained as UNIQUE via Aurora DSQL's CREATE INDEX ASYNC feature. Because DSQL enforces global strong consistency, two concurrent INSERTs from the office line and the website chat physically cannot both succeed — the second one returns a constraint violation, and Claude immediately offers the homeowner a different window. This collision-handling is built into the voice turn route at src/app/api/twilio/voice/turn/route.ts:130 — when the booking returns slot_taken, the TwiML response is a new <Gather> with "let me find another time" instead of hanging up.

The voice flow itself runs over Twilio's standard <Gather input="speech"> mechanic. Each turn is a stateless HTTP round-trip: Twilio sends the speech transcript, the route loads the per-CallSid conversation from foyer_conversations, passes the full message history to Claude with a tool-use schema, persists Claude's reply, and returns TwiML with the spoken response + next Gather. Conversation state lives in the database, not in memory — so a 10-minute call survives any Vercel function recycling.

Per-customer numbers auto-provision on shop signup. The /api/numbers/provision route searches Twilio's available local numbers (biased to the shop's primary area code so the callback shows up local), buys one, sets the voice webhook URL, and saves the E.164 number to foyer_shops.twilio_phone_number. No manual number-buying step for the operator.

Tech Stack

Layer Technology
AI Model Anthropic Claude Sonnet 4.6
Voice Twilio Programmable Voice + ConversationRelay-style <Gather> loop
Database Amazon Aurora DSQL (foyer_* tables)
Backend Next.js 16 API Routes (App Router, Fluid Compute)
Auth Supabase Auth + Google OAuth
Payments Stripe (subscriptions + webhooks)
Email Resend (templata.org)
Frontend Next.js 16, React 19, Tailwind CSS, shadcn/ui
Deployment Vercel (Fluid Compute, us-east-1)

Challenges

  • The atomic booking guarantee, stateless — getting (crew_id, start_time) uniqueness to actually hold across two concurrent writers required CREATE UNIQUE INDEX ASYNC (DSQL's flavor) plus surfacing the constraint violation as a recoverable error in the voice turn route — not a 500. The first version dropped the call when collisions hit. The fix was wrapping the appointment INSERT in a try/catch that reads result.reason === "slot_taken" and returns a new <Gather> TwiML so Claude just picks another time naturally.
  • Stateless voice with stateful conversation — Twilio's <Gather> model is one HTTP round-trip per turn, so the whole conversation has to be reconstructible from the CallSid alone. The foyer_conversations table stores the entire message history as a JSONB array indexed by (shop_id, caller_phone, status='active') — every turn reads the array, appends the new user message, sends to Claude, appends the response, writes back. No in-memory session state survives, but the call does.
  • Per-shop branding without a fork per shop — every shop has its own dispatch tone, service catalog, pricing playbook, and crew roster. Cramming all that into a Claude system prompt without paying for 32k context per turn meant collapsing the playbook into a few-hundred-token brief and lazy-loading specific services only when the homeowner mentions them.
  • Number provisioning on signup, not on demand — the first cut required the operator to manually paste a Twilio number into shop settings, which was the single biggest activation drop-off. Rebuilt as /api/numbers/provision: the moment a shop creates their first crew, the system buys them a local number biased to their area code, sets the webhook, and saves it. Operator clicks zero buttons.

What's Next

  • SMS booking flow — homeowner texts the shop, same Claude + same database, just no voice
  • Calendar export — push every confirmed appointment to the crew's Google Calendar so the truck driver sees it on his phone
  • No-show prediction — flag appointments where the historical no-show rate is high and surface a confirmation prompt
  • Pricing-playbook ingestion — drop in a PDF of the shop's rate card, Claude extracts the structured services + ballpark ranges automatically
  • Multi-language support — Spanish-speaking homeowner calls a shop with a Spanish-speaking dispatcher tone, Claude switches mid-call
  • Crew chat handoff — when Claude can't resolve (e.g. permit question), it sends a structured handoff card to the on-call tech's phone with the captured context

Sample Call Scenarios

Foyer ships with 7 sample inbound-call scenarios you can simulate via /api/calls/simulate without buying a number or making an actual phone call:

  • Emergency burst pipe
  • AC not cooling
  • No hot water
  • Panel upgrade
  • Drain clog
  • Shingle damage
  • After-hours service call

Each runs a full Claude conversation against seeded shop data (Bayou Bros HVAC + Plumbing in New Orleans), produces a real foyer_calls row with transcript, a foyer_appointments row with the booked crew + slot, and (where applicable) a foyer_quotes row with the ballpark. Judges can replay every flow without a Twilio number on hand.


Team

Built solo by @kyisaiah47 for the H0 Hackathon.

Built With

Share this project:

Updates