Skip to content

Instantly share code, notes, and snippets.

@mat
Last active November 9, 2024 13:30
Show Gist options
  • Save mat/e35393e9dfd9d7fb0972 to your computer and use it in GitHub Desktop.
Save mat/e35393e9dfd9d7fb0972 to your computer and use it in GitHub Desktop.
apple-app-site-association —with examples

“apple-app-site-association” file

One file for each domain, both www.example.com and example.com need separate files:

{
    "applinks": {
        "apps": [],
        "details": {
            "9JA89QQLNQ.com.apple.wwdc": {
                "paths": [
                    "/wwdc/news/",
                    "/videos/wwdc/2015/*"
                ]
            }
        }
    }
}

When a URL cannot be opened

  • Fall back gracefully
  • If cannot handle, open Safari UIApplication.sharedApplication().openURL(webURL)

Smart App Banners

There is no guarantee that the user experience with custom URL schemes will remain the same in the future. Smart App Banners afford the preferred experience.

https://developer.apple.com/videos/wwdc/2015/?id=509

<head>
  <meta name="apple-itunes-app" content="app-id=640199958, app-argument=https://developer.apple.com/wwdc/schedule, affiliate- data=optionalAffiliateData">
</head>

How often is apple-app-site-association updated by app on device?

I've found that the apple-app-site-association file is fetched when the app is installed on the device. So essentially, every install/update will trigger a download of the file. You can simulate this by deleting the app from your device and reinstalling it.

https://forums.developer.apple.com/thread/6972

{
"activitycontinuation": {
"apps": [
"5LL7P8E8RA.com.airbnb.app",
"5LL7P8E8RA.com.airbnb.appdev",
"5LL7P8E8RA.com.airbnb.appbeta",
"5LL7P8E8RA.com.airbnb.appenterprise",
"9BPWRS9A4J.com.airbnb.app",
"9BPWRS9A4J.com.airbnb.appdev",
"9BPWRS9A4J.com.airbnb.appbeta",
"9BPWRS9A4J.com.airbnb.appenterprise",
"KYLDQ3QJT3.com.airbnb.app",
"KYLDQ3QJT3.com.airbnb.appdev",
"KYLDQ3QJT3.com.airbnb.appbeta",
"KYLDQ3QJT3.com.airbnb.appenterprise"
]
},
"webcredentials": {
"apps": [
"5LL7P8E8RA.com.airbnb.app",
"5LL7P8E8RA.com.airbnb.appdev",
"5LL7P8E8RA.com.airbnb.appbeta",
"5LL7P8E8RA.com.airbnb.appenterprise",
"9BPWRS9A4J.com.airbnb.app",
"9BPWRS9A4J.com.airbnb.appdev",
"9BPWRS9A4J.com.airbnb.appbeta",
"9BPWRS9A4J.com.airbnb.appenterprise",
"KYLDQ3QJT3.com.airbnb.app",
"KYLDQ3QJT3.com.airbnb.appdev",
"KYLDQ3QJT3.com.airbnb.appbeta",
"KYLDQ3QJT3.com.airbnb.appenterprise"
]
}
}
{
"applinks" :
{
"apps" : [],
"details" :
{
"9JA89QQLNQ.developer.apple.wwdc-Debug" :
{
"paths" :
[
"/videos/wwdc/2011/*",
"/videos/wwdc/2012/*",
"/videos/wwdc/2013/*",
"/videos/wwdc/2014/*",
"/videos/wwdc/2015/*"
]
},
"9JA89QQLNQ.developer.apple.wwdc-Internal" :
{
"paths" :
[
"/videos/wwdc/2011/*",
"/videos/wwdc/2012/*",
"/videos/wwdc/2013/*",
"/videos/wwdc/2014/*",
"/videos/wwdc/2015/*"
]
},
"9JA89QQLNQ.developer.apple.wwdc-Release" :
{
"paths" :
[
"/videos/wwdc/2011/*",
"/videos/wwdc/2012/*",
"/videos/wwdc/2013/*",
"/videos/wwdc/2014/*",
"/videos/wwdc/2015/*"
]
},
}
}
}
{
"activitycontinuation": {
"apps": [
"7QXYLT27C4.fm.overcast.overcast"
]
},
"webcredentials": {
"apps": [
"7QXYLT27C4.fm.overcast.overcast"
]
}
}
@gary-archer
Copy link

gary-archer commented Sep 24, 2022

You should be able to verify this with the curl tool:

curl --http1.1 -i https://mobile.authsamples.com/.well-known/apple-app-site-association

A valid response has key fields similar to this:

HTTP/1.1 200 OK
Content-Type: application/json

{
    "applinks": {
        "apps": [],
        "details": [
            {
                "appID": "U3VTCHYEM7.com.authsamples.basicmobileapp",
                "paths": [ "/basicmobileapp/*" ]
            }
        ]
    }

If the response has a 302 redirect or does not indicate JSON then the server setup is incorrect. More info in my blog post, which is focused on using deep linking to receive OpenID Connect responses via claimed HTTPS schemes as recommended here. Note also that the blog post is a little out of date and needs updating to use authsamples instead of authguidance.

@rromanchuk
Copy link

rromanchuk commented Sep 24, 2022

@eyekay234 i would just simplify your life forever, and i'm just recommending because you're already using everything that makes this simple. Not a lot of people do this but mostly because they aren't aware you can. I actually serve this file direct from cloudfront, it doesn't even hit an s3 origin.

  1. Set every request for the entirety of the domain to a single CloudFront distribution
  2. If you had an ALB, set the default behavior and origin to the ALB. Use caching disabled and forward all headers
  3. Create a new origin to your s3 bucket (CloudFront can autocreate the permissions for it to be able to fetch from it)
  4. Create a new behavior matching /.well-known/apple-app-site-association with S3CachingOptomized, S3Origin
  5. Create a new behavior matching /apple-app-site-association with S3CachingOptomized, S3Origin
  6. If serving static pages or assets in s3, organize your paths like a unique namespace to utilize
  7. create a new behavior for that matches /assets/* to managed S3CachingOptimized, S3Cors, whatever compression etc that makes sense for what you're serving
  8. Create a behavior for index.html, or /pages/* or whatever your s3 structure may be

Pro tips

  • Do not namespace paths on hostname (subdomains). AKA do not overload paths, unique paths across all paths of the tld. ie. You want to send dynamic requests to nginx, or websockets, dont use sockets.mydomain.tld, use mydomain.tld/sockets OR sockets.mydomain.tld/sockets. Don't use images.mydomain.tld use mydomain.tld/images OR images.mydomain.tld/images
  • Make origins work for you. Let's say you have subdomains with different robots.txt directives. Maybe you create a bucket called annoying-crawler-files with robots.txt, sitemaps.xml, subdomain.robots.txt, create a behavior for /robots.txt, even in the most complex cases, attach a simple 3 line CF function to that behavior to select the correct file path based on hostname

TLDR:

  • Every single route in r53 has A and AAAA with alias pointing to a single CloudFront distribution.
  • Every single s3 bucket is, and forever remains "no public access"
  • No one is ever served a file by accessing an s3 bucket directly
  • Centralize caching, cors, compression, http/s requirements regardless of origin (S3, API gateway, Lambda, ALB, nginx instance, 3rd party)

Unrelated, but makes these critical/annoying/fragile tasks so much easier to work with from a simple static site, to full size production multi-origin, multi-service, serverless to monolith service.

@jenipharachel sorry years late, i have to look with what i ended up with

@eyekay234
Copy link

@gary-archer @rromanchuk Thanks a lot for the help as i was able to resolve this

@muizidn
Copy link

muizidn commented Oct 3, 2022

Hello. Thank you for sharing this. I find that this different format really annoying because in my case, the one with components like in the docs doesn't work in iOS15.
If anyone want to try fast, one can use this little project of mine. https://github.com/muizidn/apple-app-site-association.test

@walteh
Copy link

walteh commented Nov 13, 2022

Another option for anyone who needs it:

You can create a CloudFront Function and associate it to your distribution's cache behavior as a viewer-request function to bypass hosting the file completely.

function handler(event) {
	if (event.request.uri.endsWith("apple-app-site-association")) {
		return {
			statusCode : 200,
			statusDescription : "OK",
			headers : { "content-type" : { value : "application/json" } },
			body : {
				encoding : "text",
				data :  "{\"applinks\":{\"details\":[{\"appIDs\":[\"TEAMID.com.example.app\"],\"components\":[{\"/\":\"/test/*\",\"comment\":\"Matches any URL with a path that starts with /test/.\"}]}]},\"webcredentials\":{\"apps\":[\"TEAMID.com.example.app\"]}}"
			}
		 }
	}
	return event.request;
}

@wwwmaster1
Copy link

this is the best method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment