> ## Documentation Index
> Fetch the complete documentation index at: https://veriqa.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Resilience to a personal-data leak

> You can store the user ↔ channel link as a one-way keyed hash instead of a Telegram id or an email address: a leaked database does not give the contact back, and two leaked databases do not join.

Any connector between an application and a messaging app has to answer which account in it belongs to this user — and the usual answer is stored for as long as the account itself lives. Why that
column is more toxic than a password is on the
[Resilience to a personal-data leak](https://veriqa.app/whats-next/leak-resilience?lang=en) page.

<Note>
  Veriqa already provides the mechanisms this scenario needs; you implement the logic on top of them
  on your side, in whatever way suits you — see [What you build yourself](#what-you-build-yourself).
  We are working on making it simpler to implement in the product.
</Note>

## What Veriqa stores today

You base the risk decision on what is stored today.

**The channel identifier and the profile are stored in the clear.** The key of a channel identity is
the pair "channel type + user identifier within it", and the profile sits next to it: display name,
first and last name, username, email address, phone number, avatar, locale. There are no derived
forms along the storage path, and the record has no lifetime either. This is exactly the behavior
any connector has — but it is better to know it in advance.

**Masking today covers the journals only.** By contract the audit trail and the logs write neither
tokens nor unmasked identities, and that works. The rule does not touch the main store.

**And a qualification the risk decision turns on: out of the box that store does not exist.**
Veriqa ships no implementation of the port that would persist a channel identity and registers
none, so a stock installation keeps the profile only for the minutes the transaction lives.
Everything described above starts applying the moment you register an implementation of your own.
The full inventory — what is kept where, for how long, and what is encrypted — is in
[Security and data handling](/docs/guides/data-handling).

<Note>
  The encryption the product already has applies to **channel provider secrets**, not to user
  identifiers. The [Preparing for production](/docs/guides/hardening) checklist covers secrets and the database connection — not identifiers.
</Note>

## How the mode is designed

The mode is designed as the integrator's decision — a tenant-level mode switched on deliberately, not
a default that changes behavior for everyone. It comes down to four properties, and they are
connected.

**A derived value instead of the native identifier.** The record holds a one-way hash keyed with a
secret, not the identifier itself. The same person always yields the same value — the account is
recognized on every sign-in — but the value does not unwrap back into a Telegram id or an email
address.

**The secret does not live next to the data.** It belongs in a KMS or a secret store, self-hosted
deployments included, with no concessions. This is not hardening advice but a condition of the
scheme working at all: the space of possible inputs is small enough to be enumerated in full, and a
secret inside the same snapshot as the database would let an attacker simply recompute the function.
Where there is no secret store, the mode is designed **not to switch on** rather than to degrade
into something weaker.

**Different integrators get different values.** The tenant takes part in the computation, so the
same person becomes a different value in each integrator's database. Two leaked databases do not
join to each other, and neither joins to a purchased marketing list.

**And where the scenario allows — no value at all.** If the user always arrives from the channel,
the identifier comes with every confirmation and there is nothing to keep between transactions. The
strongest form of the property is not "well encrypted" but "never written down".

## What you build yourself

You implement the properties of the mode where the "user ↔ channel" link is stored:

* **A hash in your database.** Your application stores the link between an account and a channel —
  see [Sign-in and channel linking in one action](/docs/scenarios/channel-linking). Instead of the `sub`
  or the `channel_type` + `channel_user_id` pair, write a one-way hash keyed with a secret, with the
  tenant among its inputs; on every sign-in, recompute it from the `sub` you receive and use it to
  find the account. Keep the secret in a KMS or a secret store; a server that cannot obtain it does
  not start rather than falling back to storing values in the clear.
* **A derived `sub`.** To keep the native identifier from reaching the application at all, your own
  [claims mapper](/docs/guides/customization#claims-mapper) issues a derived value in `sub`: the mapper
  receives the original subject and the request's tenant (`ClaimsMappingContext.TenantId`). In that
  case, do not request the scopes that return the contact itself — `channel`, `email`, `phone`:
  with them the token carries the identifier or the address as is.
* **Nothing extra in storage.** Do not register an implementation of `IChannelIdentityRepository`:
  without one, Veriqa does not persist the channel identity, and what the sign-in needs arrives
  with every confirmation. If you do need an implementation of your own, have it write the derived
  value rather than the identifier and the profile.

## What to keep in mind

<Warning>
  **The data does not leave the scope of GDPR or national personal-data law.** Pseudonymization
  there is a safeguard (GDPR Art. 4(5) and Art. 32(1)(a)), not anonymization: the data stays
  personal and the controller's duties do not go away. What changes is how much there is left
  to lose.
</Warning>

* **Finding a user by their Telegram id or email address becomes impossible.** That is the price of the mode.
* **A scenario that pushes a confirmation into the channel itself** does need the native identifier
  back. Store it encrypted under the same service's key — reversible, unlike a hash: stronger than
  storing it in the clear, weaker than a mode that stores nothing.
* **The collected-claims store keeps its records by the original `sub`.** If you have
  [enabled it](/docs/guides/storage#collected-claims-store), the native identifier is stored there even
  when the application receives a derived `sub`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.