A repeatable, checklist-driven methodology for turning self-hosted web apps into real SwiftUI clients (iPhone + iPad + Mac from one codebase), where each native rebuild doubled as a ground-truth audit that caught live web bugs.
The challenge
I had a mature fleet of self-hosted web apps, all running as browser SPAs behind a private mesh VPN, backed by a self-hosted Postgres backend or a Node API. None had a native mobile presence: every app was just a URL, and the phone experience was "the website in Safari."
I wanted first-class native Apple clients as second clients of the same backend: same data, same purpose, native front-end, built as real SwiftUI, deliberately not WebView wrappers. And I wanted it to scale: not one bespoke app, but a repeatable generation process where each subsequent client is a phase-driven port. The methodology itself was the deliverable.
Approach (the reusable playbook)
Everything is organized around one playbook, piloted by the first client and validated by the second. Its load-bearing patterns:
- A per-app phase ladder: scope → repo/project → shared data layer → read-only views → writes → multi-platform → device install → docs. Crucially, phase 0 pulls the authoritative schema from the live database, so the Swift models mirror the real data contract rather than the docs.
- One shared logic layer, per-platform UI: networking, models, auth, and validation split from views from day one. That separation is what makes multi-platform (and future watch support) feasible.
- Auth posture resolved once, up front, deterministically picking the client shape: anon-key clients with no login vs. an auth-gated client with a Keychain-persisted session manager and a login gate. The first auth-gated app became the reusable template the rest consumed.
- Adaptive layout by size class: a TabView on the phone, a NavigationSplitView on iPad/Mac, one universal target. Standing rule: a long-lived streaming session must be owned above the size-class switch and never cancelled on view-disappear, or an iPad rotation would tear down an in-flight stream.
- Build-config = environment at compile time: a debug build points at the development server, a release build at the production node, so a production build can't accidentally write to dev.
- Each client matches its own web app's design tokens in native idiom (its real fonts registered at launch), rather than a fleet-wide design system.
Results
- A full suite of native SwiftUI clients generated over several weeks from one playbook, which grew from a scoping doc into a mature, multi-variant methodology (anon, auth-gated, and backend-API architectures) with multiple standing rules.
- The hard apps carried the differentiated value. The analytics client was the toughest data-viz work: native Swift Charts, per-month and per-category breakdowns, a year-end "wrapped" recap, and the first split-view layout. It surfaced a UTC date-pipeline bug where a date-only column edited through a local-time picker rolled the calendar day by ±1; the fix pins the whole pipeline to UTC. The property/asset client was the first to use cloud storage and the device camera: signed URLs for private media, upload-once/replace-by-delete with orphan rollback, and correct camera-permission handling. The assistant client had no database SDK at all: it talks only to a Node API behind an AI gateway, consuming server-sent event streaming over URLSession's native byte-stream API.
- A native rewrite is a free audit of the web app. Because phase 0 dumps the live schema instead of trusting the docs, the iOS work repeatedly surfaced real production bugs: a web app silently dropping audit rows because a database CHECK constraint rejected the field name it wrote (error swallowed); a documented schema that turned out to be fabricated (corrected to the six real tables); dead code keying on an impossible condition; and several stale API-contract facts. In each case the iOS client deferred the buggy behavior and filed the web fix rather than mirroring it.
- Honestly tracked, not overstated. A couple of clients were device-verified and flipped to production on real data; another was production-enabled; the rest were built through the read/write phases on the dev build, pending on-device UAT and the production flip. Several are genuine parity ports. Device distribution was unblocked via the Apple Developer Program: the program and the release→production path are in place, not a shipped public beta.
What this demonstrates
- Verify against the source of truth, not the docs. Pulling the live schema in phase 0 caught a fabricated schema and several live bugs: a native rewrite is a free audit of the original.
- A methodology beats a one-off. A phase-driven playbook turned each new client from a bespoke project into a repeatable port, with reusable templates for auth, streaming, and multi-platform layout.
- Real engineering judgment on the hard edges: UTC date pipelines, streaming sessions that survive layout flips, compile-time environment safety, and camera/ storage idioms, not a pile of thin wrappers.
- Honest scope reporting. I distinguish device-verified/shipped from built-but- unverified, and parity ports from genuinely new capability.