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

What WebKit Gives, and What It Lacks

~14 min · WebKit, public API, WKWebsiteDataStore, trade-offs

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

The judgement in two sentences

After the memory was measured, the evaluation's comparison table ended with two sentences that are worth learning by heart: "Chromium is still the better engine for raw compatibility and extensions. WebKit is far the better engine to build a browser of our own on." Both halves are true at once. This lesson is about holding them together: what WebKit hands a browser of its own through public API, and what it does not have at all.

What public API gives a browser of its own

The evaluation lists it item by item, each with its grade. The ones that shaped Voyager:

  • One isolated store per identity. WKWebsiteDataStore(forIdentifier:), available since macOS 14 and iOS 17, gives a persistent store named by an id you choose. Several can be live at once, and a pop-up inherits its opener's store. In Chromium a tab with its own logins needs its own request context, which is a whole profile, so its extensions and history split off too; keeping only the logins apart while tabs share the rest took Brave a patch set. Here it is one initializer, and nothing else splits. The gate later confirmed it: a cookie and a stored value set in one identity were invisible in the other.
  • A permission decision per request, with the origin. A page asking for the microphone or camera reaches the host with the requesting origin before any prompt, so the host can decide per site.
  • Secure contexts, and their one hard edge. Capture needs a secure context, and WebKit has no public way to mark a plain-http origin secure. Family apps are therefore opened at HTTPS names, or at loopback on the Mac that serves them.
  • Sleep and restore. interactionState carries a page's history, scroll position and form data into a new web view.
  • Page reading. Script in an isolated content world, snapshots, PDF and web archives. There is no public Reader or text-extraction call, so a reader for Pippa has to be written.
  • Content blocking by compiled rule lists, 150,000 rules per list. (The API exists; Voyager has not built ad or tracker blocking yet.)
  • The system's codecs and the system's patches. H.264, HEVC and AAC come with the OS, and security fixes arrive with macOS and iOS updates. The app ships no engine.
  • One Swift codebase for three devices. iOS allows only WebKit outside the EU and Japan, and even there another engine needs a separate entitlement from Apple, so the same engine on the Mac means the same child code on iPhone and iPad.

Extensions are the newest piece. Apple's extension-hosting API for web-view apps (WKWebExtension, WKWebExtensionContext, WKWebExtensionController) is public since macOS 15.4 and iOS 18.4. That is why Voyager's package sets its platform floors at exactly those releases: platforms: [.macOS("15.4"), .iOS("18.4")]. It loads an unpacked Manifest V3 folder as it is, within the APIs WebKit implements, and leaves the tab model, the action pop-up, permission prompts and native messaging to the host. The precedent that made this believable is Orion, a WebKit browser that runs Chrome and Firefox extensions: about 70 percent of the WebExtensions API, version 1.0 in November 2025 after six years of its own work (graded Secondary).

What WebKit lacks, by name

  • Touch ID passkeys. Measured on 2026-10-04 in a build without Apple's web-browser entitlement: PublicKeyCredential exists, but isUserVerifyingPlatformAuthenticatorAvailable() answers false, so a page gets no platform authenticator.
  • Device APIs. WebUSB, Web Serial, Web Bluetooth and WebHID, which WebKit declines, and Web MIDI, which is absent.
  • Chrome's own surroundings. Google Docs offline (Chrome and Edge only), the Chrome Web Store as an install path, Chrome DevTools (Web Inspector instead), and extensions that drive Chrome's tabs through Chrome-only APIs, as the AI assistant extensions do.
  • A tab mute. The public header has calls to pause and suspend media, and no mute; Safari's mute is private. Voyager's tab speaker mutes the page's media elements from its own content world in every frame, so sound made without an element (an Audio object never attached, WebAudio) is neither heard nor muted by it.
  • A find count. find(_:configuration:) answers only whether there was a match. "3 of 17" is counted from the page's own visible text.
  • On iPhone and iPad: service workers in ordinary tabs, Add to Home Screen, default-browser status and passkeys on any site all need the same managed entitlement.

That entitlement was a real decision, and Dad made it on 2026-10-26 after hearing that Apple grants it only with a public TestFlight link and external Beta App Review:

아니, 근데 이거 다 디폴트 브라우져 자격 얻는데 필요한 깐깐한 심사 아니야? 디폴트로 안 쓰면 그만이잖아? 이걸 외부 베타 테스트 심사까지 받는다고? 그건 하면 안 돼.

"Wait, isn't all of this the strict review for default-browser status? If I don't use it as the default, that's the end of it. External beta review, for this? We shouldn't do that." So Voyager carries no web-browser entitlement on any device. The cost is written down as current fact: no Touch ID passkey on a page, so cwkPippa's passkey sign-in shows no button and the family PIN signs in instead.

The family's own web apps, searched

One more check went into the ruling. The family runs many web apps of its own, and a WebKit browser that broke them would fail its first job. So every family frontend was searched for Chromium-only web APIs. Two uses turned up. Bonfire, the music app, asks for Web MIDI and already treats its absence as "no MIDI", so on WebKit its MIDI input simply does not work. Loomis prefers the folder picker and already carries a fallback for Safari. Nothing else in the search reached for Chromium, and the family's web apps were already running on iPhone and iPad, which are WebKit.

Bonfire's MIDI is the one family-side gap, and it stays an open ruling: a native CoreMIDI bridge for that page, or Bonfire's MIDI stays in another browser. The full table of engine-bound gaps, and why naming them keeps "superset" honest, is in the first track's lesson on named gaps.

Principle: An engine choice is honest when its losses are written as a list with names, each with the answer you chose for it, before anyone has to discover them.

Code

cwkVoyager, docs/ENGINE-EVALUATION.md: what a WebKit browser of our own gets from public API (verbatim, lines 92-115)·text
### What a WebKit browser of our own gets from public API

- One persistent, isolated store per identity
  (`WKWebsiteDataStore(forIdentifier:)`, iOS 17 and macOS 14), several live
  at once, and a pop-up inherits its opener's store. Research.
- A per-request decision on microphone and camera with the requesting
  origin, so family origins are granted without a prompt and everything else
  is asked (iOS 15 and macOS 12). Research; the family already uses the
  method in cwkAriandel (coop learning #733).
- Capture needs a secure context, and WebKit has no public way to mark a
  plain-http origin secure. Family apps are therefore opened at their
  Tailscale HTTPS names, or at loopback on the Mac that serves them.
  Research.
- `WKWebView` traffic needs no Local Network permission. Research (Apple
  TN3179).
- Content blocking by compiled rule lists, 150,000 rules per list, several
  lists at once; no programmatic interception of http or https. Research.
- Sleep and restore: `interactionState` carries history, scroll position and
  form data into a new web view (iOS 15, macOS 12). Research.
- Page reading for Pippa: script in an isolated content world, snapshot, PDF
  and web archive; no public Reader or text-extraction call. Research.
- The system's codecs. Expected; exercised in the gate.
- Security fixes arrive with macOS and iOS updates; the app ships no engine.
  Secondary (DuckDuckGo's browser states the same of itself).
cwkVoyager, Sources/VoyagerWebKit/WebKitEngine.swift: one data store per identity, through the public initializer (verbatim, lines 78-93)·swift
    /// One data store per identity, named by the identity's id: its cookies
    /// and site storage are in no other identity's reach.
    func store(for identity: IdentityID) -> WKWebsiteDataStore {
        if let store = stores[identity] { return store }
        // The private identity's store lives in memory for this run only.
        let store = identity == .privateSession ? WKWebsiteDataStore.nonPersistent() : WKWebsiteDataStore(forIdentifier: identity.raw)
        stores[identity] = store
        if identity != .privateSession {
            // The cookie store holds its observers weakly; the engine keeps
            // the watcher, and the watcher keeps no store.
            let watcher = CookieWatcher(identity: identity, engine: self)
            store.httpCookieStore.add(watcher)
            cookieWatchers[identity] = watcher
        }
        return store
    }

External links

Exercise

Choose a platform decision in front of you (an engine, a runtime, a cloud service). Write two lists. The first: what the candidate gives you through public, documented API, each item with its grade (read the documentation yourself, a research summary, a third-party report, or unverified). The second: what it lacks, each gap named precisely enough to test, with the answer you choose for it (closed in your own code, bridged natively, or accepted). Then search your own codebase for every API on the second list and record where you use it today.
Hint
Name gaps at the level of an API or a feature a user would notice ("no find count", "no Web MIDI"), not as adjectives ("less compatible"). A gap with no chosen answer is not finished; even "accepted" is an answer, as long as someone with the authority to accept it did.

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.