blindrange

Range queries over encrypted data, served by machines that cannot read it. A decentralized database with no central infrastructure, no trusted component, and — unusually — every leak measured and published.
If you have data

Store it where no one can be made to hand it over

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.

If you have a spare disk

Run a node and earn a share of what the network earns

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.

writes / sec
deletes / sec
reads / sec
keys stored
live nodes
connecting to the public network…
Build on it — SQL Build on it — Node Build on it — Python GitHub Try it in your browser Solo apps Demos Audit log S3 gateway Live network Threat model

Why store here

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 getWhy it's different
Nothing to subpoenaThere is no operator holding your plaintext, because there is no operator. Not us, not the node hosts, not all of them colluding.
No breach surfaceA 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 queryableRange filters, prefix matches and AND across fields — the thing client-side-encrypted blob storage cannot do.
A privacy budget you setleaf_width is a dial. Whatever you choose, no observer ever resolves finer than a leaf — not after watching every query you ever run.

The trick

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

What a node operator sees

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.

Pricing

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.

rows you expect to store
bytes of your plaintext JSON
unindexed fields are free — they ride inside the blob
Keys per record (× 3 replicas)
Keys on the network
Your data
Network storage (what you pay for)
Amplification
Per month

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.

PlanNetwork storagePrice
Freefirst 10 GB$0 — no card
Beyond thatper GB-month$0.005
Queriesunmeteredincluded

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.

A worked example: audit logs

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 →

Built on blindrange

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 →

Run a node

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-update
py -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-update

Python 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 can't read it, so you can't be liable for it

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.

Spare capacity, not a rig

An always-on machine with a disk that isn't full. No GPU, no stake, no token, no chain.

It heals itself

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.

How earning works

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:

Checking us

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.

Honesty as the design principle

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:

WhatResult, measured
Payload confidentialityHolds unconditionally. A fully-colluding network holds keys it cannot link and blobs it cannot decrypt.
At restZero structure — the snapshot equality attack that recovers value-groups from naive schemes finds nothing.
Query watcherAn 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 forNot 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.
MIT-licensed research prototype. Built on structured encryption (Kamara–Moataz, MongoDB QE lineage), Sophos-style forward privacy, and the attack literature it takes seriously (Naveed–Kamara–Wright, Grubbs et al., Kellaris et al.). github.com/alviso/blindrange