Every other encrypted database has an operator — the party you
subpoena, breach, or serve a warrant to. This one doesn't. Your keys
never leave your machine, the nodes hold pseudorandom pairs they cannot
link or decrypt, and you can still run BETWEEN and
LIKE 'ac%' over the result.
Two commands, from behind any home router, no port forwarding. You store bytes you cannot read and never become liable for. Payouts follow cryptographically proved possession — never a number a node reports about itself.
The honest qualifying question first: if you can't name a party you'd rather not have to trust, use Postgres. It is faster, it sorts, and it has thirty years on us. blindrange is for when you can name that party — a cloud provider, a hosting company, a jurisdiction, a future owner of your vendor, or your own ops team.
| What you get | Why it's different |
|---|---|
| Nothing to subpoena | There is no operator holding your plaintext, because there is no operator. Not us, not the node hosts, not all of them colluding. |
| No breach surface | A full snapshot of every disk in the network is unlinkable pseudorandom pairs. There is no database to leak, so there is no breach to disclose. |
| Still queryable | Range filters, prefix matches and AND across fields — the thing client-side-encrypted blob storage cannot do. |
| A privacy budget you set | leaf_width
is a dial. Whatever you choose, no observer ever resolves finer than a
leaf — not after watching every query you ever run. |
Order-preserving encryption — the classic way to run BETWEEN
on ciphertext — was broken by a decade of inference attacks, because
revealing order reveals nearly everything. blindrange gives up
server-side sorting and keeps only range filters, which is enough to
change the game: a range collapses into a handful of dyadic intervals, each
interval becomes a pseudorandom key via a PRF only the data owner can
compute, and the network answers exact-match lookups on opaque keys.
Nodes never see a comparison, an ordering, or a byte of plaintext.
AES-256-GCM covers the payloads; the owner decrypts and post-filters at
home.
# WHERE amount BETWEEN 250 AND 500 — on data the nodes can't read owner.query("amount", 25000, 50000) owner.query_prefix("name", "ac") # LIKE 'ac%' owner.query_multi([...]) # AND across fields owner.delete(rid); owner.compact() # real forgetting
I:39d8286d9d20f107ce3e03e74045ac44 → utqJUnFcSPs=
I:98a16f60dd371e5eff15b038b7acb55f → ZG32xdJmFUU=
R:b59b23f85755164b → eDF7iT+qCl4K2/LuQDdJv6…
no keys · no order · no equality · no co-occurrence · forward private
Because the owner walks the index (nodes never receive a label key), forward privacy comes from plain HMAC counter chains — no public-key crypto in the hot path.
You are charged for what the network actually holds, not for the size of your JSON — and those differ by anywhere from 5× to 40×, because a record is one ciphertext blob plus one index entry per dyadic level per indexed field, all of it replicated three times. Small records indexed finely are the expensive end; large records indexed coarsely are the cheap one. Hiding a spread that wide behind a per-gigabyte-of-your-data price would mean guessing on your behalf, so instead the formula is published and exact:
keys per record = 3 × (1 + Σ levels) levels = log2(domain ÷ leaf_width) bytes per record = 3 × (blob + 46 × Σ levels) 46 B per index entry, measured
The calculator below is that formula, and it is accurate to 0.00% against a real three-node network — not an estimate. Note what it does to the incentives: coarser precision is both cheaper and more private. Privacy is the cheap option here; resolution is the thing that costs money.
Where that lands, per GB of your data: about $0.02 for 1 kB documents indexed coarsely (roughly S3), $0.08 for hourly-resolution logs, $0.17 if you index those logs to the minute. Object-storage prices at one end, database prices at the other, and your schema is what decides which — because fine resolution genuinely costs us more to hold.
| Plan | Network storage | Price |
|---|---|---|
| Free | first 10 GB | $0 — no card |
| Beyond that | per GB-month | $0.005 |
| Queries | unmetered | included |
How metering works, and exactly what it costs you in privacy: quotas are enforced with blind-signed write tokens. Issuance is identified — you sign up, and we know what you were granted. Redemption is anonymous: your client blinds every token before we sign it, so the signatures we produce are over values we have never seen, and a token arriving at a node cannot be matched back to your account by us, by the node, or by the two of us together. What we do still learn is how much capacity you drew and when. Volume and timing — never linkage.
# a free-tier key, no card curl -X POST https://tokens.blindrange.dev/signup \ -H 'Content-Type: application/json' -d '{"email":"[email protected]"}' # point a database at it; the client buys and spends tokens for you owner.configure_tokens("https://tokens.blindrange.dev", "<your-key>")
Two caveats worth stating rather than burying. There is no email verification, so nothing prevents someone minting accounts with invented addresses — signup is rate-limited per IP and that is the whole defence. And your key is the only thing identifying your account: keep it, because it cannot be recovered, and anyone holding it can spend your quota.
Most things you could store here involve a trade — you give up sorting, or joins, or full-text, and get privacy back. An audit trail is the case where there is no trade: append-only is what the index already is, tamper-evidence falls out of per-record AEAD, and the queries audit logs actually use are the three this index is fastest at. For most data "the vendor can read it" is tolerated; for an audit trail it is a liability.
Build on it → your application talks to a local database at local speed; blindrange replicates it — encrypted, continuously — to machines that cannot read it. Familiar verbs over storage that cannot read any of it. Shaped like SQL, deliberately not SQL: everything the engine cannot do is refused with the reason, and every sample is quoted from a script the test suite runs. The Python guide is the same ground with the full API — and the web guide ships an app as one static file, no backend at all.
blindrange-audit → ships from Vector, Fluent Bit or the OpenTelemetry Collector with configuration alone, has no delete route by design, and shows every event twice — as you see it, and as a node operator sees it.
There is also an S3-compatible gateway, so backup tools, compliance archivers, Loki and rclone can write here without being ported, and an off-site backup demo built on it whose catalog stays searchable. All of it, plus the attack harness we run against our own design, is on the demos page →
InvoiceFlow is the first production application to run on this network with no other database behind it — invoices sealed on machines that cannot read them, reads at local speed, and a cutover proven by a drill that killed the network for 40 seconds in production without a user noticing. The full story, with the numbers and what building it taught the engine, is on the Built on blindrange page →
The same product exists a third time with no server at all: one HTML file, keys derived in your browser, and the application itself stored in the database it runs on. It is still multi-user — an invite shares the ledger, and after the first warmup its pages answer in 2–9ms from an encrypted local mirror. Open it and use it on the Solo apps page →
Two commands. It works from behind a home router with no port forwarding and no configuration: if your node isn't reachable it diagnoses that itself and becomes a relay tenant of a peer, then clients punch a direct QUIC path to you when they can.
python3 -m venv .venv
.venv/bin/pip install --upgrade pip # see below
.venv/bin/pip install -e ".[quic]"
.venv/bin/blindrange-node --port 7501 --data ~/.blindrange/n1 \
--seed seed.blindrange.dev:7501 --secret blindrange-public \
--max-disk 20GB --auto-updatepy -m venv .venv
.venv\Scripts\python -m pip install --upgrade pip
.venv\Scripts\python -m pip install -e ".[quic]"
.venv\Scripts\python -m blindrange.node --port 7501 ^
--data $env:USERPROFILE\.blindrange\n1 ^
--seed seed.blindrange.dev:7501 --secret blindrange-public ^
--max-disk 20GB --auto-updatePython 3.10 or newer. Upgrade pip inside the venv
before installing: a new venv seeds whatever pip the system Python bundles,
and anything before pip 21 predates the packaging standard this project uses
— it reports Directory '.' is not installable. File 'setup.py' not
found, which is true and completely unhelpful. If that still fails
with a message about the Python version, the interpreter is too old and
needs replacing rather than persuading.
Old Linux (Python older than 3.10)? Don't fight the distro — drop in a prebuilt modern Python with uv (works on anything with glibc 2.17+, i.e. CentOS 7 / Ubuntu 18.04 and newer):
curl -LsSf https://astral.sh/uv/install.sh | sh source $HOME/.local/bin/env uv venv --python 3.12 .venv uv pip install --python .venv/bin/python -e ".[quic]"
Server joined as a relay tenant? That is the
reachability self-check being honest with you: something is stopping
inbound 7501. On a datacenter machine that is usually the distro
firewall — open TCP and UDP 7501 (firewalld: firewall-cmd
--add-port=7501/tcp --add-port=7501/udp --permanent &&
firewall-cmd --reload; ufw: ufw allow 7501) and the
node returns to direct mode by itself within a minute — it re-checks
continuously. (Nodes older than today's build also bound to loopback by
default, which made tenancy certain on any server; the default is now
all interfaces, and the node's log states which mode it is in and
why.)
--max-disk takes 20GB or a
percentage like 5% of the drive. Past it the node stops
accepting writes; data it already holds is never deleted to get under
a limit you lower, since that would destroy replicas — it just stops taking
more and shrinks as the network moves data elsewhere. --auto-update
is optional and means the node runs whatever we publish next; leave it off
if you would rather pull by hand. If aioquic has no wheel for
your Python, drop [quic] — you lose direct peer paths, not the
node.
That "secret" is published deliberately: on an open network it only turns away drive-by scanners and carries no security role — confidentiality comes from keys that never leave the owner's machine.
You hold pseudorandom keys and AEAD blobs. You cannot tell what you store, whose it is, or how many customers you serve — which is the point, and also your protection.
An always-on machine with a disk that isn't full. No GPU, no stake, no token, no chain.
Nodes re-push their keys to each key's current replica set, so data migrates to you after you join and survives you leaving. Nothing to administer.
70% of storage revenue goes to node hosts; we keep 30% for the
seed, the aggregator and the work. Your share is structural ×
proved — your position on the hash ring, multiplied by possession you
have actually proved. Shares are shown live, in per-mille, on the
network status page.
The measurement deliberately assumes nodes cheat, so nothing a node says about itself can raise its earnings:
Everything else here removes a party you would have to trust. The payout measurement did not: we score the audits and compute the shares, and you had no way to tell whether a number was revised afterwards. That is a strange gap in a system whose whole argument is that you shouldn't have to take anyone's word.
Every accepted audit report and every share calculation now goes into an append-only Merkle log. Reports go in whole — they carry no owner or database identifier by design — so you can re-run the scoring over the logged inputs and check our arithmetic, not merely that we didn't quietly edit the answer later.
# remembers the head it saw; next run demands proof the log still contains it python3 examples/verify_log.py head size 3 root 4cd723911d1ce43e… signature valid consistent with the 2 entries seen previously ✓ (1 new)
Tested against the live log by actually cheating: rewriting a past share from 333 to 900, and separately deleting an entry. An operator holding yesterday's head rejects both. What this cannot do is stop us declining to log something in the first place, and it cannot by itself catch a split view — showing you one log and someone else another. That takes two operators comparing heads, which is why the verifier prints the head in a form you can paste to somebody.
And the part most networks won't print: the pool is whatever the network actually earns. Today that is $0 — there are no paying customers yet. We will not pay hosts out of revenue that hasn't come in, and we won't quote you a per-terabyte rate that implies otherwise. Run a node now because you want this to exist; the economics start when the customers do.
One more, unsolved and worth knowing before you join. Identities are cheap — anyone can mint them — but the cost of that is durability, not earnings. Payouts follow proved possession, so a minted node only earns by genuinely storing what it is sent, which makes it a large operator rather than a thief. What cheap identities really buy is replica capture: at three replicas, a party holding half the ring holds every copy of roughly an eighth of the data, and can delete or ransom it. Placement now spreads a key's replicas across distinct failure groups instead of trusting the hash — measured on nine nodes with one party running six, fully-captured keys fell from 26.8% to 0.4%. A /24 is a coarse proxy for "the same place" and it is wrong at the edges in both directions, so treat this as raising the cost of capture rather than removing it.
Every queryable-encryption scheme leaks something. Most products bury that. blindrange ships an attack harness that runs the real attacks from the literature against its own design and prints the numbers:
| What | Result, measured |
|---|---|
| Payload confidentiality | Holds unconditionally. A fully-colluding network holds keys it cannot link and blobs it cannot decrypt. |
| At rest | Zero structure — the snapshot equality attack that recovers value-groups from naive schemes finds nothing. |
| Query watcher | An adversary logging every query
can reconstruct a column's values over time —
bounded structurally by each field's leaf_width: no
observer ever resolves finer than the leaf, no matter how long it
watches. |
| Who it's for | Not healthcare, not high-stakes PII. Ordinary business data on infrastructure you don't fully trust — with the trade-offs in front of you, not in a footnote. |