ios-app-client @sushrutalgs.ai

See the full portfolio →

Built with

  • Swift 6
  • SwiftUI
  • supabase-swift
  • Google Sign-In
  • Sign in with Apple
  • Xcode Cloud

Links

  • live product↗
  • Request repo access→

Source is private; sushrutalgs.ai is a live product. Happy to walk through the code or grant read access on request.

The native iOS app for sushrutalgs.ai, an AI study companion that answers surgical-exam questions with streaming responses backed by citations, figures and tables from standard textbooks, three sign-in options, and conversation history that stays in sync across a user's devices.

The problem

sushrutalgs.ai already worked in a mobile browser, and that was the problem. Residents study in the gaps between cases, on a phone, and an answer arrives as a stream that takes several seconds to fill in with its citations, figures and tables, so a backgrounded tab or a dropped connection costs the whole answer rather than delaying it. The same people move between a phone and an iPad mid-session and expect the thread to be where they left it. A native client was the way to hold a session in the Keychain, survive app state changes while a stream is open, and hand a live answer to the user's other device.

Why it is hard

The hard part is not the chat UI: it is that the same conversation has to be true in two clients. Retry and edit fork a thread into a tree instead of overwriting it, and the web client already owned that model, so an iOS version that grew its own idea of a fork would drift on exactly the awkward cases (editing a message halfway up a branch, resuming a partly streamed reply) and the drift would only surface later, in stored conversations neither client could render. The stream is the second constraint: frames arrive off the network as thinking, metadata, text, error and done, every one of them mutates view state, and under Swift 6 strict concurrency that has to be actor-correct before it compiles at all. And because the gateway debits a daily quota per request, anything that quietly re-sends a query (a token refresh mid-answer, a retry after a timeout) spends the user's allowance a second time.

The design

  • Port the tree, do not redesign it. The conversation model is a one-to-one port of the web client's tree, down to how a fork is created and identified, so a thread started on one client opens correctly on the other. A Swift-idiomatic rewrite was the tempting option and was rejected: it would have been better Swift and a second definition of what a conversation is.
  • No ViewModels. Observable services are injected once at the root and views read them directly, which keeps one source of truth for the tree and the open stream. The conventional MVVM layer would have handed every screen its own copy of state the model already holds, which is the same divergence problem one layer down.
  • A leaf client. All inference goes through the Cloudflare Worker gateway, so the app ships no backend host and no key, and a CI check fails any build that would embed one. The cost is that nothing can be answered offline: cached history and Keychain state make a cold start useful, but the answer path always needs the network.
  • Handoff by broadcast, not polling. A stream in progress is published on a Supabase Realtime channel so the second device picks it up live, instead of persisting the finished answer and letting that device discover it whenever it next opens.
  • Three ways in. Email one-time codes, Google and Apple, each exchanged for a Supabase session with nonce-based replay protection. Offering Google on iOS obliges Sign in with Apple too, so the third path was a requirement rather than a choice.
System architecture. Tap to enlarge.

Where it stands

It ships on the App Store at 20.8 MB installed, with one SwiftUI shell covering both iPhone and iPad. Answers stream with the same citations, figures and tables as the web client, and a conversation started on either one opens correctly on the other, which is the whole point of porting the tree rather than rewriting it. The honest limits: it targets iOS 26, so the observation model that removes the ViewModel layer also gives up older devices; the iPad runs the phone's shell rather than a layout of its own; and offline gets you your history, not an answer.

Streaming chat with a cited answer.

Streaming chat with a cited answer.

In numbers

7
Observable services
1:1
chat tree ported from web
3
sign-in methods
iOS 26
Model-View, no ViewModels

Porting the web client's conversation model instead of rewriting it in idiomatic Swift is the kind of decision that returns nothing visible in a screenshot, and would have caused the most damage if skipped — a second definition of what a conversation is, discovered months later in a user's stored history.