This page describes how Housekeyring is designed to protect your data. Our full privacy policy will be published here before the app launches.
Privacy, handover and secrets design
Who sees what (the privacy rule)
Every synced row is classified on the server. The server never trusts the app to filter.
| Sensitivity | Rows |
|---|---|
private |
Items with is_private = 1 or category mortgage, and every child row of such an item: service events, documents, photos, costs, contacts, links, enquiries and field values. |
personal |
enquiries (names, phones and messages to trades), and contacts with kind = 'reference' (policy, account and meter numbers). |
financial |
costs, and photos attached to a cost (receipts). |
secret_household / secret_shared |
secret_blobs, by scope. |
| Member level | Can write | Hidden from them |
|---|---|---|
| household (owner, partner) | yes | nothing |
| viewer (house sitter, relative, tenant) | no | private, personal, financial, secret_household |
| incoming (a buyer during a handover) | no | private, personal, all secrets. Costs are visible but paid_by is removed. |
Further rules:
- Everyone always sees rows they created themselves.
- Hidden rows are sent as tombstones (
redacted: true), never left out. So when an item turns private, or someone's role is lowered, their phone deletes its cached copy on the next pull. Turning an item private re-sends its children for the same reason. - File downloads go through the same check: a viewer can't get a signed URL for a private item's document.
- Member emails are shown only to the household.
- Non-members get 404 for every house route, so house ids can't be probed.
- A viewer can be trusted with the boiler manual, the stopcock location and the shared wifi/alarm secrets. They can't see the mortgage, what anything cost, who you emailed for quotes, or your policy numbers.
Third-party access (REST API, MCP and connected apps)
Tokens and connected apps (Claude, ChatGPT…) follow the same rule as the app (scopes ∩ the live role ∩ the granted houses), with stricter defaults:
- The secrets vault is never available. It's end-to-end encrypted, so the server has only ciphertext. No scope exists for it.
- Mortgage and private items are hidden unless the owner opts in. Tokens and apps need
read:mortgage/read:private(andwrite:mortgageto record statements or payments), even though partners see these items in the app. These scopes are:- never pre-ticked, and labelled "sensitive financial data" on the consent screen;
- grantable only by the owner. A partner can grant them only if the owner allows it (
house_api_settings.partner_sensitive_scopes), and switching that off cuts their grants straight away. Viewers, house sitters and incoming buyers can never grant them.
- Every mortgage or private read is written to the audit log (
api_audit.detail, e.g.mortgage:<item id>), which you can see per token or app. - Viewers' tokens see exactly what viewers see in the app: no references, costs, private or financial rows, and no writes.
- The server never sends email by default.
send_emailneeds its own scope plus explicit confirmation. It sends from our no-reply address (no-reply@housekeyring.app) with Reply-To set to you, and you get a copy. Only recipient domains are logged. - None of this changes the handover: the mortgage and private items are still removed from the buyer's pack. Leaving a house, demotion and handover cut access at once. Deleting your account revokes every connected app.
Handover
Selling the house hands the logbook to the buyer without leaking the seller's private life.
Start. The owner calls
POST /houses/:hid/handover. This makes a single-use buyer invite, valid for 14 days by default and up to 90, and its URL is the QR in the handover pack. Only one handover can be active per house. Ownership transfer is blocked while it runs.Buyer joins using the invite. The buyer becomes
ownerwithincoming = 1:- They are read-only, can't manage members, and can't see private, personal or secret data (see the table above).
- The seller still runs the house and can keep tidying it.
GET /houses/:hid/handovershows the seller exactly what will be withdrawn.
Complete. The seller confirms with
POST …/handover/complete, normally on completion day. This permanently deletes:- private and mortgage items with all their children (including R2 files);
- all enquiries and reference contacts;
- all secret blobs and key envelopes. The key version resets, so the buyer starts a fresh vault and sets new alarm and wifi codes.
- the
paid_byfield on costs; - the guest guide and its bookings, and every unused invite.
Rows the buyer created themselves are kept. Then the seller household (owner, partners and viewers) is removed, and the buyer becomes a normal owner.
Premium. If the house was premium, the buyer gets a gift of
HANDOVER_GIFT_DAYS(365 by default, sourcehandover_gift). Otherwise the house resets to free. In both cases the seller's store reference is cleared.Cancel. The seller can cancel at any time before completion. This removes the incoming buyer and revokes the invite. If the buyer leaves, that also cancels the handover.
Everything else carries over: the structure, items, service history, documents, photos, non-reference contacts, costs (without paid_by), links and septic checks. That history is what makes the logbook worth handing over.
Secrets (end-to-end encrypted)
Alarm codes, key-safe codes and the vault are encrypted on the device. The server stores ciphertext it cannot read.
Device keys. Each install makes an X25519 key pair. The private key stays in the Keychain or Keystore, and the public key is registered with
POST /me/device-keys.House keys. Each house has two random 256-bit keys per
key_version:- a household key, for the owner and partners;
- a shared key, which viewers also get. It is for the things a house sitter needs, such as alarm or wifi.
Envelopes. A household device wraps each house key for each eligible device: X25519 key exchange, then HKDF-SHA256, then AES-256-GCM, through
PUT /houses/:hid/secrets/envelopes. The server checks that each target device is eligible for the scope:- household scope: owner and partner devices only;
- shared scope: all non-incoming members;
- incoming buyers: never.
Blobs. Secrets sync as ordinary
secret_blobsrows{scope, key_version, alg, nonce, ciphertext}, with last-write-wins and tombstones. Only the household can push them.New members and devices.
GET /houses/:hid/secrets/deviceslists eligible devices withhas_current_envelope: false. Any online household device wraps for them, and the app does this automatically on open.Rotation. When a member or device is removed, the house is flagged
rotation_needed, because the removed device had the old key. The next household device then:- creates
key_version + 1; - re-wraps it for the remaining devices;
- re-encrypts and pushes the blobs.
Uploading the new version clears the flag. Changing the actual codes (alarm, safe) is still recommended, and the app should prompt for it.
- creates
Recovery. If every household device is lost, the secrets are gone. A recovery code or passphrase-wrapped envelope is a possible later addition.
What the server can see: which devices exist, blob sizes, scopes and timestamps. It cannot see contents.
The server side, including eligibility, version checks, privacy filtering and the rotation flag, is implemented and tested. The device-side crypto is for the app to build, using libsodium or @noble/curves + WebCrypto.
Not end-to-end encrypted: the guest-guide wifi password and booking door codes. The server must render them on the guest's web page, so they are stored as plaintext. Codes are wiped when the booking ends or is revoked.
Guest guide
- The guide is premium-gated and shows only what the host puts in the guide. It is a separate document, never a view of the logbook, so nothing private can leak through it.
- Each booking gets a 256-bit random link token. Only its SHA-256 is stored, and the token is shown once.
- The link opens 7 days before check-in and dies 24 hours after check-out. It can be revoked at any time.
- Door and key-safe codes are shown only from
codes_visible_from(at most 2 days before check-in) until check-out. They are wiped at the end. - The page is public, read-only, sets noindex/nofollow, runs no scripts, sends no referrer and is not cached.
Sign-in identities
- What is stored for Apple and Google sign-in: the provider, the stable
sub, the client id, the email as last seen, and flags for verified and private-relay. - Apple refresh tokens: only to revoke them later, encrypted with AES-GCM.
- Not stored: ID tokens themselves, only a SHA-256 for single use until they expire.
- Linking by email: happens only for verified provider emails that exactly match. This stops someone with an unverified Google email taking over an existing account.
- Apple Hide My Email addresses stay as they are. They are never "upgraded" to a real address.
- Revocation: Apple tokens are revoked when an identity is unlinked or the account is deleted.
- Apple consent-revoked / account-deleted notifications: the identity is unlinked and Apple sessions end.
- Export: linked identities are part of
GET /me/export.
Logs, retention, GDPR
Logs record the route pattern (never the full URL), status, duration and ids. They never record emails, IPs, codes, tokens or row contents.
Emails in rate-limit keys are HMAC-hashed with
AUTH_PEPPER.Production turns off Cloudflare invocation logs, because they would record token URLs and IPs.
Retention, cleaned up by the daily cron:
Data Kept for Login codes until the next cron run once used; 1 day after expiry if unused Revoked or expired sessions 30 days Dev outbox 1 day Abandoned uploads 1 hour Files of deleted rows 30 days Ended guest codes until the booking ends GET /me/exportreturns everything the user created (Art. 15/20).DELETE /meerases the account (Art. 17); see API.md for the ownership rule.Cloudflare is the processor. A DPA and the EU/UK transfer terms come with the Cloudflare account. List Cloudflare, and Apple and Google as sign-in providers, in the privacy policy.
Ben already has ICO registration (UK data protection fee), so no action is needed beyond keeping it renewed.