Skip to content
For LLMsView as Markdown·

Security and data handling ​

This page describes only what the OneHazel platform code does today. Where something is partial, it says so.

Encryption at rest ​

DataHow it is stored
Connection credentials (the API keys and secrets you give us for suppliers)Encrypted with AES-256-GCM using a random 96-bit IV per encryption. The key is derived per operator account with HKDF-SHA-256 from a master key, so each account's credentials are encrypted under a different key.
Personal data fields in ingested player recordsEncrypted with AES-256-GCM under a separate per-account key (a different HKDF domain from credentials), for the fields your data template marks as personal data.
Your OneHazel API keysNot stored. We keep only a SHA-256 hash of each key. The full key is shown once, when you create it.
Webhook signing secretsPartial. Each subscription's signing secret is stored encrypted (AES-256-GCM, its own key domain), and a plaintext copy is also still stored while a migration completes. Treat webhook signing secrets as not yet covered by encryption at rest.

The credential, personal-data and webhook keys are derived under different HKDF salts, so a key for one kind of data cannot decrypt another.

Tenant isolation ​

  • Every request is tied to one operator. Authentication resolves your API key or session to a single operator account, and API handlers scope reads and writes to that account.
  • Database row-level security. Operator-scoped tables are protected by Postgres row-level security keyed to the current operator.
  • No cross-account probing. Asking for another account's connection by id returns 404 Not Found, the same as an id that does not exist, so ids cannot be used to discover other accounts.
  • Supplier URLs are checked. The gateway refuses to call a supplier URL that resolves to a private or internal network address (SSRF_BLOCKED).

Privacy requests (GDPR) ​

RequestEndpointWhat it does
Export (portability)GET /onehazel-api/operators/me/exportReturns your account record, connections (without credentials), webhook subscriptions (without signing secrets), workflows, and your player data with personal-data fields decrypted. Recent activity is included up to a cap: the latest 1,000 events, 1,000 gateway log entries and 100 jobs. Player data is capped at 2,000 rows per section, with a total count and a ...Truncated flag so a cut is never silent. If the player-data read fails, the whole export fails with EXPORT_INCOMPLETE instead of returning a partial export.
Erase your accountDELETE /onehazel-api/operators/meAnonymises your account record, revokes all API keys, clears stored connection credentials and deactivates connections, webhooks and workflows. It also drops your private analytics schema, with the player data stored in it, if your account has one. A failure of the schema step is logged, not returned.
Erase one playerDELETE /operator-data-api/entities/:externalIdReplaces the player's stored data with a deletion marker, redacts their event data and deletes their state records. If any step fails, it returns ERASURE_INCOMPLETE (and the request is safe to repeat) rather than reporting success.

Account erasure anonymises your account in place rather than deleting it. Player data stored outside a private analytics schema is not erased by this call: erase those players with the per-player endpoint first.

Webhook signatures ​

Webhooks we send you ​

Every delivery is a POST with these headers:

HeaderValue
X-OneHazel-Signaturesha256=<hex>: HMAC-SHA256 of the raw request body, keyed with the subscription's signing secret
X-OneHazel-EventEvent type
X-OneHazel-DeliveryDelivery id

To verify, compute the HMAC over the exact bytes you received (before any JSON parsing), prefix sha256=, and compare with a constant-time comparison. The body includes a timestamp field you can use to reject old deliveries. The signing secret is shown once, when you create the subscription.

ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyOneHazelSignature(rawBody: string, header: string | null, secret: string): boolean {
  if (!header) return false;
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(header);
  return a.length === b.length && timingSafeEqual(a, b);
}

Webhooks you send to a workflow ​

If you set a verification secret on a workflow's webhook trigger, inbound requests must carry X-OneHazel-Signature: sha256=<hex> computed the same way. Requests with a missing or wrong signature are rejected. Without a verification secret, inbound webhook triggers are not signature-checked.