Part of #79.
Problem
config/mobile.ts declares the entitlement:
associatedDomains: personalTeam ? [] : ['applinks:benchreview.org'],
urlSchemes: ['benchreview'],Nothing serves the other half. Universal links need https://benchreview.org/.well-known/apple-app-site-association — a JSON file naming <APPLE_TEAM_ID>.<IOS_BUNDLE_ID> and the paths it claims. Without it iOS silently declines to open links in the app and falls back to Safari, with no error anywhere: the entitlement is present, the association is missing, and the failure is invisible.
public/.well-known/ does not exist in this repo today.
What to do
Port scripts/generate-mobile-associations.ts from the wildloop reference (58 lines). It reads APPLE_TEAM_ID / IOS_BUNDLE_ID from the environment and writes public/.well-known/apple-app-site-association at build time, so the file ships with the static artifact rather than being hand-maintained.
Decide which paths the app claims. Candidates, given bench's routes:
/judges/*and/judges/*/profile|reviews|rulings|cases— the main share targets (navigator.shareis already called fromBench/Judge/ProfileHeader.stx)/court-houses/*/article/*— published reviews- NOT
/admin/*, and NOT the auth pages (/login,/reset-password,/verify-email/*): a password-reset link opening in an app shell rather than the browser is a worse flow, and an emailed verification link should stay in the browser that requested it.
Checks
- File served at
/.well-known/apple-app-site-associationwithContent-Type: application/jsonand no redirect (iOS does not follow one) appIDmatches the built app's team + bundle id exactly- A link to a judge profile, tapped from Messages, opens the app on the right page
- The
IOS_PERSONAL_TEAM=1path still builds — a free Apple team cannot sign Associated Domains, which is whyconfig/mobile.tsempties the array in that case - The gate's sitemap/robots checks still pass with
.well-known/present
Done (a1b1a6bc, bcc8cb2d)
Ported and adapted rather than copied — wildloop's script claims {"/": "/*"}, everything, which is the wrong policy for a site with a moderation surface.
Written into dist/ during the build, not into public/. It cannot then be committed stale and cannot drift from the app's identity. The part most likely to fail quietly was whether buildApp() carries a dot-directory through to dist/ at all — many copy routines skip hidden files. Verified: it does.
Path policy. /judges/*, /court-houses/* and /article/* are claimed. Excluded: /admin/*, because it is where an anonymized reviewer can be unmasked and that belongs in a browser the person chose rather than whatever app intercepted a tap; and the auth paths, because a reset or verification link is a one-time credential that belongs in the browser that asked for it.
Ordering is load-bearing. iOS takes the first matching component, so an exclusion listed after the include it carves out of is never reached — it sits in the file doing nothing. Both the unit test and gate check 27 pin it, because that mistake cannot be seen by reading the file casually.
webcredentials is deliberately absent. config/mobile.ts entitles applinks: only, and claiming both would assert a capability the signed app does not have.
The check I cannot tick, and why
- A link to a judge profile, tapped from Messages, opens the app on the right page
Needs a real device, a signed build with a non-personal team, and benchreview.org actually serving the file — the deploy is #85/#45. Nothing I can do locally substitutes for it, so it stays open.
The check only a host can satisfy
- Served with
Content-Type: application/jsonand no redirect
The file is in the artifact, but it has no extension by Apple's requirement, so a host inferring the type from one serves application/octet-stream. Measured — that is exactly what Bun's static file handling does with it. iOS rejects anything that is not application/json, at that exact path, and does not follow redirects.
Added to DEPLOY.md as a fourth item in the deploy contract, with Caddy and nginx snippets. Gate check 27 verifies the artifact; only the host can get the content type right, and nothing will report it when it is wrong.
Gate check 27
Present, valid JSON, appIDs matching the built team+bundle, exclusions ahead of inclusions, /admin never claimed. Falsified on all four branches. Reported rather than failed when APPLE_TEAM_ID is unset, matching the Turnstile check — a checkout that is not building the app has no team id, and failing every local build over that would teach people to ignore the gate.