Estimaid: engineering an offline-first iPhone app
Estimaid is a native iPhone app for cleaning contractors. It turns a walkthrough of a property into a priced, itemized document the client can keep, and it does that whether or not there is a usable signal in the building. KUBERSTAR designed it, wrote every line of it, and runs its releases.
- Role
- Product design, iOS engineering, release
- Platform
- iPhone, iOS 18 and later
- Stack
- Swift, SwiftUI, SwiftData, StoreKit
- Status
- Live on the App Store
Four constraints that decided the architecture.
None of these are preferences. Each one rules out an approach that would otherwise be the obvious way to build a small business app, and together they are why the app looks the way it does inside.
No desk, and no guarantee of signal
The document has to be produced where the work is - a basement, a stairwell, a half-renovated flat. Anything on the critical path that needs a server is a feature that fails exactly when it is needed.
Old numbers have to stay old
A price a client agreed to in March cannot move because the contractor edited their rate card in June. The historical record and the live configuration are different things and had to be stored as different things.
One repository, one team
The interface, the app code, the translations, the analytics and the release pipeline all live in the same repository, because there is no separate platform team to hand any of them to.
Not an English-only audience
The app ships in 34 translated locales besides English. Every layout has to survive a word three times longer than the one it was designed around, in a script it was not drawn for.
What we chose, and what it cost.
Six decisions worth writing down, either because the obvious alternative is worse in a way that is not obvious, or because we learned it the expensive way.
Local store first, cloud as a mirror
The app opens a SwiftData store on the device; CloudKit only mirrors it. If the mirrored container cannot be built it falls back to a purely local one, then to a destructive reset, then to memory - and the memory tier shows a permanent banner rather than pretending everything is fine.
Pricing as a pure function
The price breakdown is a nonisolated function over value types with no access to the store, the network or the main actor - so the test target calls it directly. Its result is frozen onto the job when it is saved, which is how a rate-card edit stops being able to rewrite a number a client already agreed to.
One schema, widened by inference
Every model change so far has been an additive property with a default, which the store resolves by inference when an older install opens it. Declaring a second versioned schema over the same live model types hashes to the same checksum and terminates the app on launch, before its first frame, for every upgrading user. A test now pins that shut.
The widget lives in another process
A widget extension cannot read the app's store, and relocating that store into a shared container would migrate every installation that already exists. The app instead derives a small equatable snapshot on the main actor and writes one JSON file into an App Group - and skips the write when the snapshot is unchanged, so a launch that changed nothing causes no file churn and no timeline reload.
Analytics that survive the swipe-away
The most valuable event to record is a completed purchase, which is also the moment a user is most likely to close the app. Events go into a bounded on-disk spool before the request is sent and are deleted only on a 2xx, then retried on the next foreground. With analytics switched off the spool is emptied rather than quietly kept.
A restore that refuses to guess
Restoring a backup deletes the store and re-inserts from the archive. The only thing that makes that safe is a decoder that throws on anything it cannot fully read, so tolerance for a missing key is granted one key at a time with a written reason - never as a blanket policy that would let a garbage payload decode into defaults and take the real data with it.
A layout budget measured in points, not characters.
SwiftUI hands a tab title to UIKit, so the usual shrink-to-fit modifiers never apply, and iOS lets an over-long label overlap its neighbour instead of truncating it. The app shrinks the tab bar's label font down to an 8pt floor; past that, the only remaining fix is a shorter word. So the budget is written down as a rendered width at that floor, measured with the platform's own text layout, and a test measures the five real titles out of the built app bundle in every locale.
A character count cannot tell these apart. Armenian “Ժամանակացույց” and German “Einstellungen” are both exactly 13 characters long, and render 88 and 67 points wide.
The parts that are not the app code.
34
translated locales, plus English
482
strings in the app's catalog
485
test cases across 41 test files
39
storefronts with localized release notes
Built end to end inside the studio.
Estimaid is our own product, which is why it is the one worth showing: the team that drew the screens also wrote the persistence layer, the pricing function, the widget, the localization gate and the release scripts. Nothing was handed over a boundary, so nothing here can be described as someone else's decision.
Product design
Interface, design system, and the adaptive layout rules that keep every screen usable at accessibility text sizes and in the longest translation.
iOS engineering
The whole app: SwiftUI, SwiftData persistence and its migration policy, StoreKit purchases, a WidgetKit extension, and App Intents.
Localization
34 translated locales in the app plus localized store metadata, behind a gate that fails on a missing translation or an over-budget label.
Release engineering
Scripted archive, upload, TestFlight distribution and store metadata, plus a privacy manifest checked against the events the app actually sends.
This is how we build.
If you are weighing up a native iOS project, the decisions above are the kind of thing you would be buying. Tell us what you are building and we will tell you where it gets hard.
Estimaid's own site, estimaid.app




