Deep linking from the web directly into your native iOS application is one of the most effective ways to drive user engagement and conversion. On Apple devices, this is handled through Universal Links. Unlike custom URL schemes, Universal Links are standard HTTP/HTTPS links, making them incredibly secure and resilient.

To enable Universal Links, Apple requires your web server to host a strict, secure JSON configuration file at /.well-known/apple-app-site-association. In this post, we’ll explore how to serve this file dynamically in a Django application deployed to Google Cloud Run, configure your domains, and handle link transitions inside your Swift app.


The Server-Side Challenge: Serving Static JSON with Perfect Headers

Apple's CDN strictly validates your AASA file. It expects:


Step 1: Implementing the Django View

While you could serve the AASA file as a static file, serving it via a Django View allows you to dynamically insert Team IDs, support multiple environments (development vs production), and enforce strict caching headers to reduce server overhead:

from django.http import JsonResponse
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.permissions import AllowAny

class AppleSiteAssociationView(APIView):
    """
    Serves the Apple App Site Association (AASA) file for Universal Links.
    """
    permission_classes = [AllowAny]  # Publicly accessible

    @method_decorator(cache_page(3600))  # Cache for 1 hour to reduce database hits
    def get(self, request, *args, **kwargs):
        # Your Apple Developer credentials
        team_id = "ABC123DEF"
        bundle_id = "com.therandiguys.app"

        aasa_payload = {
            "applinks": {
                "apps": [],
                "details": [
                    {
                        "appID": f"{team_id}.{bundle_id}",
                        "paths": ["*"]  # Support deep linking to all paths
                    }
                ]
            },
            "webcredentials": {
                "apps": [f"{team_id}.{bundle_id}"]
            },
            "appclips": {
                "apps": [f"{team_id}.{bundle_id}.Clip"]
            }
        }
        
        # Explicitly set the Content-Type to application/json
        return JsonResponse(aasa_payload, content_type="application/json")

Step 2: Defining the URL Patterns

Apple expects the file to be served at the root under a .well-known path. Map this in your root urls.py file:

from django.urls import path
from api.views.apple_site_association import AppleSiteAssociationView

urlpatterns = [
    # Map the AASA view at its expected path
    path('.well-known/apple-app-site-association', AppleSiteAssociationView.as_view(), name='aasa_root'),
    path('apple-app-site-association', AppleSiteAssociationView.as_view(), name='aasa_legacy'),
]

Step 3: DNS & Cloud Run Deployment

When deploying to Google Cloud Run, ensure your domain routing is configured properly:

  1. Map Custom Domains: Use GCP Cloud Run custom domain mappings or a Load Balancer to map your domain (e.g. app.yourdomain.com) to your Cloud Run backend.
  2. Update Environment Variables: Ensure the custom domain is registered inside Django's ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS settings so requests aren't blocked by host security headers.
  3. Verify DNS Propagation: Wait a few minutes and run a quick test using curl to confirm the file is visible:
    curl -H "Accept: application/json" https://app.yourdomain.com/.well-known/apple-app-site-association

Step 4: Swift App Integration

Inside your iOS app, you must add the Associated Domains capability in Xcode:

Then, handle incoming deep link URLs inside your App Delegate or Scene Delegate:

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
        if let webpageURL = userActivity.webpageURL {
            // Parse URL and route the user inside your Swift UI
            print("Successfully intercepted deep link: \(webpageURL)")
            return true
        }
    }
    return false
}

By combining Django's caching decorators with dynamic JSON views and GCP custom domain mappings, you can easily deploy a robust, secure, and production-ready deep linking system for iOS Universal Links.