Skip to content
C.W.K.
Lesson 02 of 05 · published

What Voyager Owns, and What It Pointedly Does Not

~14 min · architecture, boundaries, ownership, client

Level 0New Tab
0 XP0/44 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

A browser with no engine of its own

Most of the family's apps are engines: a server process on its own port, a launchd job that keeps it alive, a database. Voyager has none of that. It is a native application on each platform, and its architecture says it in two short sentences: it runs no server and listens on no port. Its house sign adds the rest: no Python, nothing fetched, bundled or built for the engine. The engine is the system's WebKit, already on every Mac, iPhone and iPad.

So Voyager is a client surface. Pippa's identity, conversations and brains stay in cwkPippa. Which family apps exist stays in cwkFirelink's catalog. The shared Swift plumbing stays in cwkSiblingKit. What Voyager adds is the browser itself, and the architecture draws that line twice: once as a list of what it owns, and once, more pointedly, as a list of what it does not.

What Voyager owns

  • Windows, tabs and sessions: what is open, in which order, what sleeps, what comes back after a relaunch.
  • The address field.
  • Identities: named, coloured, isolated logins, several of the same site side by side, tab by tab.
  • Trust zones: which origins are family and what family may do without asking.
  • The page lifecycle: creating, sleeping, waking and discarding web views.
  • The family launcher, as a projection of Firelink's catalog, never a list of its own.
  • The native Pippa mount and the page snapshots it hands her.
  • The host side of web extensions: their tabs, buttons, pop-ups, permission prompts and native messaging.
  • The departure log.

Read that list as a browser engineer and it is everything a user touches. None of it is how a page is drawn.

What it pointedly does not own

Not Voyager'sOwnerWhat Voyager does instead
Rendering, JavaScript, networking, media, the sandbox, the extension runtimeWebKit, patched by Apple through the OSUses them through the public API; never reimplements or bundles them
Which family apps exist and where each servesFirelink's launch catalogReads it at run time, caches it with its date; never keeps a list
Pippa's identity, conversations, memory, models, voice, toolscwkPippaHosts her as a Sidekick; holds no model key and no message store
PasskeysThe systemNever stores one
Tailnet membershipThe local tailscaledNot asked yet in code (see below)
Sync of browsing data between devicesNobody, for nowNot a v1 subject; it waits for evidence

The architecture's own phrase for the Pippa row is the clearest: Voyager is a Sidekick host, not a second Pippa. Pippa Quest teaches the Sidekick framework itself; here the point is only that a browser with Pippa built in still keeps no Pippa of its own.

Every neighbour, and what happens when it is down

The system-context table gives each neighbour a column most designs leave out: when it is down. That column is where a boundary becomes behaviour.

  • Firelink down: the Home and trust use the cached catalog and show its age ("Showing the saved catalog. Its destinations may have changed."). When Firelink refuses for want of a session away from the service host, the Home says so and offers one action, Sign In to Firelink. A stale cache never widens trust beyond its own contents.
  • cwkPippa down: the panel says she could not be reached and tries again by itself. Browsing is unaffected.
  • The Keychain locked: a remembered login is not filled, and the reason is shown as a locked Keychain, never as "no password".
  • The kit: a path dependency, so it matters at build time only, and it is never forked locally. Sibling Kit Quest covers why consuming beats copying.
  • Comet, Chrome and Safari: they own nothing of Voyager's. They are the parity oracle and never a runtime dependency.

Firelink Quest explains the catalog Voyager reads and why one fact has one owner; this lesson only needs the consequence. If Voyager kept its own copy of the family list, the first row of the table would describe two owners, and the 14-of-45 bookmark file would be back.

Owned is not the same as built

Two owned rows are promises the code does not keep yet, and a third neighbour row describes a check the code does not make.

  • The architecture lists "what is blocked before it runs (ads, trackers)" as Voyager's. Nothing blocks yet: there is no WKContentRuleList anywhere in the sources, and the plan's item is open.
  • The address field is documented as taking a URL, a search, a family app by name, or a question for Pippa. The resolver knows the first two only.
  • The system-context table says Voyager asks the local tailscaled whether a host is a tailnet peer. The code never asks. Today a family origin is trusted because the catalog or Settings names it exactly, and an unlisted tailnet host is asked like any site.

Ownership answers who would build it and who is to blame if it is wrong. It does not answer whether it exists. Keep those two questions apart and the boundary stays useful while the product is still growing into it.

The boundary Dad moved

Site passwords were drawn as 1Password's. When the 1Password connection would not hold, Dad moved the line on 2026-10-04, and the architecture quotes the ruling inside the boundary itself:

자체 비밀번호 저장소 … 1password 연동 안 되면 이걸로 fallback

"Our own password store … if the 1Password connection doesn't work, fall back to this."

So Voyager now keeps a store of its own as the fallback, with every password in the Keychain and only an index in its folder. The track on extensions and passwords tells that story. The lesson for boundaries is how it moved: by a dated ruling, written next to the line it changed, so the next reader knows the line is deliberate.

Code

cwkVoyager, Package.swift: the parent imports no WebKit, AppKit or UIKit, and the family's shared Swift arrives by path (verbatim, lines 5-29)·swift
// VoyagerKit: one Swift parent and one engine child, consumed by the Mac app
// here and by the iPhone and iPad app (docs/VOYAGER-ARCHITECTURE.md).
//
//   VoyagerCore     the parent. Tabs, sessions, identities, trust, the
//                   departure log. No WebKit, AppKit or UIKit import.
//   VoyagerWebKit   the engine child: the system's WebKit behind the root
//                   contract VoyagerCore declares.
//   VoyagerApp      the Mac shell: window, tabs, address field, menus.
let package = Package(
    name: "Voyager",
    platforms: [.macOS("15.4"), .iOS("18.4")],
    products: [
        .library(name: "VoyagerCore", targets: ["VoyagerCore"]),
        .library(name: "VoyagerWebKit", targets: ["VoyagerWebKit"]),
        .executable(name: "Voyager", targets: ["VoyagerApp"]),
    ],
    dependencies: [
        // The family's shared Swift, by path like every other consumer.
        .package(path: "../cwkSiblingKit/CwkKit"),
    ],
    targets: [
        .target(
            name: "VoyagerCore",
            dependencies: [.product(name: "CwkKitCore", package: "CwkKit")]
        ),
cwkVoyager, Sources/VoyagerCore/PippaHost.swift: a Sidekick host, speaking cwkPippa's vocabulary and injecting nothing (verbatim, lines 1-10)·swift
// Voyager as a Sidekick host (architecture invariant 8). cwkPippa's
// `web-page` host kind has a panel that asks its host what the page in front
// is; Voyager answers. Nothing is injected into Pippa's prompt: the panel
// stores the answer as a snapshot she reads through `sidekick_host_*` tools.
//
// The vocabulary below is cwkPippa's (docs/PIPPA-CHROME-EMBED.md, "Web Page
// payload kind", and frontend/src/routes/embed/Panel.tsx). Its first host was
// the Chrome extension, so the tab-identity field is named `chrome_tab_key`;
// Voyager fills it with its own tab's key. The field is the kind's "which
// tab", and cwkPippa requires it: a payload without it is refused.
cwkVoyager, Sources/VoyagerCore/FamilyCatalog.swift: the family list as a dated projection that owns nothing (verbatim, lines 60-63)·swift
/// A dated projection of Firelink's registry. It owns no accounts, credentials,
/// health checks or launch policy. Private windows never request a refresh.
@MainActor @Observable
public final class FamilyCatalogModel {

External links

Exercise

Draw the boundary for an app you are building as two tables. In the first, list what it owns. In the second, list what it deliberately does not own, with the owner of each and what your app does instead. Then add a third table of neighbours with one column per question: what it owns, how your app relates to it, and what your app does when it is down. Finally, mark every owned row that is not built yet, and every neighbour row whose check your code does not actually make.
Hint
The "when it is down" column is where vague boundaries show up: if you cannot say whether you use a cache, fail closed or tell the user, the boundary is not decided yet. And an owned row that is unbuilt is fine; an owned row that the docs describe as working is the one that misleads the next reader.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.