zero-fear browsing Β· one sovereign core Β· two lanes
the WebKit engine you already trust, wrapped in boundaries you can read
stable keeps WebKit's defaults Β· nightly flips every experimental feature ON Β· the decisions live in Rust
Qwave is an open-source, WebKit-native browser for people who want stronger boundaries around browsing data without giving up the system engine. It runs on macOS 14+ and on iPhone, iOS 15+ (minimum device: iPhone 13), sharing one sovereign core between the two lanes. Part of the 8b.IS Ecosystem and documented in the 8b.IS Documentation Hub. It combines per-container storage universes, native content shields, tab hibernation, an on-device page summarizer, a Rust sovereign core (the egress allowlist and the MEM8 wave substrate, compiled as a zero-dependency staticlib and linked into the app), and an egress allowlist that makes Qwave's own network activity auditable.
Qwave is privacy-oriented software, not a promise of anonymity. Read the threat model, the network inventory, and the 8b.IS Architectural Documentation.
- π§ What makes it different
- β‘ Install & update
- π¬ How it works β concept diagrams
- π Architecture at a glance
- πΊ Module map
- β Swift 6 engineering rules
- π Repository map
- π· Versioning & releases
- π Documentation
- π¦ Status
- β License & legal
- Container universes β each profile gets its own WebKit data store; ephemeral tabs use non-persistent storage and are never restored.
- Native shields & MV3 DNR β EasyList/uBlock rules compiled to native WebKit C++ content rules at sub-millisecond speeds with per-host JavaScript controls.
- Apple Silicon UMA SQLite β 256MB zero-copy
mmap_size, WAL mode, 64MB cache, and 4 P-core sorting threads for instant URL completions and history search. - Persistent FaviconStore β container-isolated SQLite BLOB caching for instant favicon rendering with zero cross-container telemetry leakage.
- On-device omnibox β suggestions from local history, bookmarks, open tabs, and actions, ranked on every keystroke. Network search suggestions are opt-in and off by default, so keystrokes never leave the Mac unless you turn them on.
- Energy-aware tabs β background WebKit views can be hibernated while tab state, history, and scroll position remain restorable.
- Rust sovereign core β the Category-A egress allowlist and the 79-byte
MEM8 wave frame live in
core/(zero-dependency Rust) behind a small C ABI; Swift keeps the WebKit shell, the decisions live in Rust. - MemoryWave β container-scoped encrypted memory storage with opt-in,
AI-agnostic inference providers. Stored memory bodies never reach a
remote provider:
WaveDirectorcomposes recalled memories into the prompt only when the provider is on-device (WaveDirector.swift:415-421), andMemoryWavePolicy.decidedenies a remote request that declares it carries them (.deny(.cognitiveEgress),MemoryWavePolicy.swift:81-82) β a declared-intent gate plus a caller-side guard, not a filter on the outgoing prompt. The caller-side guard is the one doing the work today; the policy is a tripwire behind it, dormant whileaskdeclares stored memory only for the on-device provider (WaveDirector.swift:364). Recalled rows a model wrote or an automatic page capture produced are carried under a separate untrusted-data heading rather than beside the pins you wrote yourself (WaveDirector.recalledMemoryBlock,:49-70). One carve-out: a timeline summary with a remote provider sends the title, time, and host of every record in the window, and no snippets (WaveDirector.swift:199-200,MemoryTimeline.swift:63-80). Pages you explicitly summarise or ask about are sent to the provider you configured; see docs/NETWORK.md. - Summarize β on-device page summarization via Apple's FoundationModels. No streaming, no network, no model output that can act on the browser.
- AutoFill β passwords and passkeys through an
ASCredentialProviderextension and WebAuthn, kept in the OS / iCloud keychain and deliberately separate from the post-quantum /DeviceKeyManagerstack. - Spaces β the container universes surface as first-class Spaces with a vertical tab sidebar; a system Focus filter can switch the active Space and tighten shields.
- The iPhone lane runs the same sovereign core β
QwaveIOSlinks the same Rust staticlib (mem8 waves, Phoenix, egress, telemetry) through the sharedSovereignCoremodule, with a battery policy that reacts to Low Power Mode and thermal pressure, and native iOS UX (pull-to-refresh, keyboard shortcuts, share sheet, haptics). - Two search engines, both honest β Ecosia (the default) and DuckDuckGo: the only engines with a vetted keyless autocomplete endpoint. Qwave will not ship an engine whose suggestions it cannot fetch without a key.
Each profile owns a persistent WKWebsiteDataStore; burner tabs get a
non-persistent store and leave nothing behind. See
ContainerRegistry,
WebViewFactory.
βββββββββββββββββββββββββββββββββββββββββββββββββ
β Qwave.app β
β β
profile A ββββΆβ WKWebsiteDataStore(.persistent) cookies, β
β ββ tabs A1β¦An localStorage, β
β IndexedDB, β
profile B ββββΆβ WKWebsiteDataStore(.persistent) service β
β ββ tabs B1β¦Bm workers β
β βββββββββββββ β
burner tab βββΆβ WKWebsiteDataStore(.nonPersistent) β
β ββ closed β gone, never restored β
βββββββββββββββββββββββββββββββββββββββββββββββββ
The blocklist ships as a committed snapshot (zero launch egress), compiles to
WKContentRuleList, and navigation waits for it. See
ShieldsDirector,
UBORuleListCompiler,
docs/BLOCKLIST.md.
blocklist.json ββ(committed snapshot, scripts/update-blocklist.sh)βββ
β β
βΌ β
UBOFilterParser βββΆ UBORuleListCompiler βββΆ WKContentRuleList β
β β β
βΌ βΌ β
per-site toggles βββ ShieldsDirector βββ applyListsThen(to:) β
β β β
βΌ βΌ β
HTTPSFirstUpgrader NavigationCoordinator gates the β
first navigation on shields β
.whenReady() β never bypassed ββββ
A pure conditions β tier β policy mapping; memory pressure demotes the tier
before anything is decided. See
EnergyGovernor,
TabHibernator,
docs/ENERGY.md, docs/PERF.md.
thermal .nominal ββ
lowPower off ββββββΌβββΆ tier .normal βββΆ tabs alive, AI allowed
occlusion false βββ
underMemoryPressure βββΆ tier .conserve / .critical
β
βΌ
hibernate background tabs,
pause/suspend media,
quietly defer AI inference
Explicit command only: extract, generate, render inert text. See
SummarizeSession,
docs/SUMMARIZE.md,
readability-probe,
foundation-models-probe.
β₯βS / toolbar βββΆ available? macOS 26 + Apple Silicon + Apple Intelligence
β β
β ββ modelNotReady βββΆ re-check on foreground (self-heals)
β ββ otherwise ββββββΆ vanish cleanly, no grey-out, no nag
βΌ
energy tier == .normal? ββ no βββΆ quietly absent this moment
β yes
βΌ
extractor.js (F1 1.00 Γ4, 0.96 docs) βββΆ LanguageModelSession.respond
β respond-only: the stream path
β refuses this workload on 26.4
β retry β€ 3 on nondeterministic
βΌ refusals; neutral failure text
inert selectable text panel βββ nothing downstream of the model acts;
zero network egress
The sovereign decisions β which hosts Qwave's own code may contact, and
whether a MEM8 wave frame is intact β live in core/, a zero-dependency Rust
crate. It compiles to a staticlib per Apple platform (core/build-apple.sh
maps Xcode's ARCHS onto cargo targets and lipos the slices), is force-loaded
into both app targets so every decision surface is present in every binary,
and is spoken through a small C ABI (core/include/qwave_core.h) from the
shared SovereignCore module in QwaveKit (Packages/QwaveKit/Sources/SovereignCore).
The egress decision the omnibox makes before sending a suggestion query is
the Rust core's, not Swift's β on the iPhone exactly as on the Mac.
core/ (Rust, zero deps)
βββ egress.rs Category-A allowlist β permits(host), subdomain-aware
βββ wave.rs the 79-byte WaveInt frame: encode, validate, grid coord
βββ phoenix.rs the Phoenix protocol β marine gate, custodian, verdict
βββ mem16.rs governing/recovery sequences (mem|16-10 under `nightly`)
βββ telemetry.rs the PII scrubber + histogram aggregator (qwave-agg)
βββ rational.rs the MEM8 rational, reduced + checked arithmetic
The WireGuard/VPN layer (PacketTunnel, WireGuardKit + Go bridge, Zig packet filter, VPNKit, PostQuantum) was removed β a tunnel is a different layer, not the browser's requirement. If it returns, it returns as a separate package.
Container-scoped, encrypted memory substrate with an explicit provider seam β
local by default, remote only when configured. See
MemoryProvider,
MemoryStore.
page text ββΆ ArticleExtractor ββΆ MEM8 store (container-scoped, encrypted)
β² β
βββββββββ MemoryProviding ββββββββββββ
default: OnDeviceProvider (local-first; remote only when
configured explicitly)
Qwave's own egress is held to a committed allowlist, and β since
#77 β EgressGuard, a
URLProtocol wired into every fixed-host client, checks it at runtime instead
of only in review. The shields launch path is separately asserted
request-free by a test whose reach is bounded (caveats below). See
EgressAllowlist,
EgressGuard,
docs/NETWORK.md.
any wired Qwave network client
β
βΌ
EgressGuard (URLProtocol) ββ checks EgressAllowlist.permits(host:)
β allowed β steps aside, request proceeds
β blocked β fails the request, logs, records
βΌ
allowlisted hosts (3): github.com Sparkle appcast
api.x.ai default remote AI endpoint
duckduckgo.com omnibox suggestions (opt-in)
β
β + on a request Memory Wave marked, and only that request:
β the endpoint host you configured, read live at check time.
β Not on the allowlist; no other client can reach it.
βΌ
launch path ββ EgressGuardTests URLProtocol recorder βββΆ zero requests
(no launch-time blocklist fetch since 0.4.4)
Two honest caveats remain. EgressGuard only reaches a client that installs
it: URLSession.shared gets it for free from the process-wide
URLProtocol.registerClass in main.swift, but any session Qwave
constructs β including one built from URLSessionConfiguration.default β has
to call EgressGuard.install(into:) explicitly
(QwaveSupport/EgressGuard.swift:12-24, pinned by
EgressGuardTests.swift:614-637). URLSession.mullvadPinned(),
DuckDuckGoSuggestionProvider and Memory Wave's remote provider do; a new
fixed-host client that skips this would not be caught here, only by review.
Memory Wave's provider was itself the standing example of that gap: its
session is ephemeral, it never called install(into:), and so api.x.ai sat
on the allowlist without one provider request ever being checked against it.
It is gated now, against the committed list plus β on its own request, and
nobody else's β an exact match on whatever HTTPS endpoint you configured
(EgressGuard.markUserConfiguredEndpoint(_:), stamped at
MemoryWave/MemoryProvider.swift:108). The mark names no host, so it grants
nothing by itself: the guard resolves it against
MemoryWavePreferences.egressPermittedHost read live at check time
(EgressGuard.swift:236). Nothing is cached, so changing the endpoint or
switching the provider off revokes the previous host immediately, with no
inference needed in between, and no other client in the process ever gains it.
Two Category-A clients are still not gated, and for two different mechanical
reasons β conflating them once broke a feature. FaviconLoader builds its own
session and never installs the guard, so the guard never sees it. The
remote-markdown fetch is the opposite: it runs on URLSession.shared, which
the global registration does reach, and is exempted by an explicit
EgressGuard.markPageDriven(_:) marker at its single call site
(BrowserCore/NavigationCoordinator.swift:363) β per request, not per host or
per session. The shields launch-path assertion is the one thing here asserting
the absence of a request rather than the shape of one: it registers a
URLProtocol recorder over the shields launch path and asserts nothing was
requested; the test builds its own ShieldsDirector rather than running the
app's launch sequence, and WebKit's own network process (Category C) is
invisible to any URLProtocol-based check, EgressGuard included.
Qwave.app AppKit shell + SwiftUI settings
βββ QwaveKit local Swift package, Swift 6 language mode
β βββ BrowserCore tabs, containers, navigation, hibernation
β βββ Shields content rules and HTTPS-First
β βββ Persistence actor-isolated SQLite stores
β βββ MemoryWave encrypted MEM8 memory substrate
β βββ Summarize on-device page summarization (macOS 26+)
β βββ WebExtensions MV3 registry and browser.* bridge
β βββ URLIdentity WHATWG/WebKit-compatible host identity
β βββ FeatureFlags guarded WebKit SPI feature access
β βββ WebCredentials keychain-only passwords + passkeys (AutoFill)
β βββ SovereignCore the Swift face over the core's C ABI
β βββ QwaveSupport logging, keychain, egress guard
βββ core/ the Rust sovereign core (staticlib, C ABI)
βββ egress.rs the Category-A allowlist
βββ wave.rs the 79-byte MEM8 WaveInt frame
βββ phoenix.rs the Phoenix protocol (marine gateβ¦verdict)
βββ telemetry.rs the privacy scrubber + histogram aggregator
βββ build-apple.sh ARCHS β cargo targets β lipo (both lanes)
QwaveIOS.app SwiftUI shell, iOS 15+ (iPhone 13 minimum)
βββ the same QwaveKit + core one SovereignCore, one decision surface
βββ battery policy Low Power / thermal β conserve / critical
βββ native UX refresh, shortcuts, share sheet, haptics
Dependency direction is one-way (app β QwaveKit; feature modules never reach into the AppKit shell) β see docs/ARCHITECTURE.md for the exact graph, isolation rules, data flow, and test boundaries.
- BrowserCore β the convergence point: tabs, containers, navigation,
hibernation. Key types:
TabManager,ContainerRegistry,TabHibernator,EnergyGovernor,NavigationCoordinator,SessionRestorer,OmniboxSuggester. source - Shields β uBO β content-rule compilation, per-site toggles, HTTPS-First.
Key types:
ShieldsDirector,UBORuleListCompiler,HTTPSFirstUpgrader. source Β· docs/BLOCKLIST.md - Persistence β actor-isolated SQLite stores. source
- MemoryWave β encrypted memory substrate. Key types:
MemoryStore,MemoryProvider(MemoryProviding),ArticleExtractor,WaveInt. source - Summarize β respond-only FoundationModels wrapper. Key types:
SummarizeSession,SummarizePolicy,ArticleExtractor(byte-identical probe script). source Β· docs/SUMMARIZE.md - WebExtensions β MV3 registry and
browser.*bridge. Content scripts are not active in the shipping app:BrowserWindowController.ensureWebViewinstalls the bridge and nothing else, so a manifest'scontent_scriptsentry is never injected into a tab andContentScriptEngineis reached only from tests (WebExtensions/WebExtensionHost.swift:8-20). source - WebCredentials β keychain-only website logins and passkeys for AutoFill.
Foundation + Security only; never links the crypto/VPN stack. Key types:
WebCredential,WebCredentialStore. source - URLIdentity β WHATWG/WebKit-compatible host identity (fixes a shielding
bypass, per
research/collections-foundation/weburl.md). source - FeatureFlags β guarded
_WKFeatureaccess with the unavailable / emptySurface / available tri-state. source - QwaveSupport β
QwaveLog(privacy-classified), keychain, egress allowlist. source
brew tap 8b-is/tap
# stable release (signed, notarised β Gatekeeper accepts it directly):
brew install --cask 8b-is/tap/qwave
brew upgrade --cask 8b-is/tap/qwave # update to the next tagged release
# bleeding edge (nightly: every experimental WebKit feature ON, mem|16-10 linked):
brew install --cask 8b-is/tap/qwave-nightly
brew upgrade --cask --greedy 8b-is/tap/qwave-nightly # re-pull the rolling buildThe casks install the ready DMGs from the GitHub Releases β no build on
your machine (the build-from-source formulae were retired: xcodebuild's
sandboxed package resolution cannot nest inside Homebrew's build sandbox).
The app lands in /Applications:
open -a Qwave- Stable β signed, notarised, stapled DMG + Sparkle appcast: https://github.com/8b-is/qwave/releases/latest β Gatekeeper accepts it directly, and in-app updates ride the Sparkle feed.
- Nightly β rolling unsigned prerelease (zip + DMG): https://github.com/8b-is/qwave/releases/tag/nightly β bleeding edge, unsigned by design; the signed lane is stable's.
No Homebrew? The installer script does the same thing:
git clone https://github.com/8b-is/qwave.git && cd qwave
tools/install-nightly.sh # build bleeding edge, swap into /Applications
# update to the latest main and stay bleeding edge:
git pull && tools/install-nightly.shRequirements:
- macOS 14 or newer (desktop lane)
- iPhone with iOS 15 or newer (iPhone 13 minimum; phone lane)
- Xcode 16+ (the package uses Swift 6 language mode)
- XcodeGen
- Rust (rustup) β the sovereign core builds a staticlib at compile time.
Universal Release builds (
-destination 'platform=macOS') need both Apple std targets:rustup target add aarch64-apple-darwin x86_64-apple-darwin.
Package tests:
swift test --package-path Packages/QwaveKit -c releaseGenerate and build the app:
xcodegen generate --spec project.yml
xcodebuild \
-project Qwave.xcodeproj \
-scheme Qwave \
-configuration Release \
-destination 'platform=macOS' \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGN_IDENTITY= \
build
# the iPhone lane (iOS 15 floor, iPhone 13 minimum):
xcodebuild \
-project Qwave.xcodeproj \
-scheme QwaveIOS \
-configuration Release \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGN_IDENTITY= \
buildThe nightly channel flips all experimental WebKit features ON and links the mem|16-10 sovereign library; stable keeps WebKit's defaults and Qwave's MIT posture.
osascript -e 'tell application "Qwave" to quit'
rm -rf /Applications/Qwave.app
# your profile, memory, and history (delete only if you want a clean slate):
rm -rf ~/Library/Application\ Support/Qwave
rm -rf ~/Library/Caches/is.8b.qwave
defaults delete is.8b.qwave 2>/dev/null || trueQwave has no system extension in the current tree (the VPN layer was removed); nothing else lingers.
project.yml is the source of truth. Never hand-edit the generated
Qwave.xcodeproj. Distribution signing follows
docs/SIGNING.md; toolchain pinning lives in
docs/PINNING.md. The iPhone lane ships the same WebKit +
shields core in a SwiftUI shell (Sources/QwaveIOS); the VPN layer was
removed β a tunnel is a different layer, not the browser's requirement.
- QwaveKit targets use Swift 6 language mode and complete strict concurrency.
- Persistence, MemoryWave, and service state use actors or explicit main-actor
ownership; cross-boundary values are
Sendablevalue types. - Async WebKit and persistence APIs stay async at their boundary; do not add blocking queues or semaphore bridges to silence compiler diagnostics.
- New network clients must update the committed egress allowlist and tests.
Packages/QwaveKit/ Swift package and headless tests
Sources/QwaveApp/ AppKit shell and SwiftUI panes
Sources/QwaveIOS/ the iPhone lane (SwiftUI shell, iOS 15+)
Sources/CredentialProvider/ the AutoFill app extension
core/ Rust sovereign core (staticlib + C ABI header)
Resources/ plists, entitlements, bundled rules
docs/ architecture, network, signing, design site
research/ evaluated platform/package notes + probes
tools/ install-nightly.sh, telemetry-export
project.yml XcodeGen source of truth
project.yml is the single source of truth for the version. Every target
(Qwave, CredentialProvider, QwaveIOS) declares the same two numbers:
CFBundleShortVersionStringβ the semver, currently2.0.4.CFBundleVersionβmajor*10000 + minor*100 + patch(2.0.0 β20000). Sparkle compares this number, and the release workflow fails the tag if any target disagrees.
The v2.0.0 major covers the Rust-core rewrite: VPN layer removal, the
sovereign core (core/), the stable/nightly channel split, and the removal
of Go/Zig from the build. v2.0.4 is the current release (2026-10-03):
signed, notarised, stapled, with the Sparkle appcast attached β existing
installs update in place.
Releases: git tag v2.0.0 && git push origin v2.0.0 runs
.github/workflows/release.yml (signed + notarised DMG and the Sparkle
appcast when the secrets are configured; unsigned zip otherwise). Nightly
ships as a rolling prerelease via scripts/release-nightly.sh. The full
procedure lives in docs/RELEASING.md.
Product and engineering specs:
- Architecture β module graph, isolation, data flow
- Security policy and threat model
- What Qwave sends β the network inventory + egress allowlist
- VPN Stage B
- Summarize design β on-device AI contract
- Blocklist pipeline
- Energy measurement Β· Performance
- Crypto review
- Signing Β· Releasing
- Toolchain pinning Β· Xcode Cloud
- GRDB evaluation Β· Roadmap audit
- Contributing
- Project site β design gallery
Research (measured, version-stamped, Qwave-specific):
- research/README.md β 41 package notes, 12 categories
- Platform baseline Β· Digest
- Bleeding-edge 2026
- WebGPU surface probe (default-on in WKWebView;
WebGPUEnabledinert): 01-webkit-browser-engine/webgpu-surface - Three-way wave benchmark (CPU / Metal / WebGPU): 03-gpu-metal-compute/wave-fbm-benchmark
- Lock &
~Copyablebenchmark: 04-concurrency-runtime/arc-lock-benchmark - Readability extraction probe (F1 measured): 02-on-device-ai/readability-probe
- FoundationModels probe (availability, latency, provider seam): 02-on-device-ai/foundation-models-probe
Shipped through the v1.0.0 line: the browser core, shields, WebExtensions MV3 bridge, MemoryWave, Summarize (macOS 26+ on Apple Silicon with Apple Intelligence), the signed release workflow, and the Swift 6 concurrency migration β all covered by package tests.
v2.0.0 is the Rust-core line: the VPN layer (PacketTunnel, WireGuardKit,
Go+Zig, VPNKit, PostQuantum) was removed, the sovereign decisions
(egress, MEM8 waves, Phoenix, telemetry scrubbing) moved into core/, the
stable/nightly channel split became real (nightly actually links the
mem|16-10 library and flips the WebKit experimental features ON), and the
WebAuthn rpId check now consults a real public-suffix list.
Also in flight: downloads UI, crash-safe session restore, a
qwave://diagnostics telemetry page, VoiceOver accessibility on the chrome,
on-device semantic memory recall, a command palette, first-run bookmark
import with Spotlight entities, container-bound Focus filters with a Spaces
sidebar, keychain-only AutoFill (passwords + passkeys), a zero-egress Safe
Browsing host-set (shipped as a sample list β see
docs/SAFE-BROWSING.md for sourcing a real feed),
and the iPhone lane β now a full lane: the same WebKit + shields + Rust core
(mem8/Phoenix/egress/telemetry), a Low Power / thermal battery policy, and
native iOS UX, at the iOS 15 floor on an iPhone 13.
Qwave is released under the MIT License. Copyright Β© 2026 8b.is / Peter Lodri.
- Privacy policy β what is stored, what leaves the device, and when
- Third-party notices β licenses for the vendored PSL data, Sparkle, mem|16-10 (nightly only, AGPL-3.0), and the SwiftPM dependencies



