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):
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:- Timestamp skew — the timestamp must be within the allowed window of the server’s clock. A stale or future-dated timestamp is rejected.
- Public key — the server loads the environment’s public key. If no key pair has been generated, no request can decrypt.
- Signature — it reconstructs the canonical string from trusted context and verifies the signature against the public key.
- 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 withtweetnacl in JavaScript (for example, in a Postman pre-request script):
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
Why did my field come back as ciphertext even though I sent a signature?
Why did my field come back as ciphertext even though I sent a signature?
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.Can I reuse a nonce or a signature?
Can I reuse a nonce or a signature?
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.
Does a failed decrypt return an error?
Does a failed decrypt return an error?
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.