Ready Together is a Kotlin Multiplatform app being built around a narrow question: can people in one private circle coordinate an explicit check-in and practice one shared plan across Android and iPhone?
It is not an earthquake alert or early-warning system. The app is available on Google Play and the App Store; the SeedLink path is operating through reviewed-event verification and private check-in notifications. Detailed physical lifecycle evidence remains a separate record. Those boundaries shape the architecture as much as the interface.
We did not choose Kotlin Multiplatform to maximize a code-sharing percentage. We chose it because some product decisions should not acquire different meanings just because they are rendered on different operating systems.
A missing response is not proof that someone is safe. An unsupported contract version is not a successful check-in. A delayed command is not permission to replay a completed action. In a two-device preparedness flow, those are product rules—not Android rules or iOS rules.
One source of meaning
The project separates pure rules, versioned contracts, data translation, shared presentation, platform hosts, and the service authority:
:domainModels, invariants, policies, and state machines
No project dependencies:contractVersioned messages and compatibility outcomes
No project dependencies:dataMapping, caches, repositories, and transport
Depends on domain + contract:app:sharedShared Compose presentation and MVI state
Depends on domain + data:app:androidAppAndroid host and integrations
app/iosAppiOS host and integrations
:serverKtor authority using domain and contract boundaries
Depends on domain + contractThe dependency direction is deliberate. Domain rules do not need to know about Compose, Firebase, Ktor, persistence, or a mobile operating system. The data layer can translate a newer or incompatible wire value into an explicit outcome instead of guessing.
That gives Android and iPhone the same starting point for contract compatibility, stale data, retry behavior, and the meaning of states such as UNKNOWN, SAFE, and NEEDS_HELP.
The same idea appears in the drill protocol. Commands carry a version, semantic type, unique command ID, expected revision, session ID, and timestamp. The client does not own the canonical start: the server does. Each device may animate local elapsed time, but the shared session meaning comes from one authoritative protocol.
Where seismic detection enters the system
We operate a second input boundary alongside reviewed earthquake catalogs: an independent service we call seismic-detector. It receives near-real-time waveform streams through the open SeedLink protocol and looks for compatible observations. Its contribution stops at a preliminary technical candidate; it does not decide that an earthquake is confirmed and it never decides who should receive a notification.
SeedLink streamsOpen, near-real-time waveform transport
Data transport, not an answerseismic-detectorChannel triggers, station evidence, association, and candidate confidence
Preliminary candidate only · no push decisionEarthquake sourcesReported events with provenance, freshness, and revision state
Separate from detector outputSource reportPreliminary, reviewed, corrected, or withdrawn by its source
Never inferred from a detector candidateRelevant context → check-inA broad saved area makes a reviewed event relevant. OneSignal invites eligible members to open the authoritative check-in and respond.
No exact location by default · no emergency dispatchThe detector can associate compatible observations and distinguish channel count from physical-station count. Before a household check-in is opened, Ready Together must independently revalidate one correlated reviewed event and its relevance to the circle. Ambiguous, stale, or unavailable evidence fails closed. A candidate may then be corrected, discarded, or expire; it must not silently become a confirmed event.
The backend is the seam that protects the mobile product from upstream differences. Each country input can remain independently disabled, and every accepted event must receive a normalized identity and lifecycle before relevance or deduplication is evaluated. Android and iPhone consume that product state—not SeedLink, detector internals, or raw provider payloads.
The SeedLink path is operating within enabled country and area configurations. Historical waveform replay remains a separate diagnostic tool: it is not a live earthquake and it does not prove production coverage, guaranteed latency, magnitude, official confirmation, or warning before shaking.
Why not maintain those rules twice?
Android and iOS still need their own hosts and integrations. What they should not need is an independent interpretation of the same safety-sensitive rules.
Keeping product semantics, compatibility handling, and shared presentation state in Kotlin does not eliminate platform work. It narrows the place where platform differences can change product behavior.
That boundary matters when a recovery path is as important as a successful path. If an event snapshot is stale, if a map cannot start, or if a callback arrives after the user has moved to a different area, the user-facing meaning should remain consistent even when the platform rendering differs.
This is a design choice, not a measured claim about cost, performance, or a percentage of shared code. The repository does not claim those metrics.
Native where the platform is genuinely native
The bounded earthquake map is a practical example.
commonMain owns accepted event identity, selected event, area-derived bounds, freshness, retry behavior, stale-callback rejection, reduced-motion behavior, and the factual summary. Android renders the contract with Google Maps Compose. iOS renders it with MapKit through UIKitView.
The platform adapters translate shared models into native APIs and report semantic callbacks: ready, failed, selected event, and camera movement.
The map is deliberately not the source of earthquake facts. The List view remains initial and authoritative. If a native map is missing configuration, slow, offline, or unavailable, the factual list remains usable and the map enters an explicit unavailable state.
That is the intended division of responsibility:
- Shared: the product’s facts, transitions, compatibility rules, recovery behavior, and visible meaning.
- Native: map renderers, lifecycle, platform identity, configuration, entitlements, and release work.
The same seam appears in identity. Android obtains anonymous Firebase tokens through an Android adapter; iOS uses a Swift adapter. Both report through one shared result model that distinguishes available, not configured, unavailable, and rejected outcomes while keeping raw token and identity values out of diagnostic text and pure domain contracts.
A native failure was still a platform failure
Shared logic does not make native runtime evidence unnecessary.
On a configured Android emulator, opening the map reproduced a NoClassDefFoundError in the Google Maps legacy renderer path. Shared tests and an Android build had passed, but they did not exercise that configured provider runtime path.
The correction stayed local: Android declared the optional legacy runtime library. The shared event model, selection behavior, degraded states, and iOS adapter did not change.
The lesson was not that shared Kotlin removes platform risk. It was that a clear boundary can keep a platform-specific failure from redefining shared product behavior.
What the evidence proves—and does not prove
Merged evidence includes shared domain and contract tests, Android host validation, iOS simulator-compatible compilation and tests, Android debug assembly, deterministic screenshot validation, server tests, and an Android configured-emulator recheck for the map issue.
For the bounded map, recorded validation included Android host tests, iOS simulator tests and compilation, Android assembly, and deterministic screenshot checks. Those results support the named automated and configured-emulator surfaces only.
They do not prove every native gesture, accessibility behavior, provider attribution, production credential restriction, billing or quota behavior, household outcome, or lifecycle edge case.
Ready Together is now public on Android and iPhone, after functional testing on both platforms. The public listings prove store availability; they do not close the formal two-device ledger or every physical notification, background, offline, and reconciliation check. Separate backend infrastructure exposes the reviewed public earthquake feed plus a bounded authenticated QA surface for synthetic private-service checks; that evidence does not mean every private journey or data-handling path has been validated with real participants.
The boundary we want to keep testing
For Ready Together, Kotlin Multiplatform is useful when the same decision must mean the same thing on both phones. Native code remains necessary when the operating systems, SDKs, credentials, lifecycle, or release obligations genuinely differ.
The architecture helps make those boundaries reviewable. A store release does not turn unfinished cross-platform or private-service evidence into a proven preparedness outcome.