Last Week
Last week was about getting iOS out of the “it builds on my machine” phase. I moved the CI/CD pipeline off expensive hosted macOS runners, set up a self-hosted runner that can reliably archive the Compose Multiplatform iOS app, and worked through App Store Connect’s metadata and policy checklist so TestFlight wouldn’t be blocked on paperwork. The big open question was whether Apple would force Sign in with Apple even for an early testing build. I decided to submit what I had, see what review actually does, and only pay the platform-specific authentication tax if it became mandatory.
iOS Alpha Is Live, But Login Friction Is Real
The good news: the iOS alpha was approved and is now available for testing, and I’ve been able to push a few follow-up builds without getting bounced for missing “Sign in with Apple.” That’s a huge relief because I’m using Kotlin Multiplatform (KMP) specifically to keep as much logic as possible in shared code, and third-party login providers are one of the fastest ways to end up with two separate auth implementations to maintain.
The less fun news showed up immediately in testing: my current login flow is an email one-time passcode (OTP). It’s simple, it avoids passwords, and it keeps the app’s authentication model platform-agnostic. But because Shokken is sending those OTP emails from a brand new domain, at least one tester never saw the code at all. The email went to spam, the tester didn’t notice the “check your spam folder” warning, and the result was a hard stop at the very first screen.
It’s tempting to write that off as user error, but that’s not how onboarding works. If a user can make a mistake, some percentage of users will make that mistake, and they’ll blame the product (fairly). Domain reputation will improve over time as real users mark messages as “not spam,” but “wait for the internet to trust my domain” is not a strategy for an alpha where every tester counts.
So I’m changing direction: I’m going to add federated login (Google on Android, Apple on iOS) even though it’s platform-specific. It’s more work, but it removes the biggest failure mode in the current OTP setup and should reduce sign-up friction at the exact moment I need testers to get into the product quickly.
What does it mean in English?
The iPhone version of Shokken is finally approved and installable, which means testing can happen on both platforms. But the very first thing a tester does—logging in—broke for at least one person because the sign-in email landed in spam. That’s not a “they should’ve read the instructions” problem; it’s a product problem. Next week I’m planning to make login more reliable and to polish the build-and-release automation so uploading new versions is less manual work.
Nerdy Details
Why email OTP fell over in an alpha
Email OTP works well when you control deliverability. In practice, deliverability is a moving target because inbox providers rank senders using signals like domain reputation. A brand new domain effectively starts at “untrusted,” and until it has a history of sending non-spam email, providers are more likely to route it to junk. That means a login system that is “correct” from an engineering standpoint can still fail at the user level: the code exists, but the user never receives it in a place they’ll see.
The key lesson is that authentication has two parts:
- The backend security model (can an attacker abuse it?)
- The delivery path to the user (can a legitimate user actually complete it?)
OTP via email can be secure and still be unusable if deliverability is inconsistent. In an alpha, you don’t have the volume to “warm up” a domain quickly, and you definitely don’t want your first-time user experience to depend on a tester remembering to dig through spam.
Design goal: make the “wrong path” hard to take
I already put “check your spam folder” on the login screen. The tester still missed it. That’s predictable: warnings are cheap to add and easy to ignore, especially when people see similar warnings everywhere.
So instead of leaning harder on copy, I’m treating this as a design constraint: the login flow shouldn’t have a common failure mode that looks indistinguishable from “the app is broken.” If the OTP doesn’t arrive, the user should have an obvious recovery path that does not involve support tickets and does not rely on email deliverability stabilizing over time.
Evaluating alternatives: SMS vs. federated sign-in
I considered switching OTP delivery to SMS, but SMS login carries its own operational baggage: phone number changes, SIM-swap risk, and a long tail of support issues when messages don’t arrive. For a business-facing tool, phone numbers also tend to be less stable than email addresses, and tying accounts to phone numbers can create awkward ownership transfer problems later.
Federated sign-in (Google on Android, Apple on iOS) solves a different class of problem: it uses identity providers that users already trust and that are very unlikely to be filtered into spam. It also reduces friction because users don’t have to context-switch to their inbox, copy a code, and come back.
The downside is exactly what I’ve been trying to avoid with KMP: platform-specific surface area. The shared module can still own the session model and the backend token exchange, but each platform needs native code to retrieve credentials from its provider and hand them off to the shared layer. That’s a maintenance cost, but at this stage it’s worth it to eliminate onboarding failures.
The next round of CI/CD polish: fewer scripts, less friction
With Android and iOS builds uploading successfully, the work shifts from “make it possible” to “make it pleasant.” Right now I have too much duplication across build and upload scripts, and that duplication makes every tweak risky because the same change needs to be applied in multiple places.
One specific paper cut is the iOS review workflow. On Google Play, uploading to an alpha track generally queues the build for review automatically. On iOS, even after the archive lands in App Store Connect, I still have to log in and click through manual certification steps to push it into the review queue. That breaks the “push a tag, walk away” dream. Next week I want to consolidate the scripts and automate more of the iOS submission flow so the pipeline feels closer to Android’s.
Versioning: iOS rules are stricter than Android
Another thing I tripped over is that iOS version strings are constrained: the user-visible version must be numeric components separated by dots (for example 1.1.1). Android is more permissive, and because Android was my first target I ended up with versions that include letters. The fix is simple but important: unify both platforms on a single numeric versioning scheme so releases don’t become a “remember the Apple rules” chore.
Alongside the version name, build numbers need a policy too. I’m leaning toward one global, strictly increasing build number for every build that produces a distinct artifact. That lines up with both ecosystems: iOS requires uniqueness, and the Play Store requires the version code to monotonically increase. A single counter also makes support easier because “build 168” is unambiguous across devices.
Release notes need to be written for humans
Right now release notes are generated from commit messages, and they tend to be too verbose and too abstract. Users don’t want a poetic summary of refactors; they want to know what changed and whether it affects them. Tightening the release-note generator so it produces short, concrete, user-facing bullets is part of the same theme as the rest of this week: remove avoidable friction from the path between “I made progress” and “a tester can benefit from it.”
Next Week
Next week is cleanup and reliability work. I’m going to consolidate the CI/CD scripts, reduce duplication, and automate more of the iOS submission steps so shipping builds takes fewer manual clicks. In parallel I’ll start implementing Google and Apple sign-in to replace (or at least de-emphasize) email OTP, and I’ll unify version names and build numbers across platforms so releases stay predictable as the testing cadence increases.