DentaPilot
An AI assistant for a dental practice that turns one chat endpoint into eight different kinds of structured record: appointments, patient notes, finance, and more.
- Role
- iOS Client & Go Backend
- Platform
- iOS (iPhone), SwiftUI
- Stack
- Swift · SwiftUI · Go · REST + Strapi CMS
- Scale
- 5 domains, 1 app: chat, calendar, patients, marketplace, finance
A dental practice, run from one tab bar.
DentaPilot puts an AI chat and voice assistant ("Pilot"), patient charting, appointment scheduling, a supply marketplace, and practice finances behind one iPhone app. I designed it end to end: the SwiftUI client and the Go backend it talks to, including the chat and voice integration.
The problem
A dental practice normally runs on several disconnected systems: a chart, a calendar, a supplier catalog, a books ledger. The goal was one conversational assistant in front of all of them, so a dentist could ask a plain-language question and get back the right kind of answer: an appointment card, a patient note, a finance line, or just a reply. The hard part was never the conversation itself; it was turning one generic chat endpoint into eight different structured UI results without exposing eight different endpoints to get there.
Constraints
- iOS 18+, iPhone only. A deliberately narrow platform target, no backward compatibility to carry.
- No offline mode. Every screen is request/response over one shared network layer; the app assumes connectivity.
- Two backends to keep in sync from the client. A Go REST API serves chat, patients, calendar, and finance; a separate CMS serves the marketplace catalog.
- The assistant's replies double as UI content. Backend prompting and the client's parser both have to agree on a shape that was never formalized as a typed API contract.
- A small, fast-moving build. No dedicated QA layer yet; every change today is validated by hand.
One endpoint, eight shapes.
A single repository façade fronts four small, protocol-scoped repositories, all sharing one network transport. The assistant's replies are classified client-side into typed UI, not typed at the API layer.
A message is appended to the chat optimistically, then sent through MainRepository to NetworkService, which attaches the device's X-User-ID header and posts to the Go backend's /chat endpoint. The response is one JSON object with a single content string. ChatParsedContent classifies that string (by a type prefix, by attempting a JSON decode, or by falling back to plain text) into one of eight result types, each rendered as its own kind of card. Voice notes follow the same path through a separate multipart upload, gated by an on-device voice-activity detector that decides when the user has actually finished speaking.
Decisions that kept it generic.
One chat endpoint, many result types
The assistant returns a single content string; the client classifies it into one of eight UI types by prefix or JSON shape. The backend's conversational surface stays one endpoint regardless of what the assistant decides to return.
A device-ID header instead of a token scheme
Authenticated requests carry a single Keychain-stored user ID as a header. It's the simplest client-side auth that could ship, chosen over bearer tokens or refresh rotation.
One transport, four narrow repositories
A single NetworkService implements one generic request method; four small protocol-scoped repositories (chat, voice, marketplace, user) compose it into MainRepository. Each feature's network surface stays small and mockable.
Two backends, split by concern
Practice data (chat, patients, calendar, finance) lives on the Go REST API. The marketplace catalog is served from a separate content-managed CMS, since a product catalog is naturally content-managed rather than hand-administered through the app's own backend.
The client decides when you've stopped talking
An adaptive, multi-signal voice-activity detector (noise-floor calibration, loudness smoothing, a natural-pause/breath-pause/end-of-speech classifier) decides when to send a voice note, replacing an earlier fixed-threshold version that either cut speakers off or left dead air.
Generic is fast to build, fragile to parse.
One generic endpoint over typed per-action endpoints
The assistant can return any of eight result shapes without a new endpoint per feature. New capabilities are a client-side parsing case, not a backend release.
Client-side parsing is inherently fragile: string-prefix sniffing and best-effort JSON extraction that falls back to an error bubble whenever the assistant's output doesn't decode cleanly.
Hand-rolled networking over a heavier framework
Fast iteration with almost no third-party dependency surface.
No retry/backoff policy, no token refresh, no offline cache yet. That's reliability work the client currently has to do without.
Adaptive voice detection over a fixed timeout
Shorter, more natural pauses for quick utterances instead of a flat one-second wait every time.
The detector still needs several consecutive quiet moments before it commits, so very short utterances don't yet get the full benefit the adaptive design intends.
Turning free-form text into a UI.
Parsing whatever the assistant sends back
Content arrives as plain text, JSON wrapped in a message key, a prefixed-and-JSON payload, or occasionally a partial response the model cut off mid-stream. The parser extracts the substring between the first { and the last } before attempting a decode, and falls back to rendering raw text if every structured attempt fails, rather than showing nothing.
Rebuilding voice turn-taking from scratch
The first version used one fixed loudness threshold and a flat second of silence to decide a user was finished. That approach was prone to both cutting off quiet speakers and sitting through dead air after short ones. It was replaced months later with a calibrated, multi-signal detector once real use exposed the limits of the simple version.
Reconciling two backends on the client
Practice data on the Go REST API and the marketplace catalog on a separate CMS have no shared schema beyond what the client itself reconciles. Keeping both feeling like one app is a client-side responsibility.
Shipping without accounts, for a while
Login and session handling arrived relatively late in the build's life; before that, the app ran without authenticated identity at all, which shaped how much of the early architecture could assume about "the current user."
What's real, and what's next.
Where a fast-moving build stands today.
This is an actively-iterated app, not a hardened release, and the security posture reflects that honestly:
- Identity — requests are authenticated by a single device-held user identifier rather than a token or session scheme. Simple to implement; it means anyone able to set that header could impersonate the account unless the backend adds its own verification.
- Transport — the current build allows plaintext HTTP alongside HTTPS rather than enforcing TLS everywhere, appropriate for active development but not yet locked down for a public release.
- Patient data — chart and appointment content reaches the backend as ordinary JSON, with no client-side redaction step before it's sent; data-handling policy for that content lives with the backend.
- Logging — development builds currently log full request and response bodies for debugging, a practice that needs to be gated out before anything closer to a public release.
None of this is unusual for a build at this stage. It's the honest list of what hardening still has to happen before DentaPilot could be called production-grade.
Nine months, with a real gap in the middle.
Project bootstrap
Initial commit establishes the SwiftUI project skeleton; real feature work starts a few weeks later.
Chat API wired up
The first version of the request/response chat flow ships, non-streaming from day one.
Bot presence, and the first voice pipeline
Lottie-driven listening/thinking/speaking states make the request/response round trip feel alive while waiting; the first AVAudioRecorder-based voice flow ships with a simple fixed-threshold silence detector.
Development pauses for several months
No commits land for roughly five and a half months before work resumes.
Chat parsing generalized
Response parsing moves to the current multi-type classification system, alongside the marketplace CMS integration.
Authentication arrives
Login, signup, and session handling ship for the first time, eight and a half months into the project.
Voice detection rebuilt
The original fixed-threshold voice detector is replaced with the adaptive, multi-signal version, after real use exposed how often the simple version got it wrong.
What a fast build teaches.
- A generic endpoint is fast to build against, but the fragility has to live somewhere. Putting it in the client parser was the right call for speed, though it's worth revisiting against a typed, discriminated API contract as the surface grows.
- A hand-rolled voice-activity detector deserves a real second pass. The first version's assumptions didn't survive contact with how people actually talk.
- Authentication is easier to retrofit than to add correctly under time pressure. Building it in from day one would have shaped a few early decisions differently.
The hard part works; the rest is a known list.
DentaPilot spans five domains behind one AI-assisted tab bar (chat, calendar, patients, marketplace, and finance), backed by a Go REST API and a separate CMS for the marketplace. The core engineering problem, reliably turning one free-form endpoint into eight distinct structured results, is solved and running.
What's left is the ordinary list a small, fast-moving build defers: automated tests, hardened transport, and a proper session scheme are next. They're named honestly here rather than glossed over.