Privacy policy

The list, and the panel in the app that computes it.

Revision: Pre-Audit Candidate, 12 October 2026

Version 1.1, dated 12 October 2026 (Pre-Audit Candidate), replacing version 1 of 21 August 2026; it brings the page into line with the service as built, and every change is recorded in the project's change history. This describes hosted Haven — the managed subscription sold on this site. An instance you run yourself is yours, and this document does not describe it.

The substance of this policy is not written down here. It is computed. The app has a panel — Settings → Your data → "What this server holds about you" — that walks the live database schema at the moment you open it and enumerates every table and every column, with a verdict on each: readable by us, sealed and unopenable by us, or a one-way hash. A column a migration adds that nobody has classified makes that panel declare itself incomplete rather than quietly shortening its list.

This page describes what that panel will tell you and why the shape of it is what it is. If the two ever disagree, the panel is right and this page is wrong, because the panel is derived from your database and this page is prose. That ordering is the whole reason to trust either.

The count

What the enumeration currently comes to

These figures are the ones the shipped code produces against the schema this build creates, and they move when the schema moves — which is the point of computing them rather than asserting them. The panel in your app recomputes them against your instance; take its numbers over these.

Counted by describeSchema() and disclosureFor() in server/lib/data-export.js, against the live database. On a narrow screen each row becomes its own labelled block.
What is counted Now What it means
Tables in the schema 51 Every table SQLite reports, including the nine the commerce work added and the two invite links added. A table the classification does not describe turns the panel red.
Columns classified 305 Every column of every table that holds rows about an account, each with its own verdict and its own sentence. The remaining tables hold nothing about any account and carry a note instead.
Items you are shown 46 Columns grouped into things a person can reason about — "Who you talk to", "What you said", "Your link to the payment processor". An item's headline verdict is the worst of its columns, never the average.
Items we can read 38 Readable by us, by an employee with database access, and by anybody who takes a copy of the database.
Items we hold and cannot open 7 Sealed under keys we have never held. This is the only verdict that is a protection claim, and it is deliberately the smallest number on this table.
Items held as a one-way hash 1 Your password. It cannot be read back. It can be guessed at offline, as fast as whoever holds it can afford, which is why the length of your password does more work here than in most products.

Thirty-eight to seven is the honest ratio, and we are not going to average it

Seven sealed items is not a small achievement — they are the seven that matter most, including every message body, every file, every filename and every conversation key. But a privacy policy that led with "encrypted" and let you infer the rest would be describing a different product. The great majority of what this server holds about you, it can read. What it cannot read is what you said; what it can read is nearly everything about the saying of it.

Sealed

The seven things we hold and have no key for

  • What you said. Message bodies, sealed under a per-conversation key the server has never held. Plaintext is padded to fixed buckets from 32 bytes to 16KB before encryption, so a stored ciphertext gives away a range and not a length.
  • What your attachments are. The filename, the MIME type and the file's own key, sealed together. EXIF, XMP and comment data are stripped from images before any of it is sealed.
  • Group names. Sealed under the conversation key — for groups created since names were sealed. Older groups still hold a name in the clear in a column a rename does not clear, and the panel says so on its own line.
  • Your private identity key. Sealed under a key derived from your password. This is the row everything else rests on, and it is why there is no password reset and cannot be one.
  • Your copies of conversation keys. Sealed to your identity key, which is itself sealed under your password.
  • Your copies of superseded conversation keys. Sealed the same way — and kept, indefinitely, everywhere you have not turned the per-conversation forward-secrecy setting on. That setting is off by default. Every edge of what turning it on buys is in the threat model.
  • What a group says you are. A short phrase a group admin can write beside your name in that group — "legal", "on the ground". Sealed under the conversation key, because it is written about you by somebody else, which makes it the one string in this schema we would least like to be holding in the clear. We hold the length of a phrase and the fact that one exists.

Readable

The ones worth knowing about before you decide

All thirty-eight are on the panel with a sentence each. These are the ones that change somebody's decision, and they are the ones the panel's own honesty audit pins to a named table and column so that a future rewrite cannot quietly relabel one as encrypted.

Who you talk to

Conversation membership is a plain, indexed table. One query joining it to itself produces the complete list of which accounts talk to which, on the whole instance. This is a lookup, not an inference, and for a lot of the people this product is aimed at it is more dangerous than anything in the sealed list above.

When, to the second

Every message carries a timestamp at one-second resolution. No sub-second column exists and no rounding or jitter is applied anywhere. Two accounts' activity can be correlated from that column alone, with no statistical technique and no access to any content.

How big every attachment is

Attachment sizes are stored in the clear and are not padded. A distinctive file has a distinctive size, and a size can be matched against a copy somebody else already holds.

Your two-factor secret

The server has to compute a code from it on every sign-in — that is what a shared-secret second factor is, and there is no version of it that is not. That sign-in step carries no password, so it cannot be locked to you. What changed: it used to be stored readable, and this page used to say that anybody holding the database could generate your codes. It is now encrypted under a key kept outside the database, so a stolen database, disk or backup is not enough. Somebody who has the running server has the key file as well, and for them nothing has changed — that is the honest limit and we would rather state it than round it up.

Your push endpoints, if you turn notifications on

A push endpoint is a stable, unique identifier for one browser on one device that survives signing out, and it names the push service it belongs to. Two accounts sharing one endpoint are one browser. What the push service itself learns is on the sub-processors page, and it is more than we learn.

When you sign in, and where from

The day you last signed in is stored only while you have the opt-in dead-man's switch armed — it is the clock the switch counts down from — and is empty otherwise; it is not suppressed by "hide presence", because that would quietly disarm the switch. Each session carries a coarse device label built from the User-Agent, and an uncommon label shared by two accounts minutes apart is a strong hint one person holds both. A preference stores 'Unknown device' instead; it is off by default because with it on, an intruder's session looks exactly like yours.

What the payment processor sends us, if you paid by card

A Stripe customer handle, which joins this account to your record at Stripe. And more than a handle: Stripe's notifications about your payments are stored here verbatim, so that each one can still be checked against the signature it came with, and a card payment's notification carries what Stripe collected from you — your email address and, depending on what it asked for, your name, your postal address, your country and the card's brand and last four digits. The card number, expiry and security code never reach us.

These notifications are the largest store of personal data in this database. They are encrypted on disk under a key this server holds, so we can read them. They are never swept — each one is also what stops the same notification being applied twice — so they outlive your account. Paying with a voucher leaves none of this, which is the whole point of it.

Which voucher you redeemed

The code itself is held only as a one-way hash — nobody, including us, can recover it from the database. What is recorded is that this account redeemed a voucher, which plan it bought and when. That one link is unavoidable: without it the server cannot tell whether a code has already been spent. It is why a voucher is unlinkable from Haven's side and not from the side of whoever sold it to you.

Your username, after the account is gone

Wherever an administrator acted on your account, your username is copied onto the audit row at the moment of the action, so the log can still answer "who deleted this account" afterwards. It therefore survives the account. That is the honest cost of an append-only audit log, and it is disclosed rather than discovered.

Group names on old groups

Groups created before group names were sealed still hold their name in the clear, and renaming one does not clear that column. If your group is old, assume its name is readable.

Beyond the database

Six things that are not rows, and are still true

  • Your connection carries an IP address to our reverse proxy, as every network connection does to every server. Rate limiters see it, use it, and forget it when the window closes. It is not written to any table at all, and that is enforced rather than promised: no column anywhere in this schema holds an address, and a test in the suite fails the build if one is ever added — it scans every column of every table on each run. The same test pins the absence of a raw browser-agent string and of a referrer. A policy can be changed by whoever writes the policy; this one has to get past a check that reads the database. Reaching an instance over a Tor onion service is the only thing that hides client addresses from the server, and Haven serves an Onion-Location header so an onion deployment advertises itself.
  • A call crosses our relay. Calls are relay-only by default, with no public STUN fallback, so placing one does not hand your address to the other participant or to a third party. The relay is ours, and it is placed to see both addresses, when a call started and how long it ran. It cannot hear a second of it: the media stays encrypted between the two browsers and a relay forwards packets it holds no key for.
  • Our logs do not name you, and that is code rather than a retention policy. The server writes a handful of lines to standard output that have a subject — an administrator elected at boot, a dead-man's switch firing, a socket handler that threw. Every one of those identifiers goes through a keyed hash first, under a 32-byte salt generated fresh when the process starts and never written to disk, and only the first twelve characters of the result are printed. It is stable inside one process, so somebody debugging a live incident can still tell forty lines about one account from forty lines about forty accounts; it changes at every restart and cannot be turned back into an account. An error prints its class, its stack frames and a keyed digest of its message — the message itself is dropped rather than scrubbed, because an arbitrary error message is free text of unknown provenance and no filter can honestly redact it. That is not caution for its own sake: a malformed sign-in request once printed a submitted password, because the JSON parser attaches the raw request body to the error it throws. A rejected payment notification logs a constant and never the body. There is one environment variable that turns the identifier hashing off for debugging, it is documented, and it defaults to off. Logs are the copy of your data most likely to leave a machine — into a log aggregator, a support ticket, a backup nobody is thinking about — and they outlive the accounts they name. This is the part of Haven we are most confident about and it has never been on this page.
  • A submission inbox lets somebody reach you with no account at all. An account can publish an address; whoever opens it writes a message, is given a codename, and gets a reply — with no account, no username and no row in this server’s user table. What we hold is one row saying a submission arrived at that inbox, when, and when the sender last came back; a one-way hash of the codename, never the codename; and the conversation key sealed to a key derived from it, which we cannot open. The timing is the part worth weighing: not who, not from where and not what, but that somebody approached this inbox on this date — which for an inbox of this kind is the metadata that matters most, and is exactly what a demand for “who contacted you around the 14th” would be answered with. It cannot hide the network connection, and the page says so above the text box rather than here. Text only for now: attachments are not supported yet.
  • Verifying a domain makes this server fetch from that domain. It is the only thing in Haven that does. An account claiming to be a newsroom asks us to look up a DNS record on the name it claims, or to fetch one fixed file from it over HTTPS, and the result goes into an append-only log. The lookup carries our address to that domain's DNS operator or web server, never yours, and it happens only when an account asks for it. What it is guarded against, because a server that fetches a name somebody else chose is the classic way into a private network: addresses that are not on the public internet are refused outright, redirects are not followed at all, the connection is made to the address that was checked rather than to a second lookup that could answer differently, and the response is size-capped. A binding, once made, is permanent. It can be withdrawn, which writes a second entry saying so, and it can never be deleted — deleting your account withdraws every live claim and leaves the record that they existed. The screen that offers it says so before the first claim, not after. And it proves less than the word "verified" suggests: we performed the lookup and you cannot repeat it, so what the log buys is that our assertion is signed, timestamped and impossible for us to deny later — not an attestation by the domain's owner.
  • This website stores one thing in your browser — whether you chose the light or the dark theme — under a key beginning haven_, which is the prefix the app's panic wipe sweeps. Nothing else. No cookie is set by any page here.

This website

What this site does on the network, which is almost nothing

No analytics, no cookies, no third-party scripts, no fonts fetched from anyone else's server, no CDN, no pixel, no tag manager, no error reporter. Not "anonymised": none. There is no analytics vendor on the sub-processor list because there is no analytics.

There are exactly two requests this site can make beyond its own HTML, stylesheet, script and favicon, both same-origin and both narrow:

  • The price list. Pages that actually render a price ask the server for the public plan catalogue — credential-free, no cookie, no query string, no custom header, and with the referrer suppressed so the request does not carry the name of the page you were reading. It exists so a price cannot be current on the server and stale on this page. Pages that render no price make no request at all; that includes the security page, the threat model and every page in this footer.
  • An enquiry you deliberately send from the contact form, which happens when you press the button and never otherwise.

Open the network tab and load any page here. That is the whole list, and it is right there in the panel rather than something to take on faith.

Other people

Who else touches any of this

Three, and only when the relevant thing is true of you: Stripe if you pay by card and the instance is configured for cards; a browser vendor's push service — Google, Mozilla or Apple — if you turn notifications on; and the hosting provider whose machines the instance runs on. Each is named on the sub-processors page with what it receives and what it does not.

We do not sell, rent or share what we hold for any commercial purpose, and there is no arrangement under which any of it is used for advertising. If that changes, it changes with a version of this document that says so on its face.

Your rights

How you actually exercise them here

The unusual part: we hold no address for you on your account — the one exception is the email address in a card payment's record above, which is not a channel we can verify you through — so there is no inbox you can write to that we could verify is yours. That rules out the ordinary "email us and we will confirm your identity" mechanism entirely — so the rights are built into the product instead, where the proof of identity is that you can sign in.

  • Access. The disclosure panel in Settings → Your data, live against your own database rows, with a count beside each item.
  • Portability. A full account export, paged, with ciphertext carried as ciphertext and its own omissions written inside the file — every column that is not exported is named there with the reason it was withheld, and anything neither exported nor accounted for is reported inside the file as an omission nobody explained.
  • Erasure. Delete your account from the app, password-confirmed. It removes your rows and unlinks your file blobs. What survives is named below rather than left for you to discover.
  • Rectification. Your username, status line and preferences are editable in the app. Almost nothing else here is the sort of fact that can be wrong — a timestamp of when you signed in is not a claim about you that we could correct.
  • Objection and restriction. Push notifications are opt-in and revocable. Presence recording is switchable, and switching it on erases what was already stored rather than merely hiding it. Device labels can be withheld. Forward secrecy is per conversation.
  • Complaint to us. Use the contact form and choose a complaint about how we handle your personal data. It is acknowledged the moment you send it, with the date it arrived, a reference to keep and the date a response is due — within a month — which is what the Data (Use and Access) Act 2025 asks of us. Complaining to us first is optional; nothing below depends on it.
  • Complaint. To your data protection authority, wherever you are. Which authority supervises us depends on where the operator is established, and that is one of the blanks a solicitor has to fill in — see the note at the top of this page.

What survives deleting your account

Not your subscription — that is stopped, first, and the deletion waits for it. This used to be the loudest omission on this page. If a card is paying for your plan, deleting your account cancels that subscription at the payment provider immediately, and only then removes your rows. If the provider cannot be reached or refuses, nothing is deleted and nothing is changed — we will not delete an account while its card is still being charged, because deleting it destroys the only reference we hold to the subscription and we would no longer be able to stop it. Cancelling this way ends the plan on the day rather than at the end of the period you have paid for, and the unused part is not refunded automatically; ask, and it is a decision a person makes in the payment provider.

And the case in between, because it is the one a page like this usually leaves out. Cancelling at the payment provider is immediate and cannot be reversed, and the deletion that follows it runs against a database that can, rarely, refuse — a lock held by another process, a full disk. Every reason we could refuse your deletion for is settled before we cancel anything: we run the entire deletion against the database and roll it back, so a refusal costs you a message and nothing else. But if the cancellation succeeds and the deletion then fails anyway, the honest description of where you are is your subscription is cancelled and your account still exists. We will not leave that silent. The subscription row is marked cancelled, so nothing in the product goes on advertising a plan you no longer have; your own security log gets an entry saying it happened; and our billing ledger gets one too, naming the subscription — that entry carries no link to your account and so outlives it, which is what makes a refund still workable afterwards. Deleting again is safe and cancels nothing twice.

Invoices and ledger entries. They deliberately carry no foreign key to your account row, precisely so they outlive it: somebody has to be able to answer a chargeback or a tax question six months later. They hold an amount, a currency, a status, the period covered, and a reference to the processor's own copy. No card number appears in one.

Your username in the administrators' audit log, wherever an administrator acted on your account, for the reason given further up this page.

A voucher row, if you redeemed one, with the account reference nulled. And messages you sent to other people remain in their conversations, sealed, as they would in any messenger — deleting your account does not reach into somebody else's.

Anything under a preservation order. If a court or other lawful authority has ordered us to preserve your account or a conversation you are in — a legal hold, see the canary page — deleting your account still removes the account, but the messages, superseded message versions and wrapped keys the order covers are kept, sealed as they always were, until the order is lifted; they are deleted then. The app cannot tell you this is happening, because telling you would disclose the order. What is kept is ciphertext and wrapped keys we have never been able to open.

The limit on all of it

The sentence this whole page rests on

The panel that proves this policy is served by the machine it describes

Deriving the list from the live schema stops it going stale. It does nothing at all about a server that wants to lie. The code drawing that panel is JavaScript this server hands your browser on every load, unsigned and unpinned, and a coerced operator does not need to break any cryptography — they modify one file, for one account, once.

That is not a hedge added to a privacy policy for cover. It is the most important fact about this product and it is on the homepage for the same reason it is here. What would narrow it — reproducible builds, published bundle hashes, an independent audit — is set out on the security page, and none of it is in place.

Changes to this policy: this page carries a version and a date, and a material change bumps both. We cannot email you about it, for the same reason we cannot email you about your bill — the notice mechanism is described in the terms.