Encrypted transport
Reports often carry sensitive data — user emails and ids, and "super sensitive" code blocks. HTTPS protects that body from passive network sniffers, but it is still fully readable in the browser DevTools network tab, at any TLS-terminating proxy or CDN, and in access logs. BugBoard lets you encrypt the payload on the client so the body is an opaque blob everywhere on the wire.
In transit, not zero-knowledge. BugBoard decrypts the report on receipt to build the card, then dedupes, stores, and displays it as usual. Encryption protects the payload on the wire and in your logs/proxies — it does not hide the data from BugBoard itself.
How it works
Encryption uses a libsodium sealed box (crypto_box_seal, X25519) — anonymous
public-key encryption:
- Each project can hold one or more encryption keys (an X25519 keypair), separate from your API keys.
- The SDK encrypts each report with the project's public key. Because only the public key is needed to encrypt, it is safe to embed in client code — exactly like a publishable API key.
- BugBoard opens the sealed box with the matching private key, which is stored encrypted at rest and never leaves the server.
A fresh ephemeral key is used per message, so the ciphertext leaks nothing reusable. Encryption is optional and auto-detected: a project can mix encrypted and plaintext clients, and the server handles each correctly.
Generate an encryption key
Open your project → Settings → API Keys → Payload encryption (requires the
manageApiKey ability) and Generate key. You get:
- a public key (
base64) — configure this in your SDK; - a key id (
bbek_…) — public; tells the server which key to decrypt with.
The public key can be copied any time (it is not a secret). Revoke a key to stop it decrypting; revocation is immediate and per-key, so you can rotate without breaking other integrations.
Configure the SDK
JavaScript: the sealed box isn't in Node's built-in
cryptoor the browser Web Crypto API, so the SDK ships a tiny tweetnacl sealed-box binding (tweetnacl-sealedbox-js) and lazy-loads it automatically the moment you set an encryption key — there's nothing extra to install or import.PHP: the sealed box comes from libsodium, exposed by PHP's
sodiumextension. It must be installed and enabled on the host — see Enabling PHP's sodium extension below.
Set the encryption public key and every report is encrypted automatically:
import { createClient } from 'bugboard'; const bugboard = createClient({ // ... encryptionKeyId: import.meta.env.VITE_BUGBOARD_ENCRYPTION_KEY_ID, encryptionPublicKey: import.meta.env.VITE_BUGBOARD_ENCRYPTION_PUBLIC_KEY, });
use BugBoard\ClientBuilder; use BugBoard\Config; $bugboard = ClientBuilder::create(new Config( // ... encryptionKeyId: getenv('BUGBOARD_ENCRYPTION_KEY_ID'), encryptionPublicKey: getenv('BUGBOARD_ENCRYPTION_PUBLIC_KEY'), ));
Enabling PHP's sodium extension
Sealed boxes come from libsodium, which PHP exposes through its sodium
extension. The extension has been bundled with PHP since 7.2 — but bundled is
not the same as enabled. Most distributions ship it as a separate package,
so on a fresh server it is often simply not there, and encryption fails at the
first call:
PHP Fatal error: Uncaught Error: Call to undefined function sodium_crypto_box_seal()
You only need this if you turn encryption on. Plain reporting uses no crypto and needs nothing installed.
1. Check whether you already have it
php -m | grep sodium
If that prints sodium, you're done — skip to step 5 and verify. Nothing
printed means it isn't loaded.
2. Install the extension
Match the package to the PHP version you actually run (php -v):
Debian / Ubuntu
sudo apt update sudo apt install php8.3-sodium
RHEL / CentOS / Rocky / AlmaLinux / Fedora
sudo dnf install php-sodium
Alpine
apk add php83-sodium
macOS (Homebrew)
Homebrew's PHP already includes sodium, so there is usually nothing to do:
brew install php
Docker
On the official php images, libsodium's headers aren't in the base image, so
install them and compile the extension in:
RUN apt-get update \
&& apt-get install -y libsodium-dev \
&& docker-php-ext-install sodium
Windows
php_sodium.dll ships with the official builds. Nothing to install; go straight
to step 3.
Building PHP from source
Configure with --with-sodium.
3. Enable it
The Debian/Ubuntu and RHEL packages normally enable the extension for you. If
php -m still doesn't list it, load it explicitly.
On Debian / Ubuntu:
sudo phpenmod sodium
Anywhere else (and on Windows), add this line to php.ini — run php --ini to
find which file is in use:
extension=sodium
4. Restart the process that serves your app
The extension is loaded at startup, so a running worker won't pick it up:
sudo systemctl restart php8.3-fpm # PHP-FPM sudo systemctl restart apache2 # mod_php
The gotcha that wastes an afternoon. The CLI and FPM usually read different
php.inifiles.php -mtells you about the CLI only, so it can happily printsodiumwhile your web requests still crash. Check the runtime your app actually uses:php-fpm -m | grep sodium— or hit a
phpinfo()page and search for "sodium".
5. Verify a sealed box actually works
The real proof is a round trip, not just a loaded extension:
php -r ' $keypair = sodium_crypto_box_keypair(); $sealed = sodium_crypto_box_seal("hello", sodium_crypto_box_publickey($keypair)); echo sodium_crypto_box_seal_open($sealed, $keypair), PHP_EOL; '
If it prints hello, sealed boxes work and the SDK can encrypt. If it fatals on
an undefined function, the extension still isn't loaded in that runtime — go
back to step 4.
The envelope
Under the hood the SDK sends an envelope instead of the plaintext body. The sealed contents are the same fields you would otherwise send:
{ "encrypted": { "v": 1, "alg": "x25519-sealedbox", "key_id": "bbek_…", "ciphertext": "<base64( sealed_box( utf8_json_of_normal_body ) )>" } }
The full byte-level contract — the sealing algorithm to port into a custom client, and how encryption composes with request signing — is in API Reference §11.
The response is hidden too
Sealing the request would achieve little if the server then echoed the stored
card straight back in plaintext — the payload you just encrypted would be
readable in the network tab anyway, on the way back. So the SDKs send
X-Bb-Hide-Response by default and the server omits the card from the
response body entirely. The outcome flags (deduplicated, dropped, reason)
still come back, since the SDK needs them to decide whether to retry.
The header is deliberately not part of the body: it stays readable when the payload is encrypted, and it sits outside the HMAC signature, which covers the body alone.
FAQ
Does it work with publishable (browser) keys? Yes — that is the primary case. Encryption is asymmetric, so the browser only needs the public key. The bearer token stays visible in the network tab, but the request body does not.
Does it work with secret (server) keys? Yes. Encrypt first, then HMAC-sign the envelope you transmit. The signature covers the ciphertext, so nothing else changes.
What if I send a plaintext report? It is accepted. The server detects the
encrypted envelope and only decrypts when present.
What happens if decryption fails? A missing/revoked key or a tampered
ciphertext returns 422 with Could not decrypt the request payload.
PHP says Call to undefined function sodium_crypto_box_seal(). The sodium
extension isn't loaded in that runtime. It is bundled with PHP but usually
packaged and enabled separately — see
Enabling PHP's sodium extension. If php -m
lists it but your web requests still fail, you enabled it for the CLI only;
restart PHP-FPM and check php-fpm -m.
Is the payload hidden from BugBoard? No. BugBoard decrypts the report to render the card. Encryption protects the payload in transit, at proxies/CDNs, and in logs — not from BugBoard itself.