A full-stack, production-grade dental clinic platform — mobile patient app + staff web dashboard — built as a single monorepo.
Author: GiorgiKavtaradze · Repo: GiorgiKavtaradze-prog/dental-app
Dental App is a monorepo combining a patient-facing mobile application and a staff web dashboard for a single dental practice — designed for real-world clinic operation from day one.
- Patients sign in with Apple or Google, complete a medical intake, book real appointments from live clinic availability, securely chat with staff, and join scheduled video consultations — all from their phone.
- Staff manage the daily schedule, patient records, chat threads, and post-op notes from a Next.js dashboard that doubles as the REST API and webhook server.
Two core principles govern every design decision:
| Principle | Detail |
|---|---|
apps/web is the backend |
It owns the Postgres schema (Drizzle ORM on Neon), all business logic, and every external integration. The mobile app is a thin rendering layer. |
| Third parties own the minimum | Clerk holds identity only; Stream holds call and message content only. Everything clinical lives in our own database, with PHI reads and writes recorded in audit_log. |
| Area | Description |
|---|---|
| 🔐 Authentication | Apple + Google SSO via Clerk; roles (patient, staff, dentist) mirrored into Postgres by webhook |
| 📋 Onboarding | 4-step intake — profile, medical history, primary concern, notifications permission |
| 📅 Smart Booking | Real availability = working hours − time-off − existing bookings; duration computed server-side; double-booking blocked by a Postgres exclusion constraint |
| 📹 Teleconsults | Scheduled video/audio calls via Stream Video (WebRTC); room ID is derived server-side (appointment-{id}), never client-supplied |
| 💬 Clinic Chat | Secure patient ↔ clinic messaging via Stream Chat with photo attachments |
| 🤖 AI Assistant | Education & triage only (gpt-4o-mini); emergency-keyword guard runs before any model call; patient photos are never sent to OpenAI |
| 👨👩👧 Family Accounts | One user account, N patient profiles — exactly one "self" profile; appointments reference patients, not users |
| 🗂️ Visit History | Complete past-appointment list with staff-authored post-op notes |
| 🌐 Public Site | Landing page, Terms of Service, and Privacy Policy (draft banner until legal sign-off) |
| 📡 Observability | Sentry on both apps; PHI never leaves the compliance boundary |
flowchart TB
Mobile["📱 Mobile App — Expo SDK 57 / React Native"]
Web["🌐 Staff Dashboard — Next.js 16 App Router"]
Clerk["🔐 Clerk — Auth, Apple/Google SSO, roles"]
API["⚙️ /api/* — Route Handlers + Zod validation"]
Webhooks["🔔 /api/webhooks — Clerk & Stream events"]
AI["🤖 /api/ai — OpenAI gpt-4o-mini assistant"]
Neon["🗄️ Neon Postgres — PHI data + audit_log"]
Stream["💬 Stream — Video WebRTC + Chat messages"]
ImageKit["🖼️ ImageKit — Private patient media CDN"]
Sentry["📡 Sentry — Error tracking (both apps)"]
Mobile -->|"ClerkProvider, Apple/Google SSO"| Clerk
Mobile -->|"REST over HTTPS"| API
Mobile -->|"WebRTC + chat SDK"| Stream
Web -->|"@clerk/nextjs middleware"| Clerk
Web --> API
API -->|"auth() → userId, orgId, role"| Clerk
API -->|"Drizzle ORM queries"| Neon
AI -->|"emergency keyword guard → model call"| Neon
Webhooks -->|"mirror users & roles"| Neon
Webhooks --> Stream
API --> Stream
API --> ImageKit
Mobile --> Sentry
Web --> Sentry
flowchart LR
R["Patient taps 'Book Appointment'"] --> Z["Zod validates request body"]
Z --> C["Clerk auth() resolves userId + role"]
C --> A["Server computes real availability"]
A --> X["Working hours − time-off − existing bookings"]
X --> E["Postgres exclusion constraint\nblocks double-booking at DB level"]
E --> S["Stream room ID derived server-side\nappointment-{id} — never client-supplied"]
S --> L["audit_log records PHI write"]
L --> B["✅ Appointment confirmed"]
flowchart LR
Req["Incoming request\n(mobile or browser)"] --> Auth{"Clerk auth()"}
Auth -->|"unauthenticated"| Deny["❌ 401 Unauthorized"]
Auth -->|"authenticated"| Role{"Role check"}
Role -->|"wrong role"| Forbidden["❌ 403 Forbidden"]
Role -->|"patient / staff / dentist"| Own{"Resource ownership\nassertOwned(id, userId)"}
Own -->|"not owner"| Forbidden2["❌ 403 Forbidden"]
Own -->|"owner"| AI{"AI route?"}
AI -->|"yes"| Guard["Emergency-keyword guard\nruns before OpenAI call\nPatient photos stripped"]
AI -->|"no"| DB["Drizzle ORM → Neon Postgres"]
Guard --> DB
DB --> Audit["audit_log records every PHI read/write"]
Audit --> Resp["✅ Response"]
- Node.js ≥ 22 and npm
- A Neon Postgres database (free tier works)
- API keys for: Clerk, Stream, OpenAI, ImageKit, Sentry (all have free tiers)
npm installcp apps/web/.env.example apps/web/.envEdit apps/web/.env and fill in the following:
| Variable | Purpose |
|---|---|
DATABASE_URL |
Neon Postgres connection string |
CLERK_SECRET_KEY |
Clerk server-side secret key |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY |
Clerk client-side publishable key |
NEXT_PUBLIC_CLERK_SIGN_IN_URL |
Route for sign-in page (/sign-in) |
NEXT_PUBLIC_CLERK_SIGN_UP_URL |
Route for sign-up page (/sign-up) |
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL |
Post-sign-in redirect URL |
NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL |
Post-sign-up redirect URL |
NEXT_PUBLIC_STREAM_API_KEY |
Stream Chat + Video (public key) |
STREAM_API_SECRET |
Stream server-side secret |
OPENAI_API_KEY |
AI assistant API key |
IMAGEKIT_PUBLIC_KEY |
ImageKit upload authentication |
IMAGEKIT_PRIVATE_KEY |
ImageKit server operations |
IMAGEKIT_URL_ENDPOINT |
ImageKit CDN base URL |
NEXT_PUBLIC_SENTRY_DSN |
Sentry error tracking DSN |
CLINIC_TZ |
Clinic timezone for all scheduling (default: America/New_York) |
npm run db:migrate # Apply Drizzle schema migrations to Neon
npm run db:seed # Seed demo data: dentists, services, working hours
npm run db:seed:stream # Mirror demo users into StreamTip: Run
npm run db:studioto open Drizzle Studio for a visual database browser.
npm run web # → http://localhost:3000npm run mobile # Start Metro bundler
# Build and run locally
npx expo run:ios # iOS simulator or physical device
npx expo run:android # Android emulator or physical device
# Build with EAS
npm run build:ios
npm run build:androidnpm run test # Run Vitest unit suite (apps/web)
npm run typecheck # TypeScript type-check all workspacesReleased under the MIT License — Copyright © 2026 GiorgiKavtaradze.
Third-party services used by this project (Clerk, Stream, OpenAI, ImageKit, Sentry, Neon) are governed by their own terms of service. This license covers the repository source code only.
Made with ❤️ by GiorgiKavtaradze
