You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Corrected 2 Sep 2026: the owed DNS-and-hosting checklist now exists on main as docs/19-universal-links-setup.md (PR #476, since updated); the human DNS step is still not done — brewpath.maximsan.dev was NXDOMAIN when checked. The prototype’s “Copy link” stub cite moved with a prototype re-drop: it sits in ShareStreakSheet, screens.jsx:411, not 433.
What to build
A tapped https://brewpath.maximsan.dev/card/<id> link opens BrewPath on that card. Without the app, it lands in the App Store.
Ruling: #34's resolution and its amendment — universal links with an App Store fallback, two addressable targets (/ and /card/<id>), the designed landing page deferred, the claim on a dedicated subdomain, never / of maximsan.dev (a root claim hijacks the owner's CV site, fails silently for everyone without the app, and Apple caches the AASA aggressively). No plugin and no platform channel: Flutter routes deep links natively into the Router API that go_router implements.
What an unearned card shows — ruled
ADR-0015: the face, not the payload. The art, the title, the lesson that earns it and a way in — but not the summary and not the keepsake line.
This settles the disagreement between #34's locked preview and what #385 shipped. The masking guards against id guessing (c1, c2, c-m2l1 are trivially typed), not against sharing — a sender can only share a card they earned, so a recipient seeing one they have not is the point rather than the leak.
Two shipped things change with it, both from #385: card_deep_link.dart opens nothing for an unearned card and states that as the rule in its comment, and card_sheet_test.dart asserts "a link to an unearned card opens nothing".
Already built
#385 landed the route: /card/:id resolves through CardDeepLink to the card sheet raised over the Cards tab's own grid, and pops back onto it. Nothing else in this ticket exists.
The work
Human step — DNS and hosting (the one part an agent cannot do). brewpath.maximsan.dev needs a DNS record and somewhere to serve one static file over HTTPS. The checklist is written — docs/19-universal-links-setup.md (PR #476) — but the step itself is still open: brewpath.maximsan.dev does not resolve today.
The AASA file at https://brewpath.maximsan.dev/.well-known/apple-app-site-association — no .json extension, Content-Type: application/json, no redirect (Apple will not follow one), naming <TeamID>.dev.maximsan.brewPath, paths / and /card/*. Safe here only because the subdomain hosts nothing else.
The Associated Domains entitlement — applinks:brewpath.maximsan.dev. ios/Runner/ currently has no entitlements file at all. Needs the Team ID, which docs/14-ios-release-checklist.md already requires.
A catch-all redirect to the App Store for every other path — a hosting config line, not a page.
The pending target must survive onboarding. Live bug: app_router.dart's redirect sends an un-onboarded user to /welcome and drops the target, so someone who installs the app because of a shared card never sees it. Deep links in v1: universal links, a domain, and what a shared collectible opens #34 calls this the single most likely bug in the feature.
An unknown <id> falls back to / silently — version skew, not user error: content is bundled per build, so a newer build can mint a link an older install cannot resolve.
The share affordance that mints a /card/<id> link does not exist — the only share in the app is the streak card, and the prototype's "Copy link" tile is a stub that copies nothing (the ShareTarget tile in ShareStreakSheet, screens.jsx:411). Deep links in v1: universal links, a domain, and what a shared collectible opens #34 rules it net-new design, not a port. Owned by #477, which this ticket blocks. Until that lands, the only link anyone can produce is the marketing / one.
A product domain.brewpath.maximsan.dev is correct for v1 and costs nothing. Switching later is non-breaking — the entitlement accepts multiple domains — so every link shared before the switch keeps working.
A link to an earned card opens that card, and a link arriving before onboarding finishes resolves after it rather than being discarded — card_link_test.dart and app_redirect_test.dart, including the case where the app's own navigation races the gate and used to overwrite the target
An address no card could own degrades silently — an unknown id leaves the learner on the collection, a malformed /card/a/b lands on the course. Read as Deep links in v1: universal links, a domain, and what a shared collectible opens #34's "fall back to /": the app's home, never an error screen. Literal / is the Loading screen, which would be worse
The one thing outstanding. Gates are clean — dart format, flutter analyze, dart_code_linter:metrics, flutter test (2228), the changelog check and flutter build ios --no-codesign, all green in PR #483 — and the checklist is handed over at docs/19-universal-links-setup.md. The AASA file is not yet served, which is the human step this ticket keeps: DNS for brewpath.maximsan.dev, the file, and the curl in step 5. Until then no link opens the app, whatever the app does.
What to build
A tapped
https://brewpath.maximsan.dev/card/<id>link opens BrewPath on that card. Without the app, it lands in the App Store.Ruling: #34's resolution and its amendment — universal links with an App Store fallback, two addressable targets (
/and/card/<id>), the designed landing page deferred, the claim on a dedicated subdomain, never/ofmaximsan.dev(a root claim hijacks the owner's CV site, fails silently for everyone without the app, and Apple caches the AASA aggressively). No plugin and no platform channel: Flutter routes deep links natively into the Router API thatgo_routerimplements.What an unearned card shows — ruled
ADR-0015: the face, not the payload. The art, the title, the lesson that earns it and a way in — but not the summary and not the keepsake line.
This settles the disagreement between #34's locked preview and what #385 shipped. The masking guards against id guessing (
c1,c2,c-m2l1are trivially typed), not against sharing — a sender can only share a card they earned, so a recipient seeing one they have not is the point rather than the leak.Two shipped things change with it, both from #385:
card_deep_link.dartopens nothing for an unearned card and states that as the rule in its comment, andcard_sheet_test.dartasserts "a link to an unearned card opens nothing".Already built
#385 landed the route:
/card/:idresolves throughCardDeepLinkto the card sheet raised over the Cards tab's own grid, and pops back onto it. Nothing else in this ticket exists.The work
Human step — DNS and hosting (the one part an agent cannot do).
brewpath.maximsan.devneeds a DNS record and somewhere to serve one static file over HTTPS. The checklist is written —docs/19-universal-links-setup.md(PR #476) — but the step itself is still open:brewpath.maximsan.devdoes not resolve today.https://brewpath.maximsan.dev/.well-known/apple-app-site-association— no.jsonextension,Content-Type: application/json, no redirect (Apple will not follow one), naming<TeamID>.dev.maximsan.brewPath, paths/and/card/*. Safe here only because the subdomain hosts nothing else.applinks:brewpath.maximsan.dev.ios/Runner/currently has no entitlements file at all. Needs the Team ID, whichdocs/14-ios-release-checklist.mdalready requires.app_router.dart's redirect sends an un-onboarded user to/welcomeand drops the target, so someone who installs the app because of a shared card never sees it. Deep links in v1: universal links, a domain, and what a shared collectible opens #34 calls this the single most likely bug in the feature.<id>falls back to/silently — version skew, not user error: content is bundled per build, so a newer build can mint a link an older install cannot resolve./link. Streak surfaces: the streak screen, week strip, freeze tokens and share #26 shipped it as an image with no URL and deferred the link here;sharePnghands over bytes only.Not in this ticket
/card/<id>link does not exist — the only share in the app is the streak card, and the prototype's "Copy link" tile is a stub that copies nothing (theShareTargettile inShareStreakSheet,screens.jsx:411). Deep links in v1: universal links, a domain, and what a shared collectible opens #34 rules it net-new design, not a port. Owned by #477, which this ticket blocks. Until that lands, the only link anyone can produce is the marketing/one.brewpath.maximsan.devis correct for v1 and costs nothing. Switching later is non-breaking — the entitlement accepts multiple domains — so every link shared before the switch keeps working.Acceptance criteria
card_link_test.dartandcard_sheet_test.dart, replacing Card detail is a pushed screen where the design has a sheet — and the keepsake line never renders #385's "opens nothing" and the test that asserted itcard_link_test.dartandapp_redirect_test.dart, including the case where the app's own navigation races the gate and used to overwrite the target/card/a/blands on the course. Read as Deep links in v1: universal links, a domain, and what a shared collectible opens #34's "fall back to/": the app's home, never an error screen. Literal/is the Loading screen, which would be worsedart format,flutter analyze,dart_code_linter:metrics,flutter test(2228), the changelog check andflutter build ios --no-codesign, all green in PR #483 — and the checklist is handed over atdocs/19-universal-links-setup.md. The AASA file is not yet served, which is the human step this ticket keeps: DNS forbrewpath.maximsan.dev, the file, and thecurlin step 5. Until then no link opens the app, whatever the app does.