Skip to content

Security and data residency

Where your data lives, and what actually protects it.

Every claim on this page names the mechanism behind it. A security page that says “bank-grade encryption” and moves on is telling you nothing you could check.

United Kingdom, and that constrains everything else

The database is in London — eu-west-2 — and it is the only place customer data is stored.

This is a constraint on the whole system rather than a hosting preference. Every service we add has to be able to process data in the UK or EU, or we do not use it, and that has already ruled things out. It applies to error tracking, analytics, email delivery and support tooling equally.

The complete list of third parties who could touch customer data is published as our sub-processor list. It is short, because the product is small and we would like to keep it that way.

What is actually in place

All of the following is built and running today, and each one is covered by a test that tries to break it.

Passwords are hashed with Argon2id

At the parameters OWASP currently recommends: 19 MiB of memory, two iterations. Argon2id is deliberately expensive in memory as well as time, which is what makes a stolen hash dump expensive to attack with specialised hardware. There is no path by which we could show you your own password, because we do not have it.

Session tokens are stored only as a peppered hash

Your browser holds a 256-bit random value; we hold an HMAC of it, keyed with a secret that lives in the environment rather than in the database. A stolen copy of the database therefore contains nothing that can be replayed as a login, because the key needed to check it was never in the database to steal.

Revocation is immediate, not eventual

Sessions are looked up on every request rather than trusted from a signed token. That costs one indexed read and buys the ability to actually end a session — changing your password signs out every other device on the spot, and tells you how many there were.

Tenant isolation is structural

The function that answers “which organisation is this?” takes no arguments at all. There is nothing a request could contain that would change the answer. Where an identifier does come from the address bar, it is never used alone — the query filters on it and on your organisation together, so somebody else’s identifier matches no row.

A request for someone else’s data returns 404, never 403

“Forbidden” would confirm the thing exists, which lets an outsider work out which app identifiers and link aliases are real, and therefore who our customers are. “Not found” says nothing at all.

API keys are shown once and stored hashed

A key belongs to one app and one environment and cannot be moved between them. We keep a hash and a short display prefix, so we genuinely cannot show you a key again — which is why rotation exists. Rotation issues the new key and kills the old one inside a single transaction.

IP addresses are never stored

An IP address is personal data under UK GDPR. We need to tell one visitor from another for rate limiting and, later, click counting; we do not need to know who they are. Addresses are salted and hashed as they arrive and the raw value is never written to disk — not in a log, not in an error report, not in a trace.

Two-factor authentication on every plan

TOTP from any authenticator app, with ten single-use backup codes. Included on the free plan. Charging for the control that most reduces account takeover is a practice we are not going to adopt.

Rate limiting on both the address and the account

Per-address alone is beaten by an attacker with many addresses; per-account alone is beaten by one common password tried against thousands of accounts. Both counters are always incremented, so tripping one cannot be used to avoid the other.

An append-only audit log

Sign-ins, failed sign-ins, password and address changes, two-factor enrolment, key and app lifecycle, and every membership change. The code that writes it exposes one function and that function inserts: there is no update helper and no delete helper anywhere in the codebase.

A nonce-based Content Security Policy

Only scripts carrying a single-use value issued for that one page load may run, so a script injected into a page is refused by the browser rather than by us noticing. Getting this right cost us static page caching, which we judged the better trade for a dashboard that holds API keys.

Nothing customer-facing is ever hard-deleted

Organisations, accounts and apps carry a deletion timestamp rather than being removed, and links are archived rather than dropped. A link printed on packaging has to keep resolving. The database enforces it: the foreign keys refuse a hard delete outright.

What we do not have

The security pages that only list strengths are the ones worth trusting least.

Not built yet. No SOC 2 or ISO 27001. We are a new company and have not been audited. Anyone claiming a certification at this stage would be describing an intention.

Not built yet. No penetration test yet. The security work described above is designed in and tested by us, which is not the same as having been attacked by somebody else. An independent test is planned before we take payments.

Not built yet. No published uptime figure and no status page yet. We have no service history to report, and a number invented before there is traffic would be meaningless.

Not built yet. No bug bounty programme. If you find something, please write to [email protected] and we will respond. We will not threaten you.

Reporting a vulnerability

Write to [email protected] with enough detail to reproduce it. We will acknowledge within two working days and tell you what we intend to do.

Please do not run automated scanners against the service, access an account that is not yours, or test anything that would degrade the service for other people. Everything else, we would genuinely rather know.