Data privacy & encryption
Forward responses without storing them, and end-to-end encrypt so no one β not even Halloform β can read them.
Two privacy modes#
Both are opt-in per form and live under Settings β Behavior. They can be combined for maximum privacy.
- Don't store responses β responses are forwarded to your connected destinations and never kept on Halloform.
- Encrypt responses (end-to-end) β responses are locked in the respondent's browser with your public key; only you can read them, and Halloform never sees the plaintext.
Don't store responses#
When on, a completed response is sent straight to your connected destinations (Google Sheets, Notion, a webhook, Slackβ¦) and is then purged β nothing is kept in Halloform's database. Because there's no stored copy, these responses won't appear in Results; they live only in your own tools.
End-to-end encryption#
Encryption uses public-key cryptography (a libsodium sealed box). You hold a keypair: the PUBLIC key is stored on the form and used to encrypt; the PRIVATE key is yours alone and decrypts. The respondent's browser encrypts every answer before it leaves the page, so Halloform's servers only ever receive ciphertext β we can't read your responses, and neither can anyone who breaches us.
- Encrypted responses can only be sent to a webhook β a Sheet, Notion, or email would just hold unreadable ciphertext.
- You (or your developer) decrypt the ciphertext on your own server with your private key.
- It pairs with Don't store: nothing readable is ever kept on Halloform.
Enable encryption#
- Settings β Behavior β Encrypt responses β Enable encryption.
- Generate & download key (recommended): a keypair is created in your browser, your private key downloads as a file (save it!), and the public key is stored on the form. Or choose Advanced to paste a public key you generated yourself offline.
- Turn on Don't store responses and connect a webhook in Integrations β encrypted responses are webhook-only.
Decrypt on your webhook#
Your webhook receives a JSON body with an `encrypted` field (base64 ciphertext) instead of readable answers. Decrypt it with libsodium and your private key:
import sodium from 'libsodium-wrappers';
await sodium.ready;
const PRIVATE_KEY = process.env.FORM_PRIVATE_KEY; // base64, keep secret
const PUBLIC_KEY = process.env.FORM_PUBLIC_KEY; // base64
function decrypt(encryptedB64) {
const cipher = sodium.from_base64(encryptedB64, sodium.base64_variants.ORIGINAL);
const pk = sodium.from_base64(PUBLIC_KEY, sodium.base64_variants.ORIGINAL);
const sk = sodium.from_base64(PRIVATE_KEY, sodium.base64_variants.ORIGINAL);
const opened = sodium.crypto_box_seal_open(cipher, pk, sk);
return JSON.parse(sodium.to_string(opened)); // β the answers object
}
// app.post('/webhook', (req, res) => {
// const answers = decrypt(req.body.encrypted);
// res.sendStatus(200);
// });Important notes#
- Encryption is a developer feature β decrypting requires running the snippet above on your server.
- Encrypted forms don't work with features that need to read answers server-side: analytics, search, Sheets/Notion, quiz scoring, PDF fill. Use plain fields + a webhook.
- Rotating the public key only affects new responses; older ones stay encrypted to the previous key.
- The private key is never sent to Halloform in either the generate or paste flow.