App Saturday: BetterKey - building reliable car controls on top of unreliable OEM APIs
What I built
BetterKey is a small iOS/watchOS system that sits on top of OEM vehicle APIs and tries to make remote lock/unlock predictable. The user-facing surfaces are the iPhone app, Apple Watch, Siri Shortcuts, and a few Lock Screen flows. Vehicle commands go through a protocol-based abstraction, with Smartcar as the primary provider.
It is not trying to replace a physical key. It is a lower-friction path for common cases: walking up to the car, or using the watch when the phone is not convenient.
Why I built it
I kept running into small unlock friction: phone in pocket, hands full, OEM app slow to launch, Bluetooth key behavior inconsistent, or the car's own app needing several taps before issuing a command. The real problem was that the system never felt deterministic.
For a lock/unlock app, trust is the product. If the UI says "locked" based on optimism instead of confirmed state, it only has to be wrong once before the user stops believing it.
Architecture / Stack
The iOS and watchOS apps are SwiftUI on Swift 6, using async/await, actors, and u/MainActor on the UI layer. Most client code is shared through Swift packages with platform adapters for iOS, watchOS, and UI tests.
The backend is a Go API on Fly.io. It handles auth, Smartcar/OEM token management, command orchestration, token refresh jobs, and push fanout. Firestore stores state and command metadata; FCM handles delivery.
Deep problem: OEM APIs are not real-time systems
Lock/unlock APIs look simple, but they behave like eventually consistent remote calls into a car that may be asleep, offline, busy, or behind a flaky backend.
A command can be accepted before the car has moved a latch. Status can be stale, providers return 502s, and immediate polling can make success look like failure. Rate limits mean "just poll harder" is not a real solution.
I initially treated command success too much like state success. That broke down quickly, especially around Lock on Depart, where an optimistic "locked" state is worse than no automation at all. Smartcar surfaces this directly: a successful response can carry `X-Command-Confirmation: unconfirmed`, meaning the request was accepted but not confirmed by the vehicle. BetterKey maps commands into `VehicleCommandResult.confirmed` or `.unconfirmed`, and the UI separates "sent but unconfirmed" from "locked."
Deep problem: Park, depart, and Lock on Depart
The naive version is simple: detect departure and issue a lock command. The real version has to account for location accuracy, background limits, duplicate events, stale state, and cases where the user intentionally leaves the car unlocked.
Lock on Depart sits on top of Park Location, which confirms that the car is parked. The coordinator is a phase-driven actor that watches geofence and motion signals, applies cooldown windows, and exits if vehicle state is too stale.
The tradeoff was responsiveness versus unnecessary commands. I biased toward fewer commands and clearer state transitions.
Lock Screen surfaces
The Lock Screen piece is Live Activities and App Intents, not a general purpose lock/unlock widget. That constraint shaped the design: Lock on Depart can expose monitoring/locking state, while Prompt to Unlock can present one narrow action when motion looks like returning to the vehicle. App Intents keep that action on the same command pipeline as the in-app button.
App Review and demo mode
Demo mode mattered more than I expected. App Review, testers, and every developer cannot be expected to have a compatible real vehicle connected through Smartcar.
At the network edge, an `InterceptorNetworkClient` swaps in a noop implementation. At the service layer, a `SimulatorVehicleService` actor provides seeded vehicles, deterministic transitions, and toggleable states like "needs reauth" or "window open."
The tradeoff is that demo mode has to stay honest enough for review and testing. If it drifts too far from production behavior, it stops being useful.
What I learned
- HTTP success can still be UX failure if the state is not confirmed.
- Background constraints shape product design more than UI decisions.
- Lock state UX needs careful language. Pending, confirmed, and unconfirmed are different things.
- Hardware-adjacent apps need a credible demo/review path from day one.
- Automations on physical hardware need a foundation feature you trust.
Happy to answer questions about the architecture.