Goal: a user fills out preferences, HOTNITE selects 3 LA events, calls at a scheduled time, accepts basic voice commands, and texts the selected links.
| Layer | Use | Reason |
|---|---|---|
| Frontend | Next.js / Vercel | Fastest route to forms, API routes, deployment. |
| Database | Supabase Postgres | Hosted Postgres, easy JS client, admin table visibility. |
| Voice + SMS | Twilio Programmable Voice + Messaging | Outbound call and SMS primitives exist. |
| Voice brain | OpenAI Realtime or server-side LLM with Twilio media/control | Use tool calls/state machine. Do not freewheel. |
| Jobs | Vercel Cron / Supabase scheduled job / simple worker | Trigger scheduled calls. |
| Maps | Google Maps links first | No Maps API needed for MVP. |
Principle: boring web app + phone integration + strict state machine. Avoid clever architecture.
users
- id uuid pk
- phone text unique not null
- name text
- neighborhood text
- home_lat numeric null
- home_lng numeric null
- radius_miles int default 5
- max_budget_usd int null
- preferred_call_time time
- timezone text default 'America/Los_Angeles'
- active boolean default true
- created_at timestamptz default now()
taste_profiles
- id uuid pk
- user_id uuid references users(id)
- likes text[]
- hates text[]
- hard_no text[]
- vibe_positive text[]
- vibe_negative text[]
- mobility_notes text
- notes text
- updated_at timestamptz default now()
events
- id uuid pk
- title text not null
- venue text
- address text
- neighborhood text
- starts_at timestamptz not null
- ends_at timestamptz null
- price_min int null
- price_max int null
- ticket_url text
- source_name text
- source_url text
- categories text[]
- vibe_tags text[]
- hard_no_tags text[]
- description text
- curator_notes text
- status text default 'candidate' -- candidate/approved/rejected/expired
- created_at timestamptz default now()
recommendations
- id uuid pk
- user_id uuid references users(id)
- event_id uuid references events(id)
- call_session_id uuid null
- rank int
- score numeric
- match_reasons text[]
- risk_flags text[]
- status text default 'queued' -- queued/presented/skipped/selected/texted
- created_at timestamptz default now()
call_sessions
- id uuid pk
- user_id uuid references users(id)
- twilio_call_sid text null
- status text default 'scheduled' -- scheduled/active/completed/failed
- scheduled_for timestamptz
- started_at timestamptz null
- ended_at timestamptz null
- current_index int default 0
- transcript jsonb default '[]'
call_events
- id uuid pk
- call_session_id uuid references call_sessions(id)
- event_type text -- agent_said/user_said/command/tool/sms/error
- payload jsonb
- created_at timestamptz default now()
feedback
- id uuid pk
- user_id uuid references users(id)
- event_id uuid references events(id)
- signal text -- yes/no/next/more_like_this/less_like_this/too_far/too_expensive
- notes text
- created_at timestamptz default now()
Phone, neighborhood, radius, budget, categories, hates, hard no tags, preferred call time.
Manual entry. 30–50 approved LA events. Tag heavily.
At call time, rank events for user and create recommendation rows.
Call connects to voice route. Voice agent receives pre-ranked list and state.
Short description. Then waits for command. No rambling.
NEXT increments index. GO BACK decrements. REPEAT replays. YES/TEXT ME stores selection.
Title, time, address, price, ticket/source link, map link.
Every NO/NEXT/YES becomes future ranking signal.
| User says | System action | Persist |
|---|---|---|
| NO / NEXT | Skip current event. Present next. | feedback.signal = next |
| REPEAT | Replay current event summary. | call_events entry |
| GO BACK | Move to previous event. Replay. | call_events entry |
| YES | Mark current event selected. Offer text link. | recommendation.status = selected |
| TEXT ME | Send SMS for current or selected events. | recommendation.status = texted |
| MORE LIKE THIS | Positive taste signal. Continue. | feedback.signal = more_like_this |
| LESS LIKE THIS | Negative taste signal. Continue. | feedback.signal = less_like_this |
| STOP / BYE | End call gracefully. | call_sessions.status = completed |
No ML. Deterministic scoring + optional LLM explanation.
score_event(user, profile, event):
score = 0
if event.category in profile.likes: score += 20
if event.category in profile.hates: score -= 40
score += 8 * count(overlap(event.vibe_tags, profile.vibe_positive))
score -= 12 * count(overlap(event.vibe_tags, profile.vibe_negative))
score -= 100 * count(overlap(event.hard_no_tags, profile.hard_no))
if event.price_max > user.max_budget_usd: score -= 25
if event.distance_miles > user.radius_miles: score -= 30
if event.starts_at too soon: score -= 10
if event.starts_at too late: score -= 10
if event.source_name in trusted_sources: score += 5
if curator_notes contains "strong": score += 10
return score
{
"event_id": "uuid",
"rank": 1,
"score": 72,
"match_reasons": ["nearby", "cheap", "matches repertory film", "not crowded"],
"risk_flags": ["starts in 70 minutes"]
}
Do not automate all sources in v0. Use these as admin inputs.
POST /api/intake
- create user + taste_profile
POST /api/admin/events
- create/update event
POST /api/recommendations/generate
- user_id + date window -> recommendation rows
POST /api/calls/schedule
- user_id + scheduled_for -> call_session
POST /api/calls/start
- call_session_id -> Twilio outbound call
POST /api/voice/webhook
- Twilio webhook / call events
POST /api/voice/command
- parsed command -> update call session state
POST /api/sms/send-selection
- send selected event details
POST /api/feedback
- persist taste signal
MVP complete when: one real user gets one useful call and one useful SMS from manually curated LA events.
SUPABASE_URL=
SUPABASE_SERVICE_ROLE_KEY=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_PHONE_NUMBER=
OPENAI_API_KEY=
ADMIN_SECRET=
APP_BASE_URL=
Build a Next.js + Supabase + Twilio prototype where a scheduled outbound call reads ranked LA event recommendations, obeys NEXT/REPEAT/GO BACK/TEXT ME, and sends SMS links.