End-to-end-encrypted, passkey-unlocked SQLite that lives in your browser. It syncs between your devices through a relay that only ever sees ciphertext - no server, relay, or app ever holds a key.
Cloud storage is a leasehold: you occupy, someone else holds title. A freehold is property you own outright - no landlord, no lease, no one else's key.
Why Freehold
Every guarantee comes from a boundary the design can actually enforce, and every limit is stated plainly. No marketing crypto.
A random 256-bit data key encrypts every database block. It's unwrapped inside a Web Worker from your passkey's WebAuthn-PRF output, lives only in wasm memory, and is zeroized on lock - never persisted in the clear, never off the device.
Export, import, and sync move only sealed ciphertext plus non-secret lineage metadata. A relay can't read your data, link across your databases, or forge your state.
Only audited RustCrypto/dalek and WebCrypto primitives, used as-is: XChaCha20-Poly1305, HKDF, HMAC, Argon2id, Ed25519. Every security decision is designed before it's built.
A full SQLite engine on an encrypting VFS: every file it touches - main DB, journal, temp - is sealed per-block. Three adversarial review passes; crash-injection clean.
No cross-origin-isolation headers required. Pure ESM SDK with hand-written types - what you import is what runs. No build step for the JavaScript.
The design says exactly what it does not guarantee - availability, freshness against a fully-malicious provider, un-leaking already-exfiltrated data. Nothing hand-waved.
Architecture
From the browser's own storage up to your app - every layer is specified, reviewed, and then built.
How it works
Unlock asserts the passkey once and opens a session; the key is unwrapped in the worker and never leaves wasm memory. Every query after that is prompt-free.
import { FreeholdVault } from '@freehold/db'; const vault = await FreeholdVault.open({ wasmUrl: new URL('./pkg/freehold.js', import.meta.url), rpName: 'My App', }); await vault.enroll(); // register passkey, init vault const code = await vault.addRecoveryCode(); // show once await vault.unlock(); // one passkey ceremony… await vault.sql('CREATE TABLE notes(id INTEGER, body TEXT)'); await vault.sql('INSERT INTO notes(body) VALUES (?)', ['hello 🔐']); // …then no more prompts until lock() const bytes = await vault.exportBundle(); // .freehold - no key inside
.freehold bundle on device A, import it on
device B, and unlock with your synced passkey or your recovery code. The bundle carries the
encrypted image and the key envelope - but no key. No server is ever in the loop.
Prefer to see it run? The passkey demo walks through enroll → unlock → export/import; the custody demo shows apps requesting scoped disclosures with revocation enforced at the broker.
The system
Freehold is built and shipped in the open, design-doc first. This is an honest snapshot, not a wish list.
| Plane | What it does | Status |
|---|---|---|
| Freehold DB | Encrypting VFS, key envelope, DEK rotation, server-blind bundle, standalone decryptor. | Working · 3 review passes |
| Freehold Sync | Version-vector engine, blind relay + HTTP adapter, stateless relay auth. | Working · proven E2E |
| Data custody | Signed grants, requester auth, zero-PII attestations - apps as custodians. | Reference + E2E |
| Device trust | Per-device identity + vault-signed certs; pairing / revocation / federation designed. | Increment 1 built |
Security posture
Freehold guarantees confidentiality from the sync path unconditionally - no server, relay, or peer ever sees a key or plaintext. It does not guarantee availability or freshness against a fully-dishonest provider: the anti-rollback epochs make staleness detectable and non-propagating, not impossible. Run a relay you control, or sync device-to-device, for a liveness guarantee.
Enroll a passkey, write some encrypted data, export a key-free bundle - all in your browser, nothing sent anywhere.