Add an Apple App Site Association (AASA) to your Wix, Squarespace or whatever website, using Cloudflare Workers

I currently work at Spotr, a company building a mobile app. We rely on Wix for the landing page, for the simple reason that it's simple enough to use for the rest of team to update it and design it themselves, letting me focus on back-end and engineering tasks in general.

You wouldn't believe how glad I am about it. One of my core principles? Don't build what you don't have to build. Don't build things you'll needlessly have to maintain.

But life with Wix, and SaaS website builders in general, is not all rosy. We have recently implemented deep (universal) links, for both the iOS and the Android versions, allowing our users to share content outside the app for the first time. Even if standard, it remains pretty awesome. What is one of the prerequisites for iOS Universal Links? An Apple App Site Association (AASA) file. There are even validators for it (thanks, Branch). The thing is, Wix won't let you upload files to the root of your website.

In short. The domain has to point to wix's servers for the website.

And Apple requires us to have that file at the root of our domain to enable universal links (links that open your app at specific views, displaying specific content).

But Wix doesn't give any way of uploading a file anywhere.

Some people worked around it using Wix's http-functions. But why would I trust a company on a highly available distributed runtime when they aren't even capable of properly centering content and making it full height. Seriously? And if their inadequacies are going to make me jump around through weird hoops, I sure as hell ain't going to do it with their tools.

Luckily, I have dabbled in Serverless over the last few years, and even though I never had the chance to work with them, I had learned about Cloudflare Workers when they came out a while ago.

Cloudflare Workers can't build your next skyscrapper just yet, but who knows? - Photo by Tuân Nguyễn Minh / Unsplash

What are these workers? Functions as a Service. At the Edge. Distributed all over the world in Cloudflare's CDN's servers. Pure. Awesomeness.

In our case, we'll interecept requests targeting yourDomain.tld/apple-app-site-association, and statically reply with our AASA file's content, as a JSON. Easy peasy, right?

This post will be assuming that you already use Cloudflare to handle your website's DNS, and know your way around their console. If you don't, rest assured, it's a pretty user-friendly interface anyway.

Given the simplicity of what we're about to do, I'll skip the CLI tool (Wrangler) and go straight to the console. The workers part might seem a bit confusing at first, but once you've managed to find your way around the AWS console, everything else is easy and intuitive.

Write down your AASA file's content

Find your appId, figure out which routes should be handled by your Universal Links, and write down your file's content. If you wanted to apply it to all paths, you would use a wildcard, like this

{
    "applinks": {
      "apps": [],
      "details": [
        {
          "appID": "yourAppId",
          "paths": [
            "*"
          ]
        }
      ]
    }
  }

For all paths except, say... /terms-and-conditions, it would look like that

{
    "applinks": {
      "apps": [],
      "details": [
        {
          "appID": "yourAppId",
          "paths": [
            "*",
            "NOT /terms-and-conditions"
          ]
        }
      ]
    }
  }

And if you only want to apply to specific paths, you could simply do write it down this way

{
    "applinks": {
      "apps": [],
      "details": [
        {
          "appID": "yourAppId",
          "paths": [
            "/products-list"
          ]
        }
      ]
    }
  }

Sidenote: as pointed out to me on Twitter by @Valgrin91, the domain you want to setup with AASA has to be hosted on and proxied through Cloudlare for this to work (cf this Cloudflare documentation article). Hopefully this should save some of you a few hours scratching your head.

Moving on to the fun part!

Your Cloudflare worker.

We'll have to navigate through the Cloudflare dashboard. It can be a bit misleading on the first screen, but straightforward after that.

Log into your coudflare dashboard, and select the domain you want to add the AASA to (don't click "Workers" on the right panel)
Select "Workers" at the top
Go to the Cloudflare Workers editor

Create your Cloudflare Worker

Name it, say... apple-app-site-association.

Create a new worker, name it, and confirm

The new worker will show up in the left menu. Click edit to display it's code.

See, easy!

This is what the default template looks like.

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
})

/**
 * Respond to the request
 * @param {Request} request
 */
async function handleRequest(request) {
  return new Response("Success!", { status: 200 });
}

It discards the request, and returns a cute useless message, along with a HTTP 200 status. A nice start, but we need more. ... Right? Wait. What do we need?

We need to return our content

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
})

/**
 * Respond to the request
 * @param {Request} request
 */
async function handleRequest(request) {
  return new Response(JSON.stringify({
    "applinks": {
      "apps": [],
      "details": [
        {
          "appID": "yourAppId",
          "paths": [
            "*"
          ]
        }
      ]
    }
  }), {
    status: 200
  });
}

And we need to return the proper headers. We are returning JSON after all.

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
})

/**
 * Respond to the request
 * @param {Request} request
 */
async function handleRequest(request) {
  let response = new Response(JSON.stringify({
    "applinks": {
      "apps": [],
      "details": [
        {
          "appID": "yourAppId",
          "paths": [
            "*"
          ]
        }
      ]
    }
  }), {
    status: 200
  });
  response.headers.set("Content-Type", "application/json");
  return response;
}

Now, replace the default code with ours. It should look like this.

Replace the default code, save. You'll get an alert saying it won't deploy until a route is associated with it. Go ahead anyway; we'll be taking care of that in a moment.

Setup your worker's route

Now, we need to setup our worker's route. Wait, what is that "route" thing? It's simply the url(s) whose requests your worker will intercept and decide what to do with them. In our case, yourdomain/apple-app-site-association. Let's set it up!

Click on routes
Add a new route
Set the route in the left text field; in our case something along *yourdomain/apple-app-site-association, pick your worker in the right select, save!

All done? Almost. Remember how our worker wouldn't deploy because it wasn't associated to a route? We need to deploy it now.

Deploy your Cloudflare Worker

Go back to scripts
Edit your worker's script
Hit deploy
Confirm!

Ok, we're done now. Congratulations! You can go tell the iOS developer that his or her Apple App Site Association is online, and he or she can now test thoseUniversal Links. Much easier than the AWS console, right?

High five! - Photo by Camylla Battani / Unsplash

Epilogue

Now, this is a pretty specific and niche use-case. But, using routes, you can replace  anything with a Cloudflare worker. Dynamic pages using data from the Key-Value Store? Done. Dynamically integrating static assets using Cloudflare Storage? Done. A / B testing? Localizing not just the text but your whole app's UI? You name it!

And now, your turn!