Skip to content

CLI commands

rndl validate

Validate a domain's apple-app-site-association and assetlinks.json against the checks Apple and Google enforce, cross-check them against your route table, and gate CI on the result.

rndl validate fetches a domain's universal-link (AASA) and Android App Links files and validates them the way the platforms do, not the way the docs suggest. Errors exit 1, so the same command stops a broken universal link from shipping when you run it in CI.

rndl validate --domain example.com                 # AASA + assetlinks.json
rndl validate --domain example.com --json          # full result as JSON
rndl validate --domain example.com --sarif         # SARIF 2.1.0 for CI upload
rndl validate --domain example.com --package com.example.app --sha256 AA:BB:...

What is checked

For https://<domain>/.well-known/apple-app-site-association:

  • Reachable over plain HTTPS with a 200, and no redirects (Apple fetches with redirects disabled, so a 301 means Apple sees no file at all).
  • Under Apple's 128KB size cap.
  • Valid JSON schema: applinks.details[] with appIDs (or the legacy appID) in <TeamID>.<BundleID> form, and components[] entries using /, ?, #, exclude, and comment.
  • Content-Type is a warning, not an error (Apple ignores it, but text/html usually means a fallback page is being served).

For https://<domain>/.well-known/assetlinks.json:

  • The relation is exactly delegate_permission/common.handle_all_urls on an android_app target.
  • package_name matches your app (from --package or your app.json).
  • sha256_cert_fingerprints are well-formed; --sha256 additionally asserts a specific fingerprint is present.

The route cross-check

Run validate from your app's directory (or pass --app-dir/--config) and it compares the AASA components against your actual route table:

  • A route that no non-excluded component covers is an error: AASA_MISSING_ROUTE. That link will open the browser, not your app.
  • A component that matches no route is a warning: AASA_ORPHAN_PATTERN. It is probably stale, or hiding a typo.

Skip the cross-check with --no-cross-check if you only want the file-level validation.

Flags

FlagDescription
--domain <domain>Required. The domain to validate.
--jsonPrint the full result as JSON. Mutually exclusive with --sarif.
--sarifPrint a SARIF 2.1.0 report for code-scanning upload. Mutually exclusive with --json.
--package <name>Expected Android package name. Falls back to app.json when omitted.
--sha256 <fingerprint>Expected SHA-256 signing-cert fingerprint to look for in assetlinks.json.
--app-dir <dir>Expo Router app directory for the route cross-check.
--config <module[#export]>React Navigation linking module for the route cross-check.
--no-cross-checkSkip matching the route table against AASA components.

Caching and propagation

Apple serves AASA through a CDN, so changes can take 24 hours or more to reach devices (a first-time fetch can take days). While testing, add ?mode=developer to the Associated Domains entitlement to bypass the CDN, and reinstall the app to force a re-fetch. Android re-verifies App Links on install and update. rndl validate surfaces the caching note in its output and never treats a fresh change as a hard failure.

Every diagnostic has a stable code (AASA_REDIRECTED, ASSETLINKS_FINGERPRINT_MISMATCH, and so on) with a concrete fix. The troubleshooting page maps the ten most common deep-link failures to the code that catches each one.