Security

What holds, and how you can check it.

Written to be verified rather than believed. Each line names the mechanism behind it, and the last section lists what is still on the way — because that is the part a security review asks about.

Last reviewed July 2026

Tenant isolation

The strongest property in the system, and the one worth understanding first. Separation between customers is enforced by the database, not by application code remembering to add a where clause.

Row-level security, forcedEvery tenant table has row-level security enabled and forced, with policies that compare the row’s organization against a transaction-local setting. The application’s database role has no BYPASSRLS, so the policies apply to it the same way they apply to anything else.
A query with no tenant fails loudlyThe policies read the current organization through a function that raises when it is unset. A query issued outside the one wrapper that sets it does not quietly return an empty result — it errors. An empty result is how a missing filter hides for a year.
One choke point, one transactionExactly one module in the codebase may hold a database connection, and a lint rule fails the build if another one imports a driver. The tenant setting is transaction-local, which is what keeps it from leaking into whichever request borrows the same pooled connection next.
Cross-tenant reads are deliberately awkwardThe handful of paths that must read across tenants — sign-in, before any organization is known, and platform-wide reporting — use a separate, separately-named function on a separate database role, so every one of them is a single search in review.
Photos are keyed per organizationObject-storage keys are prefixed with the organization id and the read path refuses a key outside the caller’s own prefix. Lead and event ids are global sequences, so a key without that prefix carries no evidence of who owns the bytes.
Offline queues are separated tooOne host serves every tenant, so the browser no longer isolates storage by origin. A booth phone therefore queues captures in a separate IndexedDB database per organization, and the organization travels in the payload so the server refuses a stale tab that syncs after the user switched workspaces.

Where it runs

Application, database and object storage run on EU infrastructure— a single server in Germany or Finland, with backups to EU storage in the same region. There is no US region and no cross-region replication.

Two third parties see data in the course of doing their job: the model provider that reads a card photo and runs the enrichment search, and the mail provider that delivers a sign-in link. Both are named on the privacy page, which is also where to look for what is sent to each.

Credentials and secrets

Nothing reusable is stored in the clearSign-in links, the short sign-in code and booth join codes are stored as SHA-256 hashes and compared in constant time. A stolen database backup does not contain a set of working sign-in credentials.
No passwords at allSign-in is a one-time link or code to an email address. There is no password to reuse across sites, to phish, or for us to leak.
API keys are read-only, and shown onceA key can read leads and analytics and can do nothing else — writes stay behind a session, because a long-lived key ends up in a scheduled job’s environment. Keys are hashed at rest and displayed exactly once, with a short fragment kept so you can tell them apart afterwards.

Webhook delivery

Every delivery carries a delivery id, a timestamp and an HMAC-SHA256 signature over the raw request body with the timestamp and a scheme version inside the signed string. That is what binds the signature to the body and to the moment — a captured request cannot be replayed under a fresh timestamp, and a future scheme cannot be accepted by accident by a receiver that only implements this one.

The scheme is documented in full, including the two mistakes every receiver makes once: re-serialising the parsed JSON instead of hashing the raw bytes, and comparing digests with === instead of a constant-time comparison.

What each capture records

A lead stores what was on the card, what the enrichment pass could verify with the pages it read, the answers your staff collected, and — where your team chose one — the lawful basis recorded at the booth together with the exact wording of the notice that was shown when the choice was made. Superseded wordings are kept, because “which notice did they agree to” is the only question a stored version can be asked later.

You are the controller of that data. The product records the choice; it does not make it, does not default one, and does not advise on which is right.

What is on the way

The list every security review asks for, so you do not have to ask. These are in progress rather than in place, and the distinction matters more than the roadmap does — a page that lists only strengths is not a security page.

  • An external penetration test and a third-party review. Neither has been carried out yet.
  • A signed data-processing agreement, a subprocessor list and a data-protection impact assessment. In preparation, not yet available to sign.
  • A retention and deletion schedule enforced by the system. Today data stays until it is deleted, and deletion is a request to the address below rather than a button.
  • SSO, SCIM and audit-log export, for organisations that need them.

If one of these is what stands between you and using the product, say so — knowing which one is genuinely useful, and it moves things up the list.

Reporting something

If you have found a vulnerability, write to info@klaari.ee and describe what you found and how to reproduce it. There is no bug-bounty programme and no safe-harbour policy in place yet, so a note before testing anything disruptive is appreciated.