AI-powered patient engagement chatbot for dental practices. Built as a portfolio project for AI consulting.
- RAG Chatbot — Answers patient questions via Pinecone + OpenAI GPT-4o
- Appointment Booking — Collects patient info and presents a Calendly booking link
- CRM Sync — Creates/updates contacts and logs conversation summaries to HubSpot
- Admin Dashboard — Clerk-authenticated portal for the practice owner
| Layer | Tech |
|---|---|
| Framework | Next.js 14 (App Router, TypeScript strict) |
| Hosting | Vercel |
| Auth | Clerk (admin only) |
| Database | Supabase (PostgreSQL) |
| Vector DB | Pinecone (demo-dental namespace) |
| LLM | OpenAI GPT-4o |
| Embeddings | OpenAI text-embedding-3-small |
| Scheduling | Calendly v2 API |
| CRM | HubSpot API v3 |
| Resend (Phase 4) | |
| Styling | Tailwind CSS |
git clone <repo-url>
cd dental-ai-chatbot
npm install- Go to supabase.com and create a new project
- Open the SQL Editor and run the full SQL block below
- Copy your keys from Settings → API:
Project URL→NEXT_PUBLIC_SUPABASE_URLanon / publickey →NEXT_PUBLIC_SUPABASE_ANON_KEYservice_rolekey →SUPABASE_SERVICE_ROLE_KEY
Click to view full SQL schema
-- conversations
create table if not exists conversations (
id uuid primary key default gen_random_uuid(),
created_at timestamptz default now(),
patient_name text,
patient_email text,
patient_phone text,
messages jsonb,
intent_summary text,
booking_url_presented boolean default false,
hubspot_contact_id text,
tenant_id text default 'demo-dental'
);
-- documents
create table if not exists documents (
id uuid primary key default gen_random_uuid(),
created_at timestamptz default now(),
name text not null,
type text not null check (type in ('pdf', 'website')),
status text not null default 'processing' check (status in ('processing', 'ready', 'error')),
chunk_count integer default 0,
tenant_id text default 'demo-dental'
);
-- tenant_settings
create table if not exists tenant_settings (
id uuid primary key default gen_random_uuid(),
tenant_id text unique default 'demo-dental',
practice_name text,
calendly_scheduling_url text,
calendly_event_type_uri text,
hubspot_access_token text,
emergency_phone text,
welcome_message text,
updated_at timestamptz default now()
);
-- Enable RLS
alter table conversations enable row level security;
alter table documents enable row level security;
alter table tenant_settings enable row level security;
-- Permissive policies (tighten before multi-tenant production)
create policy "Service role full access" on conversations for all using (true) with check (true);
create policy "Service role full access" on documents for all using (true) with check (true);
create policy "Service role full access" on tenant_settings for all using (true) with check (true);
-- Seed initial settings row
insert into tenant_settings (
tenant_id, practice_name, welcome_message, emergency_phone
) values (
'demo-dental',
'Demo Dental Practice',
'Hi 👋 I''m your dental assistant. How can I help you today?',
'(555) 000-0000'
) on conflict (tenant_id) do nothing;- Go to clerk.com and create a new application
- Choose Email + Password (or add social logins as desired)
- Copy your keys from the Clerk Dashboard:
Publishable Key→NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYSecret Key→CLERK_SECRET_KEY
- In Clerk Dashboard → Paths, set:
- Sign-in URL:
/admin/sign-in - After sign-in URL:
/admin/dashboard
- Sign-in URL:
- Go to pinecone.io and create an index
- Dimensions: 1536, Metric: cosine
- Copy your API key →
PINECONE_API_KEY - Copy your index name →
PINECONE_INDEX
- OpenAI: Get API key from platform.openai.com
- Calendly: Get API key and set up an event type in your Calendly account
- HubSpot: Create a private app in HubSpot and copy the access token
- HubSpot Portal ID (optional): Found in HubSpot → Account & Billing. Used to generate direct links to contacts in the dashboard.
Create .env.local in the project root:
# OpenAI
OPENAI_API_KEY=
# Pinecone
PINECONE_API_KEY=
PINECONE_INDEX=
# Calendly
CALENDLY_API_KEY=
CALENDLY_EVENT_TYPE_URI=https://api.calendly.com/event_types/XXXX
CALENDLY_SCHEDULING_URL=https://calendly.com/yourname/30min
# HubSpot
HUBSPOT_ACCESS_TOKEN=
NEXT_PUBLIC_HUBSPOT_PORTAL_ID= # Optional — enables direct contact links in dashboard
# Resend (Phase 4)
RESEND_API_KEY=
# Supabase
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/admin/sign-in
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/admin/dashboardnpm run dev- Chatbot: http://localhost:3000
- Admin Dashboard: http://localhost:3000/admin/sign-in
Bulk-ingest all PDFs from the /data folder:
npm run ingest -- --pdfIngest Single pdf
npm run ingest -- --file=crowns-bridges.pdf
Ingest a website URL:
npm run ingest -- --url=https://yourpractice.com/servicesYou can also ingest documents through the admin dashboard at /admin/documents.
The SQL above seeds a default tenant_settings row for demo-dental. After signing in to the admin dashboard, go to Settings to update:
- Practice name
- Chatbot welcome message
- Emergency phone number
- Calendly URLs
- HubSpot access token
| Route | Description |
|---|---|
/admin/sign-in |
Clerk-authenticated login |
/admin/dashboard |
Analytics overview |
/admin/conversations |
All patient chat sessions |
/admin/conversations/[id] |
Full transcript + patient details |
/admin/documents |
Manage RAG data sources |
/admin/settings |
Practice configuration |
| Phase | Status |
|---|---|
| Phase 1 — RAG chatbot (Pinecone + GPT-4o) | ✅ Complete |
| Phase 2 — Calendly booking + HubSpot CRM | ✅ Complete |
| Phase 3 — Admin dashboard (Clerk + Supabase) | ✅ Complete |
| Phase 4 — Multi-tenancy + Stripe + webhooks + email | 🔲 Not Started |
- Single-tenant for now (
tenant_id = 'demo-dental'hardcoded). Multi-tenancy requires adding atenantIdlookup layer — no rewrites. - Uses
openai.chat.completions.create()— never the Responses API - Single OpenAI client in
lib/openai.ts— never instantiate a second - HubSpot and Resend calls are fire-and-forget — never block chat response
- Supabase conversation saves are fire-and-forget — never break chat
- Settings are cached for 60s in
lib/settings.tsto avoid DB hits per message