7.1 KiB
| name | description |
|---|---|
| web-presence | Set up the web side of a mobile app or game - iOS universal links (apple-app-site-association, Associated Domains entitlement), Android App Links (Digital Asset Links assetlinks.json with the Play app signing SHA-256, intent filter autoVerify), the Safari Smart App Banner meta tag and a minimal landing page with App Store and Google Play links - then check the live files (HTTPS, no redirects, application/json, ids match). Use when the user wants links to open the app, sets up a domain or landing page for an app, sees universal links or app links not opening the app, gets "assetlinks" / "apple-app-site-association" errors, or asks for a Smart App Banner. |
Web presence: universal links, app links, Smart App Banner, landing page
CLI: storebound (or bunx storebound in a project that has it, or bun <toolchain>/cli/index.ts).
storebound web init [--out web] [--landing] [--version-code N] [--dry] [--json]: writes the files into a local folder (defaultweb/). Local writes only; nothing is uploaded.storebound web check [<domain>] [--json]: read-only, no credentials. Fetches the live files fromhttps://<domain>without following redirects and lists every check as ok / problem / info with the fix. Exit code 1 when there is a problem.
Config (storebound.config.ts):
apple.teamId(10 characters, developer.apple.com > Account > Membership details),apple.bundleId,apple.appId(App Store Connect > App Information > Apple ID, for the banner and the landing link).play.packageName.web.domain(links.example.com, no scheme or path),web.paths(default["/*"]),web.exclude(paths that stay in the browser),web.androidSha256(Play app signing SHA-256,AB:CD:...),web.webcredentials(password AutoFill),web.landing,web.out.app.name,app.privacyPolicyUrl,app.supportUrl,app.termsUrlfill the landing page.
Steps
- Pick the domain with the owner. A subdomain only for links (
links.example.com) is the simplest: thenweb.paths: ["/*"]is fine. On a domain that also has a normal website, list only the app paths (["/invite/*", "/open/*"]) so the rest of the site stays in the browser. - Fingerprint for Android. With Play App Signing the key on users' devices is Google's, not the local keystore:
- Play Console > Test and release > App integrity > Play app signing: copy "SHA-256 certificate fingerprint" of the
app signing key into
web.androidSha256. Add the upload key too if you install locally signed builds and want links to verify there (an extra fingerprint is fine). - Or
storebound web init --version-code <a versionCode you uploaded>: reads it read-only from the Play Developer API (generatedapks.list,certificateSha256Hash) and prints it; then put it inweb.androidSha256.
- Play Console > Test and release > App integrity > Play app signing: copy "SHA-256 certificate fingerprint" of the
app signing key into
storebound web init --dry, show the output, thenstorebound web init(add--landingforindex.html). Missing inputs are listed asskipped: <why, how to fix>; unchanged files are left alone.- Owner step: deploy the folder contents to the domain root over HTTPS. Requirements the host must meet:
/.well-known/apple-app-site-association(no extension) and/.well-known/assetlinks.json, status 200,Content-Type: application/json, no redirect (no http -> https, apex -> www or trailing-slash redirect for these paths), public (no login, no IP or bot blocking, any user agent), valid certificate.- Many static hosts skip dot-folders or serve extension-less files as
application/octet-stream: add a headers rule for the AASA path, and make sure a single-page-app fallback does not answer these paths withindex.html. - The files go on every host the app handles (
example.comandwww.example.comare two hosts).
- App side (the code; storebound does not edit it):
- iOS: Signing & Capabilities > Associated Domains:
applinks:<domain>(andwebcredentials:<domain>withweb.webcredentials). The App ID needs the Associated Domains capability. Handle the URL inonOpenURL/scene(_:continue:). While developing against a server that is not public, useapplinks:<domain>?mode=developer(developer mode on the device) to bypass Apple's CDN. - Android: in the activity, an intent filter with
android:autoVerify="true", actionVIEW, categoriesDEFAULTandBROWSABLE,<data android:scheme="https" android:host="<domain>" />plus the same paths asweb.paths(android:pathPattern/pathPrefix).web.excludeis also written as dynamic rules in assetlinks.json (Android 15+); older versions only use the intent filter, so keep excluded paths out of it.
- iOS: Signing & Capabilities > Associated Domains:
storebound web checkuntil there are no problems. Then test on devices: Apple's CDN fetches the AASA within about a day after install and rechecks about weekly; Android verifies at install (adb shell pm get-app-links <package>, re-verify withadb shell pm verify-app-links --re-verify <package>).- Smart App Banner:
smart-app-banner.htmlholds the<meta name="apple-itunes-app" content="app-id=...">tag. Put it in the<head>of each page that should show it (the landing page already has it). Addapp-argument=<url>by hand to pass a deep link. It shows in Safari on iPhone and iPad, not in the simulator or inside frames, and not again after the user closes it.
Landing page
index.html is a small, responsive page with no external requests: the app name, an App Store link
(https://apps.apple.com/app/id<appId>), a Google Play link (https://play.google.com/store/apps/details?id=<package>)
and privacy / terms / support links. The store marks are the simple-icons files (appstore.svg, googleplay.svg,
copied unchanged next to it) shown in the text colour, with the trademark note in the footer. They only name the
store; for Apple's and Google's official badges, download them from Apple's marketing resources and Google Play's
badge page and follow their guidelines. Edit the page freely after the first write; web init reports it as
updated when it would replace your edits, so run --dry first.
Owner decisions
- The domain, which paths open the app, and whether a landing page replaces an existing site.
- Hosting and DNS changes: storebound writes files, the owner deploys them.
Out of scope
- Deploying or hosting the files, DNS, certificates.
- Editing the Xcode entitlements or AndroidManifest.xml.
- Deferred deep links (install then open the right screen) and link attribution: that needs a link service.
- App Clips (
appclipsin the AASA): not generated.
Gotchas
- A redirect on the AASA or assetlinks path breaks verification even when the browser shows the file fine.
text/plainorapplication/octet-streamon assetlinks.json fails Android verification; Apple wantsapplication/jsonand an AASA of at most 128 KB.- Lower-case or colon-less fingerprints: assetlinks.json uses upper-case
AB:CD:...;web initwrites that form. - The local keystore fingerprint usually differs from the Play app signing one: links that work in debug builds can fail in the Play build.
- iOS caches the AASA: after a fix, reinstall the app or wait for the CDN (developer mode skips it while testing).