August 8, 2026 · Shipping

Native apps without the native pain

This article describes the product at publication. See AI Builder and Agent Teams for current capabilities.

Say you've been building a to-do list app in chat for a couple weeks. It's a website right now — React, a database, nothing fancy. You type "make me an Android version" and hit enter. Here's what actually happens between that keystroke and an .aab landing in your downloads folder, because most platforms won't show you this part, and the part they hide is where all the pain used to live.

The Android .aab

Gradle takes over first. Your app's native modules — camera access, local storage, whatever plugins the build pulled in — each declare which NDK version they were compiled against, and those declarations don't always agree. I've watched a module built against NDK r25 refuse to link against one that assumed r26, and the error it throws doesn't say "version mismatch," it says something about a missing symbol three layers deep in a .so file. Kotlin versions do something sneakier: a version pinned inside one Gradle module can silently shadow the one declared at the top of your build script, and the build succeeds — it just produces a binary that crashes on specific Android versions in the field. None of this is exotic. It's the standard tax of shipping native Android, and it's why teams hire a person whose entire job is knowing which flag makes this week's error disappear.

The build here runs the real toolchain and eats that resolution work itself:

15–25 minnative compile phase, real Gradle toolchain
  • Dependency conflicts caught before they become a runtime crash
  • Toolchain configuration that gets better every time a new failure teaches it something

A faster fake version of this — something that shapes an .aab without running actual Gradle tasks — would ship in under a minute. It would also die the moment your app needs a background service or a native crypto library, and Play Store review would flag it within a day. We'd rather spend the twenty minutes.

The macOS .dmg

Two builds go into this one file. Xcode's command-line tools compile an Apple Silicon binary and an Intel binary separately, then lipo glues them into a single universal executable.

90%+of new Mac sales are Apple Silicon

That's tempting — ship the one binary and call it done — until you remember a lot of people run their employer's laptop, not their own choice of hardware, and that laptop might be three years old and Intel. Rather than make a user figure out which chip they have (most can't tell you), we ship both and let the OS pick silently. The alternative, which we tried early on, is cross-compiling everything from a Linux box using emulated toolchains. It's faster. It's also how you end up with a codesigning edge case that only appears on real macOS 12 hardware, six weeks after ship, reported by a confused user who has no idea why their app won't open.

The Windows installer

This is where the first-run experience decides whether your user trusts the app at all. Windows SmartScreen doesn't know your installer yet — it hasn't built up reputation with Microsoft's servers — so it shows a blue "Windows protected your PC" screen with a button that says "Don't run" in bold and a barely-visible "More info" link that, once clicked, reveals "Run anyway." macOS does its own version of this dance: right-click, Open, confirm, because apps outside the App Store aren't trusted by default either. Early on we linked both of these to a generic FAQ page. Support tickets told us that doesn't work — someone staring at a screen that says their download might be malware doesn't go read documentation, they screenshot it and ask if they've been hacked. So the install flow detects the OS and shows the exact three clicks needed, no FAQ required. Small detail, but the file itself matters too: the download is named after your product, not after a build artifact. Nobody should have to explain over chat support that they downloaded "app-release-signed-v2-final.exe" and can't tell if it's the right one.

The browser extension manifest

This is the odd one out in the whole pipeline — no Gradle, no NDK, no compile step in the usual sense. What it has instead is a manifest, and the manifest is a negotiation with a Chrome Web Store reviewer you'll never talk to directly.

Permission requestedReview outcome
<all_urls> (broader than the feature needs)Two-week back-and-forth with someone who won't say exactly what they objected to
activeTab (scoped to the actual need)Clears same-day

MV3 also complicates something MV2 made easy: background service workers get unloaded mid-task by design, a Google policy decision aimed at battery life, and a feature that needs to survive that has to be built around it rather than against it. We default every extension to the narrowest permission set its actual functionality needs and widen only when a specific feature demands it.

The keystore

Underneath the Android build sits the one artifact you never see and can't afford to lose: the signing key. Lose it and you don't just lose the ability to update your app — you lose the ability to update it under its existing identity, permanently, with no recovery path Google will grant you, ever. It's not glamorous infrastructure. It's a file. But it's the difference between shipping version six months from now as a seamless update and shipping it as a brand-new listing that starts at zero installs and zero reviews. We generate one per project and hold onto it so every future build signs with the same key as day one.

The chat thread underneath all of it

None of the above lives in a separate "mobile project." It's the same conversation that built the web app. Ask for a UI change, the web build updates; ask for an Android bundle next, and it compiles from that same current state, not a fork that drifted out of sync three weeks ago. Most teams I've watched try to bolt on native later end up maintaining two codebases that grow apart — a web app that ships every day and a native wrapper somebody has to remember to catch up before each release. That gap is where staleness lives, and it's exactly what a single build history removes. It cuts both ways, though: if the chat's been fast and loose lately, the Android build inherits that too. It's not a separate polish pass, it's a direct compile of whatever's actually there — which in practice keeps people honest, because there's no "we'll clean it up before submission" side quest to skip.

When it's ready for a storefront, the ship path hands off to your own Play Store listing and your own Apple Developer account. Not ours. We didn't want to sit between you and your own distribution.

Coming next: iOS and macOS App Store builds. The packaging machinery is basically the same shape as what's above — Apple's signing and review pipeline is its own project, and we'd rather ship it working than ship it soon.
Shipping
ShareXLinkedInFacebookRedditQuoraWhatsAppTelegramEmail
← All posts