ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-07-03Advanced

When Universal Links Break Silently — Catching Association Drift with an Agent-Run Verification Gate

Universal Links and App Links fall back to the browser with no error when your association files or entitlements drift apart. Here is a design that generates the files from one source of truth and hands weekly and pre-release checks to an agent.

Universal LinksApp LinksAntigravity306iOS26Android25Deep Linking

Premium Article

For a while, one of the wallpaper apps I run on my own had a broken share link that never once opened the app. Tapping it just quietly launched Safari. No crash, no error log. I only found out through a user's message, and I burned half a day tracing it.

Universal Links on iOS and App Links on Android look like ordinary URLs when they work. Behind them, though, sit three independent declarations that must match exactly: the app's entitlements, the association file the OS fetches to verify ownership, and the app's own intent filters. The moment any one of them drifts, the link doesn't throw an exception — it falls back to "open in the browser," which looks perfectly normal. That is what makes this dangerous. When it breaks, nobody notices.

This article lays out that silent failure as a design problem: stop maintaining the declarations twice by hand, and hand the reconciliation — weekly and before every release — to an Antigravity agent.

Why it breaks silently — three declarations, three owners

Before a link can open, these three things are each declared in a different place. They drift because they live in different repositories, are owned by different people, and change at different times.

DeclarationLocationTypical way it drifts
Association file (AASA / assetlinks.json)Web server /.well-known/Domain move, CDN caching, wrong Content-Type
App declaration (Associated Domains / intent-filter)Entitlements, AndroidManifestMissing domain, missing autoVerify, path change
Signing info (Android fingerprint)SHA-256 inside assetlinks.jsonUpload key swap, Play App Signing certificate change

This gets worse when you let an agent reshape UI and routing. A new screen brings a new path in the app, but the published file update gets left behind. The generated code runs correctly, yet the link quietly detaches. Left alone, that asymmetry accumulates.

Make the route definition a single source of truth

The fix starts by refusing to hand-write link paths in three places. Collapse them into one definition file and generate the association files from it. At minimum, the app and the published files can no longer disagree by construction.

// link-routes.json — the one source of truth for link design
{
  "domain": "example.com",
  "ios": {
    "appID": "TEAMID123.com.example.wallpaper"
  },
  "android": {
    "package": "com.example.wallpaper",
    "sha256": ["AA:BB:CC:...:99"]
  },
  "routes": [
    { "id": "collection", "path": "/c/*", "screen": "CollectionScreen" },
    { "id": "wallpaper",   "path": "/w/*", "screen": "WallpaperScreen" },
    { "id": "invite",      "path": "/invite/*", "screen": "InviteScreen" }
  ]
}

From this one file, write out the iOS AASA and the Android assetlinks.json in code. The point is to never let anyone hand-edit the JSON.

// gen-association-files.mjs — zero dependencies; generate published files from routes
import { readFileSync, mkdirSync, writeFileSync } from "node:fs";
 
const cfg = JSON.parse(readFileSync("link-routes.json", "utf8"));
 
// iOS: apple-app-site-association
const aasa = {
  applinks: {
    apps: [],
    details: [
      {
        appID: cfg.ios.appID,
        paths: cfg.routes.map((r) => r.path),
      },
    ],
  },
};
 
// Android: Digital Asset Links
const assetlinks = [
  {
    relation: ["delegate_permission/common.handle_all_urls"],
    target: {
      namespace: "android_app",
      package_name: cfg.android.package,
      sha256_cert_fingerprints: cfg.android.sha256,
    },
  },
];
 
mkdirSync("public/.well-known", { recursive: true });
// no extension, served as application/json
writeFileSync("public/.well-known/apple-app-site-association", JSON.stringify(aasa, null, 2));
writeFileSync("public/.well-known/assetlinks.json", JSON.stringify(assetlinks, null, 2));
 
console.log(`generated ${cfg.routes.length} routes for ${cfg.domain}`);

Keep the app-side declarations verifiable against the same definition. The iOS entitlement and the Android intent filter look like this:

<!-- AndroidManifest.xml — always include autoVerify -->
<intent-filter android:autoVerify="true">
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="https" android:host="example.com" />
  <data android:pathPrefix="/c/" />
  <data android:pathPrefix="/w/" />
  <data android:pathPrefix="/invite/" />
</intent-filter>

On iOS you declare applinks:example.com under Associated Domains. If a single character of the domain or a missing subdomain is off here, the OS never fetches the association file, and the link falls silently back to the browser.

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
How to stop hand-editing AASA and assetlinks.json and generate both from one route definition
A dependency-free verifier that reconciles entitlements, intent filters, and the published files
A two-stage gate for pre-release and weekly unattended runs, with six weeks of measured results
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 $10 for lifetime access
View Membership →

Related Articles

App Dev2026-05-25
One Month Splitting Antigravity's Inline Edit and Agent Mode Across Four Wallpaper Apps
A month of notes from running Antigravity's Inline Edit and Agent Mode across four production wallpaper apps — with real counts, the decision rule I wrote into AGENTS.md, a one-pass dSYM fix, and how credit cost factors in.
App Dev2026-04-18
Building and Releasing an Art Portfolio App for iOS and Android with Antigravity — The Full Story
How I used Antigravity to build a portfolio app for my own artwork and release it simultaneously on iOS and Android — from concept through App Store and Google Play approval.
App Dev2026-04-02
Building AR Apps with Antigravity: A Beginner's Guide to ARKit & ARCore 2026
Learn how to build augmented reality apps with Antigravity's AI agents, ARKit (iOS), and ARCore (Android). This step-by-step guide covers the 2026 AR market landscape, environment setup, and hands-on code examples.
📚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 →