Skip to content

Universal links: the dedicated subdomain, the AASA file, and the card route #171

Description

@maximsan

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 entitlementapplinks: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 streak share carries the / 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; sharePng hands over bytes only.

Not in this ticket

  • 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.
  • The designed landing page — deferred by Deep links in v1: universal links, a domain, and what a shared collectible opens #34; the same URLs upgrade in place later, with nothing reissued.
  • 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.

Acceptance criteria

  • A link to an unearned card draws its art, title and the lesson that earns it, and draws neither the summary nor the keepsake line — card_link_test.dart and card_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 it
  • 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature-buildA screen or mechanic the app does not have yetready-for-humanRequires human implementation

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions