Skip to content

Instantly share code, notes, and snippets.

@mat
Last active February 19, 2024 07:46
Show Gist options
  • Star 78 You must be signed in to star a gist
  • Fork 12 You must be signed in to fork a gist
  • 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"
]
}
}
@rromanchuk
Copy link

do you know if there is any actual reference documentation somewhere? Even latest is using components key now https://developer.apple.com/documentation/bundleresources/applinks

is * the only matching operator? Does order matter? Will an exclude stop evaluation on remaining paths?

@jenipharachel
Copy link

@rromanchuk Did the above setup work for you? The docs contain the components key like you mentioned. And I could not see the above setup in any docs but most blogs contain the above setup rather than the one mentioned in docs.

@eyekay234
Copy link

@gary-archer @rromanchuk Please help out with this:

I have an A record subdomain called (not real domain) sharing.sample.app with no Alias and a simple routing policy on route 53 in aws. The main website is hosted in S3 and the subdomain just points as stated above.
The app developer needs to upload the apple app association file and i have tried uploading the said file on an ubuntu server that the sub-domain points to which is /var/www/html/sharing but when i try to access the file calling the url https://sharing.sample.app/apple-app-site-association it just reverts me back to https://sharing.sample.app

I then tried to upload the said file to S3 on the public website url at its root and when i call the url https://sample.app/spple-app-site-association it downloads the file which shows me that it sees it.

How do i resolve this so that the url the developer needed to be https://sharing.sample.app/apple-app-site-association resolves and shows what the developer needs to display on the web browser as i tried to validate it on this url: https://branch.io/resources/aasa-validator/ and i got the following messages as an error:

sample.app – This domain has some validation issues
(GREEN) Congrats! Your domain is valid (valid DNS).
(GREEN) Your file is served over HTTPS. Learn More
(RED) Your server returned an error status code (>= 400). This includes client side and server side errors. Want to know what this means? Click here. Learn More
(AMBER) Content type test did not run. Be sure to define a ‘content-type’ header.
(AMBER) JSON test did not run. Your file should contain valid JSON. Learn More

@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