Case study · Health · AI

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
Overview

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.
Architecture

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.

DentaPilot architecture: PilotScreen and voice capture talk through MainRepository and NetworkService to a Go REST backend and a separate Strapi-style CMS for the marketplace. IOS CLIENT PilotScreen Chat UI · ViewModel Voice capture AVAudioRecorder · adaptive VAD ChatParsedContent Classifies content into 8 UI types MainRepository Chat · Voice · Marketplace · User NetworkService URLSession · X-User-ID header AuthenticationService Keychain-backed session state BACKEND Go REST API /chat · /voice-chat · /user /finance/transactions Assistant response One content string, 8 possible shapes Marketplace CMS Products · categories · banners REST
The assistant returns one content string. Everything about whether it becomes a chat bubble, an appointment card, or a finance line is decided on-device, after the response arrives.

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.

Engineering decisions

Decisions that kept it generic.

01

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.

02

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.

03

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.

04

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.

05

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.

Trade-offs

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.

The cost

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.

The cost

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 cost

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.

Technical challenges

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."

Performance & reliability

What's real, and what's next.

8
Structured result types
Appointment, patient, event, course, news, finance, message, or plain text, classified entirely on-device from one endpoint.
0.8–2.5s
Adaptive silence window
Replaced a flat one-second threshold that both cut off quiet speakers and left dead air after short ones.
0
Automated tests, today
The build's biggest honest gap: every change is currently validated by hand, not by a suite.
Security

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.

Engineering journal

Nine months, with a real gap in the middle.

2024 · Kickoff

Project bootstrap

Initial commit establishes the SwiftUI project skeleton; real feature work starts a few weeks later.

2025 · Build

Chat API wired up

The first version of the request/response chat flow ships, non-streaming from day one.

2025 · Build

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.

2025 · Pause

Development pauses for several months

No commits land for roughly five and a half months before work resumes.

2025 · Refactor

Chat parsing generalized

Response parsing moves to the current multi-type classification system, alongside the marketplace CMS integration.

2025 · Decision

Authentication arrives

Login, signup, and session handling ship for the first time, eight and a half months into the project.

2025 · Mistake / Refactor

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.

Lessons learned

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.
Outcome

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.