Skip to main content
An encrypted field is returned as an AENC envelope — a Base64 string that always begins with QUVO — on a normal read. To receive the plaintext, a request must prove it is authorized by signing a canonical string with the project’s private key and attaching three headers. Authorization is checked per request. A request with no signature, or an invalid one, gets the ciphertext back — the read never fails because decryption was not authorized. This keeps ordinary reads working while gating plaintext behind the signature.

The decrypt headers

Send all three headers together. They are all-or-nothing — a partial set is ignored.

The canonical string

The signature is computed over a canonical string the server reconstructs from its own trusted context — you sign the same shape the server will rebuild. Fields are joined with a newline (\n):
Sign the raw bytes of the exact string above — real newline characters, no trailing newline, no extra whitespace. A single mismatched byte produces a different signature and the value comes back as ciphertext.

Signing algorithm

The key pair is Ed25519. To authorize a read:
1

Build the canonical string

Assemble v1\n{projectId}\n{environment}\nquery\n{timestamp}\n{nonce} using a fresh timestamp and a fresh random nonce.
2

Sign it

Sign the UTF-8 bytes of that string with your Ed25519 private key.
3

Base64-encode the signature

Base64-encode the 64-byte signature — that is the X-Decrypt-Signature value.
4

Send the request with the three headers

Attach X-Decrypt-Signature, X-Decrypt-Timestamp, and X-Decrypt-Nonce, then run your query. Encrypted fields in the response come back as plaintext.

How the server verifies

The server checks each step in order and stops at the first failure — returning ciphertext, never an error:
  1. Timestamp skew — the timestamp must be within the allowed window of the server’s clock. A stale or future-dated timestamp is rejected.
  2. Public key — the server loads the environment’s public key. If no key pair has been generated, no request can decrypt.
  3. Signature — it reconstructs the canonical string from trusted context and verifies the signature against the public key.
  4. Nonce — only after the signature is valid, the nonce is consumed. Nonces are single-use: reusing one (even from a request that failed) is rejected as a replay. Generate a fresh nonce for every request, including retries.
The nonce is consumed only after the signature verifies, so invalid signatures can’t exhaust the nonce space. If the replay store is unavailable, verification fails closed — the value stays ciphertext.

Worked example

Signing with tweetnacl in JavaScript (for example, in a Postman pre-request script):
Then run any read that returns the encrypted field. With a valid signature the field is plaintext; without it, the field is the QUVO… ciphertext.

CORS

If you call the API from a browser, the three decrypt headers must be allowed by CORS. The auto-generated GraphQL endpoint already allows them. For a browser app hitting the API directly, configure the allowed request headers under Backend → Settings → Network.

FAQ

The signature didn’t verify against the reconstructed canonical string. Check that the project ID, environment, method (query), timestamp, and nonce in your signed string exactly match what you sent in the headers — including newline characters and no trailing newline. Also confirm the timestamp is current and the nonce is fresh.
No. Nonces are single-use and are rejected on reuse as a replay — even if the first request failed. Generate a fresh timestamp and nonce for every request, including retries.
No. Authorization is fail-open to ciphertext: a missing or invalid signature returns the encrypted value, not an error. Only the data write path can error on encryption.