Security
Version 1.2 · Last updated 5 September 2026
This page is Annex II of the Data Processing Addendum — the technical and organisational measures we are contractually committed to. It describes what is in place today, and it ends with a list of what is not. We would rather you learned the gaps from us than found them yourself.
Reporting a vulnerability
Email security@capitis.app. Tell us what you found and how to reproduce it. We will acknowledge within 24 hours and keep you updated until it is closed.
Please do not run automated scanners against production, do not access or modify data that is not yours, and give us reasonable time to fix an issue before publishing it. We will not pursue legal action against anyone acting in good faith within those limits. We do not currently pay bug bounties.
How identifiers are protected
This is the part that matters most, so it comes first.
We receive raw email addresses and phone numbers, and hash them the moment they arrive. The
email and phone fields on POST /v1/end-users and POST /v1/end-users/{uuid}/identifiers, the
email and phone columns of the audience CSV import, and the Single.id partner webhook all take
the plain value. Raw is currently the only option: there is no field anywhere in the API for an
identifier you have hashed yourself. The API process computes an HMAC-SHA256 fingerprint of the
value in memory and stores only that fingerprint — what you send in those fields is never written
to the database. Wallet addresses work the same way: you send the address, we store the
fingerprint.
Card tokens are the exception: they are not hashed at all. A card token must already be an opaque reference issued by a card vault, and later steps have to be able to use it, so it is stored as the vault issued it (lower-cased). To keep a real card number out of that field we reject any value with the length and checksum of one. A bank-link reference type exists in the schema, but no route can write one today.
Email and phone fingerprints are hashed a second time before storage. The fingerprint we computed is passed through a second HMAC using a server-side secret that exists only in the API environment — never in the database, never in the dashboard, never in a backup of the database. Those stored values carry a version prefix so the secret can be rotated.
Why that second step exists: the fingerprint is deterministic, which is what makes matching a returning shopper possible across publishers. Determinism cuts both ways — anyone holding both the hashing parameters and a guessed value could test whether it matches. Adding a secret the database does not contain means a stolen database, on its own, cannot be tested against a list of candidate email addresses.
Only email and phone get that second step. A wallet address is already public on the blockchain, so a second keyed hash would protect nothing that is not already visible to anyone; we store the fingerprint as computed. A card token gets no hash at all: as above, it has to stay usable in the form the vault issued it.
Anything you put in the free-text metadata object on an end-user is stored as you send it.
That field is yours and we do not inspect it, so do not put an email address, a phone number, or
anything else you want hashed, into it.
These are pseudonymous personal data, not anonymous data, and we treat them that way in every policy on this site. Anyone who tells you hashed email addresses are anonymous is wrong.
Revocation is immediate. An identifier can be revoked individually through the dashboard or the API, which removes it from matching from that moment.
Infrastructure
- All production infrastructure runs in Manchester, United Kingdom, on infrastructure operated by Hostinger International Limited.
- End-user data — identifiers, clicks and conversions — is held in the UK and is never replicated to another country. Some customer contact data does leave the UK, because the services that send your sign-in emails and handle your billing sit outside it. Each one, and the safeguard covering the transfer, is named on the subprocessors page. None of them receives end-user data.
- The database and cache are not reachable from the public internet. They listen only on the host's internal interface, and the host firewall denies all inbound traffic by default, permitting only administrative SSH and HTTPS.
- Administrative access to the host is by SSH key only. Password authentication is disabled at the server, as is password-based root login.
Data in transit
- TLS 1.2 or above on every public endpoint, with certificates renewed automatically.
- HTTP Strict Transport Security is enforced (
max-age=15552000; includeSubDomains), so browsers refuse to talk to us over plain HTTP after the first visit. - Additional response headers in force on every page:
X-Frame-Options: DENY(no clickjacking via embedding),X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin, and aPermissions-Policythat switches off camera, microphone and geolocation.
Access control and tenant separation
- Sign-in is by email link, Google or GitHub. There are no passwords for us to lose.
- Session cookies are
HttpOnly,Secure,SameSite=Lax, and use the__Host-and__Secure-prefixes, which stop a subdomain from writing them. - API keys are stored as hashes, never in plain text. A key is shown once, at creation.
- Every API request is scoped to a single publisher. Every query filters by publisher at the application layer, so one customer's request cannot reach another customer's rows.
- Requests made with an API key are also held to their publisher by the database itself: they run under a restricted database role whose row security policies return only that publisher's rows, even if a query forgot its own filter. This second layer does not yet cover the dashboard pages that read the database directly, or the platform's internal jobs — see the list at the end.
- Internal endpoints used by the platform itself are separated from the public API surface and require their own credentials.
- Administrative access to the dashboard is a separate, explicitly granted role.
Logging and audit
- Structured application and access logging, with credential-shaped values redacted before they are written.
- A per-publisher audit log records security-relevant actions — API key creation and revocation, identifier attachment and revocation, webhook changes — and is visible to you in the dashboard.
- The click and conversion ledger is append-only for the commercial facts: which link, which merchant, which network, what it earned. Those are written once and corrected by adding a later record, not by editing history, which is what lets a commission dispute be reconstructed months later. Two columns are the exception and are deliberately not permanent — the IP address and the user agent on a click are erased 90 days after the click by a nightly job. The record itself survives that erasure; only those two fields are emptied.
Deleting your end-users' data
- Closing an account starts a 30-day countdown, after which a scheduled job erases the personal data of every end-user on it: the end-user records, their identifiers, and any card or bank reference. Nothing about this depends on a person remembering to act.
- The click and conversion records themselves are kept, with the end-user detached from them and the IP address and user agent erased. They are the commission trail, and a network can restate a conversion long after the click.
- The job records what it deleted, row by row and count by count, in your audit log. That record is what a written confirmation of deletion is drawn from.
- Running it twice cannot double-delete or move the completion date, so an interrupted run is safe to retry.
Your own details go on the same clock. Names, email addresses, sign-in records, saved network logins, webhook addresses and settings are deleted automatically 30 days after you close the account — you do not have to ask. A subscription is cancelled at the same moment so you are not billed again. What remains is an anonymous record holding the payment history, which UK company law requires us to keep for six years; the account record is emptied rather than deleted because deleting it would take that history with it.
Backups and resilience
- The database is dumped nightly, compressed, and retained on the server for 14 days. The job that does it is the server's own backup job: Capitis runs on a shared server that also hosts other projects operated by the same people, and that one job backs up every one of them, each project's database to its own file.
- Every dump is size-checked immediately after it is written, so a silent failure raises an error rather than leaving a directory of empty files.
- Health and readiness checks run on every service; a watchdog restarts a service that stops answering.
- A second copy of every dump is pulled off the server daily, to a company-controlled machine, over a connection that only reads from the server and never writes to it. That machine is a workstation rather than a data centre, and it travels with the person who operates it, so we do not claim a country for it. Every end-user record it holds originated in the UK production database and the copy is encrypted at rest. The dumps land on that machine's disk, which is encrypted at rest. Thirty days of copies are kept there.
- The configuration bundle — which holds production secrets — is not copied off the server today. The server writes it every night alongside the dumps, but the step that would carry it to the second machine, encrypted during the transfer with a key the server does not hold, is not currently running. Until it is, production configuration exists only on the server itself, and losing the host would mean rebuilding that configuration by hand.
- The age of the newest dump on the server is checked every fifteen minutes and shown on an internal status page, so a nightly job that had quietly stopped would be visible.
What that second copy is, and is not. The dumps on the server sit on the same disk as the database they protect, so the copy held off the server is what stands between us and the loss of the host — for the database, not for the configuration bundle above. It is one machine, which has to be switched on for that day's copy to be taken, and nothing raises an alarm if copies stop arriving — the check above watches the server's dumps, not the copies of them. No third party holds your data independently of us, and no restore has been rehearsed end-to-end. Both are in the list below. We are telling you this in the document that a court would read.
Software development
- There is no second-party code review. One account holds write access to the repository, so every change is read by the person who wrote it and by nobody else.
- Continuous integration runs linting and formatting checks, type checking, the automated test suite against a real PostgreSQL database, and a production build — on every push to the main branch and on every pull request. Nothing enforces the result. The repository's plan does not offer branch protection, so no check can be made a required one, and the deploy does not ask whether the last run passed. A change whose checks failed can still reach production.
- A pre-commit hook runs the linter and formatter over changed files on the author's own machine. It corrects what it can rather than refusing, and it runs neither the type check nor the tests, so it is a convenience and not a gate.
- Request and response bodies are validated against typed schemas at the API boundary, so malformed or unexpected input is rejected before it reaches business logic.
- Database changes ship as version-controlled migration files applied by a migration runner. Editing a production database by hand is prohibited by internal policy.
- Secrets live in environment files on the host, never in the code repository. Documented handling rules forbid printing or logging them.
Incident response
If we discover a personal data breach affecting your data we will tell you without undue delay and in any event within 48 hours of becoming aware, with what we know at the time and the rest as we learn it. That commitment is contractual — DPA §7.
Security contact: security@capitis.app.
Business continuity and the honest bit
Capitis is an early-stage company operating a small, deliberately simple production estate. Some of what a mature vendor questionnaire asks for, we do not have yet. Here is the complete list, so you can make your own decision:
| Not yet in place | What that means for you | Status |
|---|---|---|
| A backup held independently of us | A daily copy of every database dump is now kept off the server on company hardware (see Backups above) — the configuration bundle is not — but it is one machine, and nothing alerts anyone if the copies stop arriving. No third party holds a copy of your data independently of us | Not started |
| Second-party code review | One account has write access to the repository. No change is read by another engineer before it reaches the main branch — the automated checks and the author are the only things standing between a mistake and production | Comes with the second engineer |
| An enforced merge gate on continuous integration | The checks run on every change, but nothing acts on the result: a change whose checks failed can still be merged and deployed | Needs a repository plan that offers branch protection, or making the repository public |
| Sending us an identifier you hashed yourself | Every identity route takes the plain email address, phone number or wallet address. It is protected in transit by TLS and hashed the instant it arrives, but it does leave your systems in its original form | Deferred |
| Database-level row security on every path, with a login that cannot step around it | Requests made with an API key now also run under database row security (see Access control above). The dashboard pages that read the database directly and the platform's internal jobs do not yet, and the application's database login can still step outside those rules — so they catch a mistake in a query, not a compromised application | Planned |
| A server of our own | Capitis runs on a shared server that also hosts other projects operated by the same people. Each has its own database, but they share the machine, its disk, its backup job and its administrators, so a fault in or a compromise of that server affects all of them | Not started |
| Automated dependency vulnerability scanning in CI | We update dependencies, but no automated alert tells us the day a component we use is found vulnerable | Planned |
| SOC 2 / ISO 27001 certification | No third party has audited these claims | Not started. We will not imply otherwise |
| Penetration test by an external firm | No independent adversarial test has been run against production | Not started |
| Formal disaster recovery test | Restores have not been rehearsed end-to-end against a clean host, from either the copies on the server or the copies held off it | Planned |
| Multi-factor authentication on customer accounts | Sign-in is passwordless — an email link or an identity provider. If you use Google or GitHub, whatever MFA you have there applies. If you use the email link, your inbox is the factor | Native MFA planned |
| Encryption of data at rest on the host disk | The host's disk is not encrypted at the block level. Data at rest is protected by host access control and a firewall that denies everything inbound by default — not by disk encryption. Someone in physical possession of the underlying drive could read it | Planned |
| Encryption of stored network credentials with a dedicated key | The schema is in place for it; today credentials rely on host access control rather than an application-layer key | Planned |
| 99.9% uptime commitment | We do not offer a contractual service level yet, and we will not pretend to | Comes with general availability |
If any of these is a blocker for you, say so at security@capitis.app. We would rather have the conversation than have you assume.
Security reports: security@capitis.app · Privacy: privacy@capitis.app
N888 LIMITED, 20 Wenlock Road, London, England, N1 7GU — Company Number 17434249