Vitalency: Technical Specification
Status: v0.1 architecture sketch, 2026-04-25
Source plan: MASTER-PLAN.md
This document refines the architecture from MASTER-PLAN.md into engineering specifics. It's the document a developer (or Charlie) reads when building any agent or integration.
1. System overview
┌────────────────────────────────────┐
│ EVENT INGESTION (every 20 min) │
│ ─ Mindbody / Vagaro polling │
│ ─ Twilio webhooks (SMS/voice) │
│ ─ Lead-form webhooks (Meta, web) │
└────────────────┬───────────────────┘
│
▼
┌────────────────────────────────────┐
│ CENTRAL BRAIN (decision engine) │
│ ─ Update client profiles │
│ ─ Recompute predictions: │
│ next_visit_due │
│ churn_risk │
│ upsell_score │
└────────────────┬───────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ REBOOKING │ │ NO-SHOW │ │ LEAD CONVERSION │
│ AGENT │ │ RECOVERY │ │ AGENT │
│ │ │ AGENT │ │ │
└──────┬───────┘ └──────┬───────┘ └────────┬─────────┘
│ │ │
▼ ▼ ▼
┌────────────────────────────────────────────────────┐
│ ACTION LAYER │
│ ─ Compose message (LLM, brand-voice-aware) │
│ ─ Pick channel (SMS / email / call-back) │
│ ─ Schedule send │
│ ─ Log outcome │
└────────────────────────────────────────────────────┘
+ INBOUND: Voice booking agent (always-on, Twilio/Vapi)
+ INBOUND: Subscription optimization engine (weekly job)
2. Data model
Postgres schema (target, see schemas/*.sql once written)
clients
CREATE TABLE clients (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
spa_id UUID NOT NULL REFERENCES spas(id),
external_id TEXT, -- Mindbody/Vagaro client ID
name TEXT NOT NULL,
phone TEXT, -- E.164
email TEXT,
treatments JSONB DEFAULT '[]', -- [{name, last_done, count, avg_spend}]
last_visit TIMESTAMPTZ,
total_spent NUMERIC(10,2) DEFAULT 0,
visit_count INT DEFAULT 0,
tags TEXT[] DEFAULT '{}', -- vip, churn-risk, no-show-prone
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX clients_spa_lastvisit ON clients(spa_id, last_visit DESC);
events
CREATE TABLE events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
spa_id UUID NOT NULL,
client_id UUID REFERENCES clients(id),
type TEXT NOT NULL, -- booking, no-show, lead, call, sms
payload JSONB, -- raw event data
occurred_at TIMESTAMPTZ NOT NULL,
processed_at TIMESTAMPTZ
);
CREATE INDEX events_spa_processed ON events(spa_id, processed_at);
CREATE INDEX events_client_time ON events(client_id, occurred_at DESC);
predictions
CREATE TABLE predictions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_id UUID NOT NULL REFERENCES clients(id),
next_visit_due TIMESTAMPTZ,
churn_risk NUMERIC(3,2), -- 0.00–1.00
upsell_score NUMERIC(3,2), -- 0.00–1.00
upsell_target TEXT, -- name of recommended treatment/subscription
rationale TEXT, -- LLM-generated explanation
computed_at TIMESTAMPTZ DEFAULT now()
);
calls
CREATE TABLE calls (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
spa_id UUID NOT NULL,
client_id UUID REFERENCES clients(id),
direction TEXT, -- inbound, outbound
transcript TEXT,
intent TEXT, -- book, reschedule, cancel, faq, complaint, other
outcome TEXT, -- booked, transferred, dropped, escalated
duration_sec INT,
started_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ
);
subscriptions
CREATE TABLE subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
client_id UUID NOT NULL REFERENCES clients(id),
plan TEXT NOT NULL, -- spa-defined plan name
credits NUMERIC(10,2), -- if credit-based
renewal_date DATE,
status TEXT DEFAULT 'active', -- active, paused, churned
created_at TIMESTAMPTZ DEFAULT now()
);
Tenant isolation
Every data row carries spa_id. Queries are spa-scoped at the DB layer (RLS in v2; app-layer filter in v1). One client = one Postgres database, partitioned by spa_id row-level until volume justifies separate DBs per spa.
What we explicitly do NOT store
- Treatment details that constitute PHI under HIPAA (most medspa Botox/filler/laser data is not PHI; medical-weight-loss / hormone / IV-nutrition data IS, flag at intake)
- Payment card numbers (Stripe / square processes; we hold tokens only)
- Photos / images of clients (out of scope for v1)
- Session notes from clinical encounters
3. Agent specs
3.1 Rebooking Agent
Trigger cadence: every 20 min, scans clients where now() > predictions.next_visit_due and no booking event in the future.
Decision logic:
- Fetch client + treatment history
- For each treatment, look up the spa's configured cadence (Botox 90d, filler 6mo, laser package per package terms)
- If
last_visit + cadence < now()and no future booking, queue rebooking action - Pick channel: SMS if phone + opted-in; else email
- Compose message (LLM, brand-voice-tuned, NO "your treatment is overdue!" robotic tone)
- Send via Twilio / SMTP
- Log to
events(type=outbound_rebook)
Key parameters per spa:
- Treatment cadence map
- Brand voice profile (tone snippet, dos/don'ts, sample messages)
- Quiet hours (don't text after 9 PM local)
- Opt-out list
3.2 No-Show Recovery Agent
Trigger: appointment status flipped to no_show in Mindbody/Vagaro.
Decision logic:
- Within 30 min of no-show: send empathetic recovery SMS with one-tap rebooking link
- If no response in 24 hr: second message, soft tone, possibly with small re-engagement offer (spa-configured)
- If no response in 72 hr: flag for human follow-up (front desk receives a daily digest)
3.3 Lead Conversion Agent
Trigger: lead webhook from Meta / web form / Instagram DM.
Decision logic:
- Within 5 min: respond with consultation-booking link + one-line warm hook
- Score lead intent (LLM): high / medium / low
- High-intent: continue conversation, book consultation directly
- Medium: nurture with 2-3 message sequence over 5 days
- Low: archive, optional re-engagement at 30 days
3.4 Voice Booking Agent
Tech: Vapi or Retell (both abstract over Twilio Voice + LLM + STT/TTS). Decide based on Mindbody/Vagaro integration support.
Call flow (per MASTER-PLAN):
- Greeting (spa-branded), "Thanks for calling [Spa Name]. I'm the booking assistant. How can I help?"
- Intent detection, book / reschedule / cancel / FAQ / complaint
- Qualification, name, phone, treatment desired, preferred time window
- Offer time slots, query Mindbody for openings
- Confirm + book
- SMS confirmation
- Escalate to human if: complaint, complication, "not a good fit" detection, repeated misunderstanding
Hard rule: any sentence the agent says that touches dosing, contraindications, "is X safe for me," or post-procedure symptoms triggers an immediate human transfer. The agent literally cannot say "yes you can take...", that's a transfer trigger.
3.5 Subscription Optimization Engine
Cadence: weekly batch.
Logic:
- For each client with ≥3 visits and ≥6 months tenure: compute treatment-frequency model
- If frequency × annual price > spa's subscription tier: client is a candidate
- LLM generates: (a) staff talking-point card for next visit, (b) optional client-facing nudge
- Front-desk dashboard shows top 10 subscription candidates per week
4. Integration adapters
4.1 Mindbody
- Auth: OAuth 2.0
- Pull: appointments, clients, services, sales (incremental via
LastModifiedDateTime) - Push: create appointment, update client tag
4.2 Vagaro
- Auth: API key
- Pull / push: similar to Mindbody but flatter API
- Lower priority than Mindbody (smaller medspa share)
4.3 Twilio
- SMS send + receive (opt-out compliance: STOP keyword, log to
subscriptions_optout) - Voice (if not using Vapi/Retell stack on top)
4.4 Email (Sendgrid / Postmark / Mailgun)
- Decide based on Annette's existing accounts
5. Brand-voice-per-spa system
Each spa onboards with a voice profile:
spa_id: <uuid>
spa_name: "Glow Aesthetics Boulder"
voice_profile:
tone: "warm, professional, slightly elevated; sounds like a luxury hotel concierge, not a tech company"
dos:
- "use the client's first name once per message"
- "reference the specific treatment they had"
- "acknowledge the time since last visit gently"
donts:
- "no exclamation marks"
- "no emojis except heart for VIP-tagged clients"
- "no urgency language ('don't miss out')"
sample_message: "Hi Sarah, it's been about 12 weeks since your last Botox visit. Looking ahead, would you like to schedule for the week of June 3rd? Happy to pencil something in."
quiet_hours: "21:00-08:00 America/Denver"
The LLM composes every outbound message conditioned on this profile. Audit trail: every message logs the prompt + the model's output for compliance.
6. Security + compliance
- Auth: per-spa admin login (email + magic link or SSO)
- Tenant isolation:
spa_idenforced at app + DB layer - TCPA compliance: SMS opt-in tracked; STOP/HELP handled
- Data retention: configurable per spa (default 24 months of event history)
- Backups: daily Postgres snapshots, 30-day retention
- PII handling: phone/email logged; medical detail (treatment notes) NOT logged in plaintext at LLM layer (use treatment-name-only abstractions for prompt)
7. Open architectural questions
| # | Question | Owner |
|---|---|---|
| 1 | Python or Node for orchestration? | Annette |
| 2 | Self-host on VPS or Vercel/Railway? | Charlie recommends VPS once VPS is stood up (Phase 2 of MyExecAssistant). Vercel/Railway in interim. |
| 3 | Voice stack: Vapi vs. Retell vs. roll-our-own on Twilio? | Spike all three week 1 of Boulder, pick by integration depth |
| 4 | Multi-tenant Postgres vs. DB-per-spa? | Multi-tenant w/ RLS until 50+ spas; revisit then |
| 5 | LLM provider for message composition? | DeepSeek V3.1 via OpenRouter for short-form per global rules; Opus 4.7 for high-stakes voice calibration / brand-voice training |
8. v1 ship definition
Minimum viable Revenue Brain (what client #1 in Boulder gets, $199–$399/mo Starter or Growth tier):
- Mindbody integration polling every 20 min
- Rebooking Agent firing SMS via Twilio
- No-Show Recovery Agent firing SMS within 30 min of marking
- Front-desk dashboard showing daily action queue
- Brand-voice profile configured for that one spa
Not in v1: voice agent, lead conversion, subscription engine, multi-spa support. Those are Premium/Pro tier features added in v2 and v3.
Rule: client #1's contract should reflect Starter or Growth pricing, the system delivers MORE than the Starter scope to ensure delight, but billing matches the tier sold.