AASA Dynamic/Static Serving and Path Control#
ํ๋ง๋๋ก (๋น๊ฐ๋ฐ์์ฉ)#
์ด ์คํฌ์ "ํน์ ์น ์ฃผ์๋ฅผ ๋๋ ์ ๋ ์ฑ์ด ์ด๋ฆด์ง, ๊ทธ๋ฅ ์นํ์ด์ง๋ก ์ด๋ฆด์ง"๋ฅผ ๊ฒฐ์ ํ๋ ๊ท์นํ(AASA)๋ฅผ ๋ค๋ฃน๋๋ค. ํธํ ์๋ด๋ฐ์คํฌ๊ฐ ์๋์ ๊ฐ์ค๋ก ์๋ดํ ์ง ๋ก๋น์์ ์ฒ๋ฆฌํ ์ง ์ ํ๋ ๊ฒ์ฒ๋ผ, ์ด๋ค ๋งํฌ๋ ์ฑ์ผ๋ก ๋ณด๋ด๊ณ ์ด๋ค ๋งํฌ(์: ๋ก๊ทธ์ธ ์ฒ๋ฆฌ์ฉ ์ฃผ์)๋ ์ผ๋ถ๋ฌ ์ฑ์ผ๋ก ๋ณด๋ด์ง ์๋๋ก ๊ธธ์ ์ก์์ค๋๋ค. ์ด ๊ธธ ์๋ด๊ฐ ์๋ชป๋๋ฉด ์์ ๋ก๊ทธ์ธ์ด ์ค๊ฐ์ ๋๊ธฐ๊ฑฐ๋ ๋งํฌ๊ฐ ์ฑ์์ ์ ์ด๋ฆฌ๋ ๋ฌธ์ ๊ฐ ์๊น๋๋ค.
๋ฌด์์ยท์ธ์ (๋น๊ฐ๋ฐ์์ฉ)#
-
๋ฌด์์ ํด์ฃผ๋์
- ํน์ ์ฃผ์(์: ์์ ๋ก๊ทธ์ธ ์ฝ๋ฐฑ)๋ ์ฑ์ด ๊ฐ๋ก์ฑ์ง ์๋๋ก ์ ์ธ ๊ท์น์ ๋ง๋ค์ด ์ค๋๋ค.
- ์ด ๊ท์นํ(AASA)๋ฅผ ์๋ฒ๊ฐ ์๋์ผ๋ก ๋ง๋ค์ด ๋ด๋ณด๋ด๊ฑฐ๋, ๊ณ ์ ํ์ผ๋ก ๋๋น์ฑ ์ ๋ ์ ์๊ฒ ๊ตฌ์ฑํฉ๋๋ค.
- ์ฌ๋ฌ ๋๋ฉ์ธยท์ฑ ํ๊ฒฝ(๊ฐ๋ฐ/์คํ ์ด์ง/์ด์)์์ ๊ท์น์ด ์ด๊ธ๋์ง ์๋๋ก ๋ง์ถฐ ์ค๋๋ค.
- Firebase Hosting์ด ์๋์ผ๋ก ๋ง๋ ์๋ชป๋ ๊ท์นํ๋ฅผ ์ฐ๋ฆฌ ๊ฒ์ผ๋ก ๋ฎ์ด์ฐ๋๋ก ๋์์ค๋๋ค.
-
์ธ์ ์ฐ์ด๋์
- ์์ ๋ก๊ทธ์ธ(๋ค์ด๋ฒ, ์นด์นด์ค ๋ฑ)์ ๋๋ ๋๋ ๋ก๊ทธ์ธ์ด ๋๋๊ธฐ๋ ์ ์ ์ฑ์ด ๋จผ์ ์ด๋ ค์ ๋ก๊ทธ์ธ์ด ์คํจํ ๋
- iOS์์ ๋งํฌ๋ฅผ ๋๋ฌ๋ ์ฑ์ด ์ ์ด๋ฆฌ๊ฑฐ๋, ๋ฐ๋๋ก ์ฑ์ผ๋ก ๋ค์ด๊ฐ๋ฉด ์ ๋๋ ์ฃผ์๊น์ง ์ฑ์ด ์ด๋ฆด ๋
- ์ ์์ ๋ก๊ทธ์ธ ์๋จ์ ์ถ๊ฐํ๊ฑฐ๋, ๋๋ฉ์ธ/์ฑ์ ์๋ก ์ค์ ยท๋๋ฒ๊น ํ ๋
ํต์ฌ ์ฉ์ด (๋น๊ฐ๋ฐ์์ฉ)#
| ์ฉ์ด | ์ฌ์ด ์ค๋ช |
|---|---|
| AASA (apple-app-site-association) | ์ ํ์ด ์ ํ "์ด ์ฃผ์๋ ์ฑ์ผ๋ก ๋ณด๋ผ์ง ๋ง์ง" ๊ท์น์ ๋ด์ ํ์ผ |
| Universal Link | ์น ์ฃผ์๋ฅผ ๋๋ ์ ๋ ์๋์ผ๋ก ์ฑ์ด ์ด๋ฆฌ๊ฒ ํด์ฃผ๋ ์ ํ์ ๋งํฌ ๋ฐฉ์ |
| Path (๊ฒฝ๋ก) | ์น ์ฃผ์์์ ๋๋ฉ์ธ ๋ค์ ๋ถ๋ ๋ถ๋ถ(์: /auth/...), ์ด๋ค ํ์ด์ง์ธ์ง ๊ฐ๋ฆฌํค๋ ๊ธธ |
| NOT ๊ท์น | "์ด ์ฃผ์๋ค์ ์ฑ์ผ๋ก ๋ณด๋ด์ง ๋ง"๋ผ๊ณ ์ฝ ์ง์ด ์ ์ธํ๋ ๊ท์น |
| OAuth ์ฝ๋ฐฑ | ์์ ๋ก๊ทธ์ธ ํ ์๋ฒ๊ฐ ์ธ์ฆ์ ๋ง๋ฌด๋ฆฌํ๊ธฐ ์ํด ์ ๊น ๊ฑฐ์น๋ ์ค๊ฐ ์ฃผ์ |
| Dynamic AASA (๋์ ) | ์๋ฒ๊ฐ ๊ทธ๋๊ทธ๋ ๊ท์นํ๋ฅผ ๋ง๋ค์ด ๋ด๋ณด๋ด๋ ๋ฐฉ์ |
| Static AASA (์ ์ ) | ๋ฏธ๋ฆฌ ๋ง๋ค์ด ๋ ๊ณ ์ ํ์ผ๋ก ๊ท์นํ๋ฅผ ๋ด๋ณด๋ด๋ ๋ฐฉ์ (์๋ฒ๊ฐ ์ฃฝ์ด๋ ๋์) |
| Firebase Hosting | ์น์ฌ์ดํธ๋ฅผ ๋น ๋ฅด๊ฒ ๋ณด์ฌ์ฃผ๋ ๊ตฌ๊ธ์ ํธ์คํ /CDN ์๋น์ค |
| CDN ์บ์ | ๋น ๋ฅธ ์๋ต์ ์ํด ์ ์ ์ ์ฅํด ๋ ์ฌ๋ณธ, ๋ฐ๋ ๋ด์ฉ์ด ๋ฆ๊ฒ ๋ฐ์๋ ์ ์์ |
| Serverpod | ์ด ํ๋ก์ ํธ๊ฐ ์ฐ๋ ์๋ฒ ํ๋ ์์ํฌ(๋ฐฑ์๋ ๋๊ตฌ) |
| Bundle ID / Team ID | ์ฑ๊ณผ ๊ฐ๋ฐํ์ ๊ตฌ๋ถํ๋ ์ ํ์ ๊ณ ์ ์๋ณ ๋ฒํธ |
Covers dynamically generating AASA (Apple App Site Association) on the server and excluding specific paths from Universal Links.
Triggers#
- Excluding specific paths from AASA (OAuth callbacks, etc.)
- Dynamic AASA serving setup
- Multi-domain/app AASA management
- Universal Links debugging
Path Exclusion Pattern#
Problem Scenario#
When OAuth login (Naver, Kakao, etc.) server callback URLs (/auth/naver/callback) match the AASA
"paths": ["*"] setting as iOS Universal Links, the app opens before the server completes token exchange.
1. User โ Naver login page
2. Naver โ Server /auth/naver/callback (token exchange needed)
3. โ iOS redirects /auth/naver/callback to app
4. Server token exchange incomplete โ Login failure
Solution: NOT Prefix#
{
" applinks " : {
" details " : [
{
" appID " : " TEAM_ID.BUNDLE_ID " ,
" paths " : [
" NOT /auth/* " ,
" NOT /internal/* " ,
" NOT /api/* " ,
" * "
]
}
]
}
}
Paths to Exclude#
| Path | Reason |
|---|---|
/auth/* | OAuth callbacks (Naver, Kakao, Google, etc.) |
/internal/* |
Server-to-server communication (Lambda callbacks, webhooks, etc.) |
/api/* | REST API endpoints |
/webhooks/* | External service webhooks (payment, notifications, etc.) |
/health | Health check endpoint |
Evaluation Order#
Apple AASA paths evaluation rules:
- NOT rules are evaluated first โ If matched, excluded from Universal Links
- Include rules evaluated โ
*,/books/*, etc. - First matching rule applies
Dynamic AASA Serving (Serverpod)#
WellKnownRoute Pattern#
class WellKnownRoute extends Route {
Map<String, dynamic> _appleAppSiteAssociation() {
return {
'applinks': {
'apps': <String>[],
'details': [
{
'appID': '\${EnvConfig.appleTeamId}.\${EnvConfig.packageName}',
'paths': [
'NOT /auth/*',
'NOT /internal/*',
'NOT /api/*',
'*',
],
},
],
},
'webcredentials': {
'apps': [
'\${EnvConfig.appleTeamId}.\${EnvConfig.packageName}',
],
},
};
}
@override
FutureOr<Result> handleCall(Session session, Request request) {
if (request.url.path == '/.well-known/apple-app-site-association') {
final body = jsonEncode(_appleAppSiteAssociation());
return Response.ok(
body: Body.fromString(body, mimeType: .json),
headers: Headers.build((headers) {
headers[HttpHeaders.cacheControlHeader] = ['max-age=31536000'];
}),
);
}
return Response.notFound();
}
}
Dynamic vs Static AASA#
| Method | Advantages | Disadvantages |
|---|---|---|
| Dynamic (server code) | Auto-applies per-environment Bundle ID, code-managed | Server dependency |
| Static (file) | Served even when server is down | Manual per-environment management |
Recommended: Use dynamic AASA as default, maintain static file as fallback.
Static AASA Fallback#
web/.well-known/apple-app-site-association
Include Bundle IDs for all environments (dev/stg/prod):
{
" applinks " : {
" details " : [
{
" appIDs " : [
" TEAM_ID.com.example.app.dev " ,
" TEAM_ID.com.example.app.stg " ,
" TEAM_ID.com.example.app "
],
" paths " : [
" NOT /auth/* " ,
" NOT /internal/* " ,
" NOT /api/* " ,
" * "
]
}
]
}
}
Difference from Android assetlinks.json#
| Item | iOS AASA | Android assetlinks.json |
|---|---|---|
| Path control location | Server (AASA paths) | App (AndroidManifest intent-filter) |
| Exclusion method | NOT /path/* |
Only specify included paths in intent-filter |
| Config change | Server deploy required | App update required |
โ No assetlinks.json modification needed for Android (handles domain-level verification only)
Verification#
# Check dynamic AASA
curl -s https://yourdomain.com/.well-known/apple-app-site-association | jq .
# Verify NOT rule behavior: /auth/ path should NOT open app
# Access https://yourdomain.com/auth/naver/callback in iOS Safari
# โ Should be handled on web, not open the app
# Check Apple CDN cache (up to 24-hour delay)
curl -s https://app-site-association.cdn-apple.com/a/v1/yourdomain.com | jq .
Firebase Hosting AASA Interception#
Problem: Firebase Hosting Auto-Generates AASA#
When using Firebase Hosting as a reverse proxy/CDN, .well-known/apple-app-site-association
requests do not reach the backend server.
iOS device โ DNS โ Firebase Hosting CDN โ Backend server (Serverpod, etc.)
โ
AASA intercepted here
Firebase auto-generates AASA when an iOS app is registered in the project. This auto-generated AASA only excludes Firebase internal paths (/__/auth/*,
/_/*) with NOT prefix, and does not include custom NOT rules (e.g., NOT /auth/*).
Result#
- Backend server's
WellKnownRouteis correctly configured but Firebase responds first - OAuth callbacks (
/auth/naver/callback, etc.) match as Universal Links and open the app - Server token exchange incomplete โ Login failure
Solution: Deploy Custom Static AASA File#
Deploying a static AASA file in Firebase Hosting's public directory serves with higher priority
than Firebase's auto-generated AASA.
Priority:
1st: public directory static files โ Custom AASA
2nd: Firebase auto-generated AASA
3rd: rewrites rules (backend proxy)
Step 1: Create Static AASA File
web/.well-known/apple-app-site-association
{
" applinks " : {
" apps " : [],
" details " : [
{
" appID " : " TEAM_ID.BUNDLE_ID " ,
" paths " : [
" NOT /auth/* " ,
" NOT /internal/* " ,
" NOT /api/* " ,
" NOT /__/auth/action/ " ,
" NOT /__/auth/handler/ " ,
" NOT /_/* " ,
" /* "
]
}
]
}
}
Important: Firebase default NOT rules (
/__/auth/*,/_/*) must also be included.
Step 2: firebase.json Configuration
{
" hosting " : {
" public " : " build/web " ,
" ignore " : [ " firebase.json " , " **/node_modules/** " ],
" headers " : [
{
" source " : " /.well-known/apple-app-site-association " ,
" headers " : [
{ " key " : " Content-Type " , " value " : " application/json " }
]
}
]
}
}
Cautions:
-
Must remove
"**/.*"fromignorearray (so.well-knowndirectory is not ignored) - Explicitly specify Content-Type header (Firebase may not auto-detect)
Step 3: Copy .well-known in Build Script
.well-known directory is not included in build output after flutter build web, so manual copy is needed.
# deploy_web.sh or CI script
flutter build web --release
# Copy .well-known directory
if [ -d " web/.well-known " ]; then
cp -r " web/.well-known " " build/web/.well-known "
fi
# Firebase deploy
firebase deploy --only hosting
Step 4: Post-Deploy Verification
# Verify Firebase Hosting serves custom AASA
curl -s https://yourdomain.com/.well-known/apple-app-site-association | jq .
# Verify NOT /auth/* rule is included
curl -s https://yourdomain.com/.well-known/apple-app-site-association | jq ' .applinks.details[0].paths '
Dynamic and Static AASA Synchronization#
In Firebase Hosting environments, both AASA locations must be synchronized:
| File | Serving Domain | Purpose |
|---|---|---|
web/.well-known/apple-app-site-association |
Firebase Hosting domain (e.g., www.example.com) |
Replaces Firebase-intercepted AASA |
well_known_route.dart (dynamic) |
Backend direct access domain (e.g., api.example.com) |
Served directly by backend server |
The paths settings in both files must be identical.
Troubleshooting#
When Universal Links Are Not Working#
- Verify AASA Content-Type is
application/json - Verify direct HTTPS serving without redirects
- Wait for Apple CDN cache (24 hours)
- iOS Settings โ Developer โ Associated Domains diagnostics
When OAuth Callbacks Open the App#
- Add
NOT /auth/*to AASA paths - When using Firebase Hosting: Deploy custom static AASA file (server deploy alone is insufficient)
- Redeploy server + Firebase Hosting
- Wait for Apple CDN cache refresh (or reinstall app)
- Access callback URL directly in Safari to verify web handling
When AASA Doesn't Change After Firebase Hosting Deploy#
- Verify
"**/.*"removed fromfirebase.jsonignore - Verify
web/.well-knownโbuild/web/.well-knowncopy in build script - Wait for Firebase CDN cache (15 minutes)
- Verify with
curl -H "Cache-Control: no-cache"to bypass cache
Checklist#
- NOT prefix applied to server-only paths
- Both dynamic and static AASA fallback have identical paths settings
- New OAuth providers covered by
/auth/*pattern - Firebase Hosting: Custom static AASA file deployed
-
Firebase Hosting:
**/.*ignore removed fromfirebase.json -
Firebase Hosting:
.well-knowncopy added to build script - Post-deploy AASA response verified with
curl - Tested on iOS device after Apple CDN cache refresh