A digital will is only as trustworthy as the identity behind it. If you cannot prove the person who signed the document is the person who wrote it, the whole thing falls apart in court. That is why estate platforms cannot treat KYC (Know Your Customer) as a checkbox. It has to be the foundation.

This post walks through the compliance stack that protects sensitive estate data: how identity gets verified with Sumsub, how passwords are handled with OPAQUE so the server never sees them, and how fields are encrypted so a database leak does not equal a data breach. Code samples are real patterns from a production estate platform.

Why estate platforms are different

A typical SaaS KYC requirement is "is this person real and not on a sanctions list?" An estate platform has stricter needs:

- Legal validity. A will needs notarisation grade identity proof. Photo ID plus a liveness selfie, not just an email confirmation.

- Long lived data. Records have to stay searchable, decryptable, and auditable for decades. People die. Their wills do not get re-uploaded.

- Tamper evidence. If a beneficiary contests the document, you need cryptographic proof that nothing changed after signing.

- Multi role access. Executors, witnesses, beneficiaries, and legal partners all need different slices of the same record. Each slice has its own access rules.

Three frameworks shape the controls:

- HIPAA if any health information is stored (advance directives, organ donation wishes).

- SOC2 Type 2 for service organisation controls. Annual audit, continuous monitoring.

- ISO 27001 for the information security management system. Risk register, policies, controls catalog.

Together they push you toward a few non negotiables: encryption at rest with AES-256, encryption in transit with TLS 1.3, audit logs for every access, role based access control, and proof you can revoke access on demand.

Step 1: Identity verification with Sumsub

Sumsub is a third party identity verification provider. The user uploads a passport or national ID, takes a selfie, and Sumsub runs OCR, liveness checks, and sanctions screening. The result is a `reviewAnswer` of `GREEN`, `RED`, or `YELLOW`.

You never want to call Sumsub from the browser. The credentials sign each request, and they have to stay on the server.

Signing a Sumsub request

Sumsub uses HMAC-SHA256 over a canonical string. Get this wrong and every request returns 401.

The signature goes in headers alongside the app token and the timestamp:

A few non obvious rules:

- The timestamp is seconds, not milliseconds. Off by a factor of 1000 is a common first bug.

  • The `body` is the raw JSON string you send. If you stringify with different whitespace than what gets sent on the wire, the signature breaks.
  • Always set a timeout. KYC calls block your user flow, and a hanging Sumsub call hangs your sign up.

Validating the response with Zod

Sumsub returns a deeply nested JSON document with optional fields everywhere. Parse it through Zod so a future API change cannot crash the route.

`.passthrough()` is doing real work here. It tells Zod to keep unknown fields instead of stripping them, which matters when Sumsub adds a new property and you do not want a deploy to break.

Mapping the verdict to your domain

Map the Sumsub answer to a status your codebase actually understands. Do this in one place so the rule is not duplicated across UI, audit logs, and gating logic.

`YELLOW` (manual review needed) maps to `pending`, not `approved`. A user in manual review should not be able to sign legal documents yet.

Step 2: Password handling with OPAQUE

Passwords are the other half of authentication. Most apps hash them with bcrypt or Argon2 and call it a day. That works, but the server still sees the plaintext password during login. For estate documents, that is a risk you do not need to take.

OPAQUE (RFC 9807) is a Password Authenticated Key Exchange protocol. The server stores a registration record but cannot derive the password from it, even with infinite compute. The user proves they know the password without ever sending it.

As a bonus, OPAQUE produces an `exportKey`, a high entropy secret derived from the password. You can use it as input to client side encryption, so the same password that logs the user in also unlocks their encrypted data.

Registration flow

Three steps, two on the client and one on the server.

The server stores `registrationRecord`. That record cannot be brute forced offline the way a bcrypt hash can. There is no hash to crack.

Login flow

Same shape, four steps total.

Note `opaque.client.finishLogin` returns `undefined` for a wrong password. Always check before destructuring or you get a confusing crash instead of a clean auth error.

Deriving an encryption key from the export key

The `exportKey` is 64 bytes of high entropy. Run it through HKDF with a domain separation label so the same export key cannot be reused across purposes.

Two things worth noticing:

  • `extractable: false` means the raw key bytes never leave the Web Crypto subsystem. JavaScript code cannot read them out, which limits the blast radius of a compromised dependency.
  • The `info` string includes a version (`v1`). When you change the derivation rules later, bump the version so old keys do not accidentally collide with new ones.

A practical caveat: OPAQUE export keys are fine for deriving wrapping keys, but Argon2 is the right primitive for the actual password to key step in many designs. Argon2 is memory hard and tuned for password derivation. Use OPAQUE for authentication, and Argon2 for the long lived encryption key wrap. Many production systems use both.

Step 3: Field level encryption at rest

Disk encryption is not enough. If your database is exfiltrated, the attacker has plaintext rows. Field level encryption means every sensitive column is encrypted with AES-256-GCM before it is ever written to Postgres.

Three rules:

- Random IV per encryption. Reusing an IV with the same key destroys GCM security. Always `randomBytes`.

- Auth tag is mandatory. GCM gives you authenticated encryption. Throw away the tag and you have crippled the algorithm.

- Generic error messages. When decryption fails, return `"Decryption failed"`. Do not leak whether the IV, tag, or ciphertext was the broken part. That is information for an attacker.

For searchable fields (like email or phone), encrypt the value but also store a hashed version with HMAC-SHA256 and a separate `HASH_SALT`. You can find the row by hash, then decrypt the actual value once you know it is yours to read.

Step 4: Audit logging without leaking

Every read and write of sensitive data needs an audit log row. SOC2 wants to know who saw what, when, and from where. HIPAA wants the same for any PHI.

The trap: audit logs themselves are sensitive. If you write `User 42 viewed SSN 123-45-6789`, your audit log just became another copy of the SSN. The fix is to encrypt the log entry separately, with its own key, and to store references rather than values.

The `AUDIT_LOG_ENCRYPTION_KEY` is a different key from the field encryption key. If one leaks, the other still protects its layer. That is defence in depth in the simplest possible form.

Putting the layers together

Here is what a single login looks like end to end:

  • User submits an email and a password from the browser.
  • Browser runs OPAQUE login. Server verifies and returns a session cookie. Server never saw the password.
  • Browser derives an encryption key from the OPAQUE `exportKey`.
  • User uploads ID documents. Browser sends to Sumsub directly via a signed token.
  • Sumsub webhook hits your server with the verdict. Server validates the webhook signature, parses with Zod, and updates the user's `kycStatus` to `approved` or `pending`.
  • User views their will. Server fetches the encrypted field from Postgres, decrypts it with the field encryption key, and returns plaintext over TLS 1.3.
  • Audit log row is written with the access metadata, encrypted with the audit key.

Each step is independent. Compromise the database, you get ciphertext. Compromise the auth server, you cannot read fields. Compromise both, you still need the user's password to derive their personal vault key. That is the point.

What to take away

  • KYC for estate platforms is identity verification plus liveness plus continuous compliance. It is not a one time form.
  • Sumsub handles the document and biometric layer. Sign every request, parse every response with Zod, map verdicts in one place.
  • OPAQUE removes the server from the password trust path. The bonus `exportKey` doubles as input for client side encryption.
  • Field level encryption with AES-256-GCM means a database leak is not a data leak. Random IV, auth tag, generic errors.
  • Audit logs are part of the threat model. Encrypt them with a separate key and store references, not values.

The compliance frameworks (HIPAA, SOC2, ISO 27001) read like a wall of paperwork, but underneath they push you toward the same handful of engineering primitives. Get those primitives right and the audit becomes evidence collection, not a scramble.