BugBoardDocs

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:

  1. Each project can hold one or more encryption keys (an X25519 keypair), separate from your API keys.
  2. 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.
  3. 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 crypto or 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 sodium extension. 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.ini files. php -m tells you about the CLI only, so it can happily print sodium while 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.