ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-05-02Advanced

The Order I Had to Fix Before Tauri 2 Apps Were Safe to Sell — Signing, Notarization, Updates, Licensing

The mistakes I shipped before getting paid Tauri 2 apps right — signing in the wrong order, a DMG that carried an unsigned app through notarization, a rollout gate that leaked at 0 percent, and license verification that trusted the clock.

antigravity449tauridesktop2code-signingnotarization2monetization31license

Premium Article

The Order I Had to Fix Before Tauri 2 Apps Were Safe to Sell

Most developers who finish a Tauri 2 app discover the painful truth on release day: the hard part wasn't writing the code. It was everything that comes after tauri build returns successfully. Notarization rejections, SmartScreen warnings, broken updaters, license keys that don't survive offline use, Stripe webhooks firing twice — these are the topics that the official quickstart politely sidesteps, and they're exactly where every paid desktop app gets stuck.

I've shipped iPhone and Android apps as an indie developer for years, but desktop is a different animal. The store no longer handles distribution for you. You build the pipeline yourself, or you don't ship. This article walks through the full production deployment pattern I use for paid Tauri 2 apps, with Antigravity's agents doing the heavy lifting on the boilerplate so I can focus on the design decisions.

Why "Production Tauri 2" is Genuinely Hard

Tauri's edge over Electron isn't just smaller binaries and faster startup. It's distribution freedom. Tauri 2 generates native installers directly and lets you bring your own update infrastructure. That freedom is powerful — and it means everything is your responsibility.

In the Electron world, electron-updater and electron-builder automate most of this. Tauri 2 ships with tauri-plugin-updater, but key management, distribution endpoints, and staged rollout strategy are yours to build. The reason Antigravity matters here is that you can split this work across agents naturally: the Manager Surface lets one agent write the Rust signature-verification code while another writes the TypeScript license UI, and a third writes your Cloudflare Worker. That separation of concerns is the design philosophy that runs through this entire article.

A second reality: the failure modes of desktop distribution are completely different from web. A bad deploy on the web means rolling back in seconds; a bad release of a desktop app sits on customer machines until they relaunch. That changes how you design every layer. Your update server has to be conservative by default. Your license format has to survive across versions. Your installer has to be re-runnable without breaking existing data. None of these are obvious until your first hot-fix that has to ship without breaking anyone, and you discover the file you needed to update is owned by an installer that overwrites it.

The third thing that catches people: Tauri 2's smaller surface area cuts both ways. Yes, the binary is small and starts fast. But the ecosystem is younger than Electron's, which means many problems you'll hit don't have a Stack Overflow answer. This is exactly where Antigravity earns its keep — its agents can read the Tauri 2 source directly, propose fixes that match upstream's intent, and explain why a particular API exists rather than guessing at usage from forum posts. I've spent more than one evening watching the Manager Surface track down an issue across tauri-plugin-updater's Rust code, the Tauri JS bindings, and the underlying WebView crate — work that would have taken me a full day alone.

If you want to compare against the Electron side of this problem, Building AI-Powered Desktop Applications with Electron and Antigravity covers how packaging and updates differ there. This article is the other path: owning every piece of distribution yourself.

Automating macOS Notarization with Antigravity

Code signing alone isn't enough on macOS. A signed .dmg or .app will still display "cannot verify the developer" until you submit it to Apple for notarization, then staple the result to the binary. Skip notarization and your users see a scary warning — and Gatekeeper will block the app entirely on certain configurations.

The first version of my script produced a confusing outcome: notarization passed, and the shipped app still warned users. Three things were wrong, and none of them were about syntax. All three were about order.

First, I leaned on codesign --deep. Apple discourages it for distribution, and it will happily walk past sidecars and frameworks without applying the entitlements you intended. Second, I omitted --timestamp, which notarization rejects as a signature without a secure timestamp. Third, and worst: the DMG that tauri build produced already contained the unsigned .app, and that was the DMG I was notarizing and stapling.

The outside was notarized. The inside was not. Here is the version with the order rebuilt:

#!/usr/bin/env bash
# notarize.sh - Tauri 2 macOS pipeline (sign -> rebuild DMG -> notarize -> staple)
# Required env: APPLE_ID, APPLE_TEAM_ID, APPLE_APP_PASSWORD, SIGNING_IDENTITY
# Assumes tauri build ran with --bundles app; this script owns the DMG
set -euo pipefail
 
APP_NAME="MyApp"
APP_PATH="src-tauri/target/universal-apple-darwin/release/bundle/macos/${APP_NAME}.app"
DMG_PATH="dist/${APP_NAME}_universal.dmg"
mkdir -p dist
 
# 1. Sign inside-out. Do not use --deep for distribution builds:
#    nested code does not inherit your entitlements, and the failure is hard to trace later.
while IFS= read -r -d '' NESTED; do
  codesign --force --options runtime --timestamp \
    --sign "$SIGNING_IDENTITY" "$NESTED"
done < <(find "$APP_PATH/Contents/MacOS" "$APP_PATH/Contents/Frameworks" \
           -type f -perm -u+x -print0 2>/dev/null)
 
# 2. Sign the .app itself last (--timestamp is required or notarization fails
#    with "the signature does not include a secure timestamp")
codesign --force --options runtime --timestamp \
  --sign "$SIGNING_IDENTITY" \
  --entitlements src-tauri/entitlements.plist \
  "$APP_PATH"
codesign --verify --strict --verbose=2 "$APP_PATH"
 
# 3. Rebuild the DMG from the signed .app. Skip this and you ship an unsigned app inside.
STAGE="$(mktemp -d)"
cp -R "$APP_PATH" "$STAGE/"
ln -s /Applications "$STAGE/Applications"
rm -f "$DMG_PATH"
hdiutil create -volname "$APP_NAME" -srcfolder "$STAGE" -ov -format UDZO "$DMG_PATH"
codesign --force --timestamp --sign "$SIGNING_IDENTITY" "$DMG_PATH"
 
# 4. Notarize the DMG. Read the status field; do not trust the exit code alone.
xcrun notarytool submit "$DMG_PATH" \
  --apple-id "$APPLE_ID" \
  --team-id "$APPLE_TEAM_ID" \
  --password "$APPLE_APP_PASSWORD" \
  --wait \
  --output-format json > notarize_result.json
 
STATUS=$(jq -r '.status' notarize_result.json)
SUBMISSION_ID=$(jq -r '.id' notarize_result.json)
if [ "$STATUS" != "Accepted" ]; then
  echo "Notarization did not pass: status=${STATUS} id=${SUBMISSION_ID}"
  xcrun notarytool log "$SUBMISSION_ID" \
    --apple-id "$APPLE_ID" \
    --team-id "$APPLE_TEAM_ID" \
    --password "$APPLE_APP_PASSWORD"
  exit 1
fi
 
# 5. Staple and verify (a DMG is assessed against its primary signature)
xcrun stapler staple "$DMG_PATH"
spctl --assess --type open --context context:primary-signature --verbose=4 "$DMG_PATH"
echo "Notarized: $DMG_PATH"

The difference between the old and new script is not the number of commands. It is where verification can catch you. The old one assessed the .app, so it could never notice an unsigned binary sitting inside the DMG. The new one assesses the DMG itself with --context context:primary-signature, which is the artifact your users actually download.

Trusting the exit code of notarytool submit --wait was the other quiet risk. I had a run come back Invalid while CI stayed green, and that build went out. Reading the status field explicitly is four lines of shell and removes the entire class of mistake.

Expected output is Accepted from notarytool, then spctl reporting source=Notarized Developer ID after stapling. If you still see Unnotarized Developer ID, either stapling failed or the artifact you notarized is not the artifact you are shipping.

I hand the log reading to an Antigravity Sub-Agent: "take notarytool log JSON and bucket issues[].message into missing entitlements, missing timestamp, or unsigned nested binary — then print only the offending paths." Notarization errors are worded abstractly, so one layer of classification meaningfully shortens recovery time.

A subtle thing about notarytool's behavior worth knowing: the --wait flag will sit and poll for up to about 15 minutes per submission. If your CI runner has a tight timeout, you may need to either increase the runner timeout or split into a non-blocking submit followed by a separate poll job. I went with the simpler --wait approach because every minute of CI complexity I added paid for itself only in marginal speed improvements, while the simpler version stayed reliable across hundreds of releases. Pick the option that minimizes the moving parts in your pipeline.

There's also a question of what to do when notarization is slow but ultimately succeeds. Apple's median turnaround is under three minutes, but I've seen 12-minute submissions during product launches and right after macOS dot releases ship. If you're publishing on a schedule (e.g. weekly Friday releases), staggering your notarization submissions away from common rush hours helps. The asymmetry: notarization has no SLA, so when Apple is slow, you wait. Build that variance into your release schedule rather than fighting it.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
You will avoid the ordering trap where the DMG produced by tauri build carries an unsigned app straight through notarization, using a script that rebuilds it at the right moment
You will close two real rollout bugs — a gate that ships to some machines at 0 percent, and a bucket that reshuffles on every launch — with a corrected Worker you can paste in
You will get a single Worker that handles Stripe signature verification, idempotency, and Ed25519 license issuance, ready to port into your own product
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

App Dev2026-06-30
The App Open Ad Antigravity Wrote for Me Fires Every Time I Return From My Own Paywall or Rewarded Video
Ask Antigravity to add an App Open ad and it shows one the instant you return from your own rewarded video or the Google Play purchase sheet — which also brushes against AdMob policy. Here is a foreground arbiter that records why the app came back, with working Kotlin and a verification matrix.
App Dev2026-06-25
An Agent Granted 'Watch an Ad to Unlock a Wallpaper' Entirely Client-Side — Re-Verifying Reward Grants with AdMob SSV
I asked an Antigravity agent to wire up 'watch a rewarded ad to unlock a wallpaper,' and it returned an implementation that wrote the unlock flag client-side only. Here is why that is not enough, how I re-verified the reward grant with AdMob server-side verification (SSV), and how I stopped double grants too.
App Dev2026-06-19
I Started the Ad SDK Before Asking for ATT — the Init-Order Bug That Quietly Lowered First-Session eCPM
When I rolled AdMob mediation out to four iOS apps, only the very first session showed weaker ad revenue. The cause was the order between the ATT prompt and MobileAds initialization. Here is why the order matters, plus how I had Antigravity audit the init sequence across all four apps.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →