Fourteen of forty-five
Before Voyager, the family apps reached Dad's browser through a bookmark file somebody kept by hand. It stopped at 14 apps while the family grew to about 45. Nobody decided to stop it. A copy simply has no way to hear that the original moved.
So the architecture's fourth invariant forbids the copy outright: "Family facts are read, never copied." The app list, its URLs and its favourites come from Firelink's launch catalog at run time, and "no list of apps, ports or hosts lives in Voyager's source, settings or documentation." What the catalog lists, and how Firelink composes it from the family's registry, is Firelink Quest's subject. This lesson is the reading side: how a browser holds a fact it does not own without becoming its second owner.
One client, two readers
The catalog answers at GET /api/v1/launcher/catalog?context=…, where the context is loopback, lan or tailnet. Firelink's own Native Launcher read it first. When Voyager became the second Swift reader, the request and its types moved into the kit's CwkKitCore as FamilyCatalogClient, and both apps now consume that one file. Its header draws the line in one sentence: "Registry composition, credentials, favorites/recents writes and cache ownership stay with the apps." Why a second consumer is the moment to lift code into the kit is Sibling Kit Quest's subject.
The client is strict about what it accepts. A 401 or 403 becomes authorizationRequired. A reply counts as a catalog only when its schema_version is 1, its context echoes the one asked for, its revision is not empty and no two targets share an id. Anything else is invalidResponse, and nothing half-parsed reaches the screen.
A dated projection
FamilyCatalogModel, in Voyager's parent, describes itself in one line: "A dated projection of Firelink's registry. It owns no accounts, credentials, health checks or launch policy." The second block below is the projection. For each target it does four things:
- It drops a target this device cannot open. That is the kit's rule,
opens(on:), and lesson 3 explains it. - A web target survives only with an
httporhttpsscheme, a host, and no user name or password inside the URL. The test suite feeds it ajavascript:target, and that one never reaches the grid. - A native app survives only with a dotted bundle identifier made of letters, digits, dots and hyphens. The Mac opens it through Launch Services by that identifier, and says the app "is not installed on this Mac" when it is not.
- Favourites come first, then titles in the Finder's order (
localizedStandardCompare).
The projection carries the moment it was read, and the Home prints it as "Updated …" beside the count of destinations. A live read during the Home's verification on 2026-10-03 returned 46. That is a dated observation. The code never assumes a count.
A cache that knows where it came from
The model saves the last good catalog as family-catalog.json in Voyager's folder. (The architecture's list of files still calls it catalog-cache.json; the code wins.) The saved entry carries a key, the hub address plus the context: baseURL.absoluteString + "|" + context. When the address changes, the first block clears the projection and shows the saved catalog only if its key matches the new endpoint. One hub's catalog never appears as another hub's answer.
When a read fails and a saved catalog exists, the Home keeps it on screen with one line under it: "Showing the saved catalog. Its destinations may have changed." A single test pins all three halves of that promise. The saved catalog survives an outage, it is not shown for a different endpoint, and the file on disk is byte-for-byte untouched by the failed read.
Late answers and repeated asks
Two reads can be in flight at once, for example when Dad changes the hub in Settings while the old hub is still answering. A generation counter settles it. Each read takes a ticket, and after every await it checks that its ticket is still the newest. A reply that arrives after a newer read began is dropped. The third block is the test: a gate holds two replies open, releases the current endpoint first and the old one second, and expects the current revision to stand.
Repeated asks collapse instead of multiplying. A read already under way absorbs another request, even a forced one, and a projection younger than 60 seconds is not read again unless forced. The Home's Refresh button forces. A private window never asks at all, and the verification build creates the model disabled, so a smoke run never touches the real Firelink.
Three things the Home can say
With a projection, the Home draws the grid. When Firelink refuses because this device has no session with the hub, needsSignIn turns on and the Home offers one action, Sign In to Firelink. The next lesson is about that credential. With neither, the Home says "Family apps are unavailable here right now." and offers to open Firelink. In none of the three does Voyager make up a list of its own.