Security

What we hold, and what we deliberately do not

Trailhead handles ad-click evidence and revenue records, which means it sits close to customer identity and close to money. This page is the reference for how that is handled. Every item traces to code rather than to intent.

For the design reasoning behind these choices, read the whitepaper.

Identifiers

Trailhead matches people across systems, which normally means holding a pile of email addresses and phone numbers. It does not.

  • Hashed at ingress. Email is lowercased and hashed with SHA-256. Phone is normalized to E.164 and hashed with SHA-256. This happens at the capture boundary, before storage. Plaintext identifiers do not persist.
  • Matching runs on hashes. The stitch engine compares hashes, so there is no point in the pipeline where the plaintext is needed again.
  • Raw bodies are not kept. The audit archive is an allowlist projection of the fields the receiver understands, with the identifier sources redacted. A webhook payload containing something unexpected does not quietly end up in storage.

Capture boundary

Capture runs on a subdomain you own, as a small Cloudflare Worker, with a direct-post fallback if you would rather not deploy one.

  • No cookies are set by the worker or by the snippet.
  • The worker logs nothing and answers with an empty 204. The page never sees the receiver's internals.
  • Landing URLs are stored without their query string, so session tokens, personal data and anything else that rides in a URL do not persist.
  • Ad-click context lives in the visitor's own sessionStorage so it survives navigation. Trailhead plants nothing of its own on the device.
  • A click with no joinable signal is not stored at all. There is no value in a row that can never join to anything, and keeping it would be collecting for its own sake.
  • Lead webhooks are HMAC-signed and deduplicate at the database, so a replayed request cannot double-count.

Tenant isolation

Every query is scoped to a workspace through a single dependency, so isolation is one thing to review rather than a property each endpoint has to remember.

Cross-tenant access returns 404, not 403. A 403 confirms the resource exists and belongs to someone else, which turns any endpoint into an existence oracle. A 404 says nothing.

The test suite runs against both SQLite and PostgreSQL, currently 323 tests, because a query that isolates correctly on one engine and not the other is exactly the bug this discipline exists to catch.

Authentication and roles

  • argon2id password hashing, with one shared configuration across registration, member invite and password reset.
  • Sessions are database-backed and revocable. The cookie carries a random token and the database stores only its SHA-256, so a database read does not hand over live sessions.
  • Uniform failure responses on login and registration, with rate limiting, so neither timing nor wording reveals whether an account exists.
  • Four roles: owner, admin, analyst and viewer, with guards against escalating your own role and against removing a workspace's last owner.
  • Password reset tokens are single-use and claimed atomically, so a token cannot be redeemed twice in a race.

Credentials at rest

Ad-platform connector secrets and webhook signing keys are encrypted at rest with Fernet, using a key supplied by the environment and held outside the database. Key rotation is supported: retired keys stay decrypt-only, so a rotation does not require a migration window.

Credentials are write-only from the API's point of view. You can set one and you can replace one. No endpoint returns a stored secret, so there is no path by which a compromised session leaks your ad-account credentials.

Write-back blast radius

Write-back is the only part of Trailhead that can affect something outside your own workspace, so it carries the most gates.

  • Sandbox by default. Payloads are assembled, recorded and auditable, and nothing is transmitted.
  • Two independent switches before anything sends. One per deployment, naming which platform senders are verified, and one per workspace, recording that this specific customer is cleared. Either one absent means sandbox.
  • Approval binds to content. The approval covers the plan's hash and the connector-config digest together. Change the dataset, the account or the conversion action and the approval is void rather than stale.
  • Idempotency is permanent. Every conversion carries a per-outcome key in a durable ledger, so retries and re-runs cannot duplicate.
  • Every exclusion carries its reason, so a plan that drops most of a batch tells you why before you approve it.

Erasure and consent

  • Per-person erasure. A right-to-be-forgotten operation removes an individual's records across the pipeline.
  • Full workspace purge, behind a typed-name confirmation, for when an engagement ends and you want the data gone rather than dormant.
  • Consent Mode v2 signals for ad storage, ad user data and ad personalization are captured with every click and normalized to granted, denied or unset, then carried through the pipeline rather than dropped at the door.

Deployment posture

  • The production preflight refuses to boot when the secret key is missing, the admin key is under 24 characters, cookies are not marked secure, or the database is SQLite rather than PostgreSQL. A misconfigured container fails loudly instead of running insecure or on storage that vanishes.
  • Migrations run before the app starts, and the app refuses to boot if the live schema is behind the code, so a stale database fails once rather than producing errors all day.
  • Single image, non-root user, with the API and the built webapp served same-origin so there is no cross-origin surface to configure wrong.
  • Self-hosting is available on the enterprise plan if your situation calls for the data staying on infrastructure you control.

Known gaps

A security page that lists only strengths is marketing. These are the current gaps, and they are the same ones named on the homepage.

  • Single sign-on is not shipped. OIDC is on the enterprise track and gets scoped with you. Access today is password plus role.
  • Team invites do not go out by email. An invite produces a one-time temporary password that you deliver through a channel you trust. Email delivery is coming.
  • No formal certification. There is no SOC 2 report to send you. The controls on this page are real and reviewable in code, and an auditor has not reviewed them.
  • No published penetration test. The codebase has been through repeated adversarial review internally, including the findings recorded in the commit history, and that is a different thing from an external test.

Reporting something

If you find a security issue, mail info@sasquatchcreativetx.com with "security" in the subject and we will come back to you. We would rather hear it from you than read about it.