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:
- HTTPS only: The endpoint must be protected by a valid, trusted SSL/TLS certificate.
- Perfect MIME Type: It must be served with a Content-Type of exactly
application/json(orapplication/pkcs7-mimeif signed, though standard JSON is now fully supported). - Public Access: It must return a 200 OK without requiring authentication or session cookies.
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:
- 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. - Update Environment Variables: Ensure the custom domain is registered inside Django's
ALLOWED_HOSTSandCSRF_TRUSTED_ORIGINSsettings so requests aren't blocked by host security headers. - 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:
- Open Xcode → Select your target → Signing & Capabilities.
- Add "Associated Domains" and input:
applinks:app.yourdomain.com.
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.