Security & trust modelaudited · isolated · documented

Private by policy by default.
Zero-knowledge by construction when you opt in.

Your CV is the most sensitive document in your inbox. Your hiring plan is the most sensitive document in your company. Two modes ship today: Standard — encrypted at rest with a server-held key, convenient, the default — and Max-Privacy — passphrase-derived DEK that lives only in your browser, server stores ciphertext only. You pick during onboarding; switching costs a re-encrypt of your columns and is reversible.

Standard: AES-256-GCM at rest · server-keyed · TLS · RLSMax-Privacy: client-side DEK · PBKDF2-SHA256 · AES-256-GCMSOC-2 Type I: on the post-launch roadmap
01 Three promises

Three things we don't do — and have built the system so we can't accidentally start.

we don't

Browse your profile.

Sensitive fields are encrypted at rest with AES-256-GCM. Application code paths that touch your data run inside a per-account database role; cross-tenant reads are physically rejected at the SQL layer by row-level security policies.

// app role · account_id GUC not set
SELECT profile FROM users WHERE account_id = 'other';
// returns
0 rows — RLS rejected
we don't

Read your reach-outs.

Messages between candidates and employers ride the same per-account isolation. They're encrypted at rest, accessible only to the conversation participants, and every read is logged. The server brokers delivery and signs the audit trail.

// audit log entry
{
  action: "message.read",
  actor: "profile#4821",
  trace_id: "0x9f3e…"
}
we don't

Sell your data.

The whole resume-mill industry runs on this. We don't, won't, and the export endpoints physically don't exist in the codebase. It's also written into our privacy policy as an irreversible commitment. You can audit the API surface yourself — we're open about what tools the system exposes.

// data broker request
GET /export?candidates=*
// our response
HTTP 404 · route does not exist
02 Architecture today

Three zones. One direction of trust.

Plaintext lives in three places in Standard mode: your browser, our server (where it's re-encrypted at rest), and a scoped Claude call. In Max-Privacy mode (described below), the server tier holds ciphertext only — plaintext lives in your browser and the inference call.

zone 01 · client
Your device
Browser session over TLS · authenticated
  • CV uploaded over TLS · parsed server-side
  • Auth.js session cookie (HttpOnly, Secure)
  • MCP tools available via OAuth or bearer token
  • Audit trail visible in Settings → Security
plaintext · in transit only
zone 02 · server
hunt.work API
eu-west · Postgres + RLS · encrypted at rest
  • Sensitive fields AES-256-GCM at rest
  • Postgres row-level security per account_id
  • Application role can't bypass RLS
  • Every mutation written to audit log
isolated · audited
zone 03 · compute
Claude inference
Anthropic API · plaintext within a single scoped call
  • Profile slices sent per call, not in bulk
  • Score & reasoning returned · logged
  • Anthropic enterprise terms: no training on your data
  • Every call traced with a stable trace_id
plaintext · scoped & logged
01 / receive

Over TLS

Browser → server via TLS 1.3. Authenticated session.

02 / store

Encrypted at rest

Sensitive columns AES-256-GCM. Master key from AUTH_SECRET.

03 / isolate

RLS-enforced

Per-account row policy. App role can't read other tenants.

04 / audit

Every mutation

Trigger-written log row visible in your account.

03 Cryptographic details · today

The boring, vetted primitives.

No homegrown crypto. Standard library primitives with conservative parameters. What's listed below is the Standard-mode defaults shared by every account — the Max-Privacy section after describes how the same primitives shift from server-keyed to browser-keyed when you opt in.

at-rest encryption · live

AES-256-GCM, authenticated.

Sensitive columns (CV text, hunt narratives, snapshot bodies, messages) are encrypted with AES-256-GCM before they hit the database. The master key is derived from a long-lived server-side secret (AUTH_SECRET). Tamper-evident — modified blobs fail to decrypt.

// inside the server, before INSERT
const ciphertext = aesGcm.encrypt({
  key: masterKey,
  iv: random(12),
  aad: "account:" + accountId,
  plaintext: profileBytes
});
tenant isolation · live

Row-level security in Postgres.

Two database roles: job_seeker (migrations, bypass) and job_seeker_app (the role the live request runs as). Every account-scoped table has a policy account_id = current_account_id(). If the request-bound GUC isn't set, queries return zero rows — by Postgres, not by code.

// every account-scoped table
CREATE POLICY tenant_isolation ON hunts
USING (account_id = current_account_id());

// request-bound — server-side
SET LOCAL app.account_id = '…';
SET LOCAL ROLE job_seeker_app;
parameter reference · today

Specs at a glance.

Symmetric cipherAES-256-GCM (AEAD)
Master keyDerived from server-side AUTH_SECRET · rotation procedure documented
Tenant isolationPostgres row-level security · 20-table corpus asserted in test:rls
TransitTLS 1.3 via Cloudflare Tunnel · zero-trust posture in front of Coolify-on-bare-metal
Audit logTrigger-written on every account-scoped mutation · visible to you · 30 days free / 12 months Premium
SessionAuth.js database sessions · HttpOnly, Secure cookie · MCP via OAuth 2.1 or scoped bearer
Untrusted-content boundaryThree-layer scrub (HTML, control-char, prompt-injection patterns) on every third-party body — listings, emails, scraped pages
ImplementationNode 22 stdlib (crypto.subtle) · no homegrown ciphers
04 Max-Privacy mode · shipped & opt-in

True zero-knowledge — when you turn it on.

The default is Standard mode (above) because matching, scoring, and snapshot generation all need plaintext during the inference call — convenient for new users. Max-Privacy mode is the strict-no-server-access path: you set a passphrase during onboarding, your browser derives the DEK, and the server only ever sees ciphertext. There's no admin override — and so no court can compel one. Switching from Standard → Max-Privacy re-encrypts your columns in a single transaction; switching back wipes the canary and restores server-keyed mode.

max-privacy · key derivation

Passphrase → key, derived on your device.

PBKDF2-SHA256 with 200,000 iterations over passphrase + per-account salt produces a 32-byte DEK in browser memory. We never see the DEK itself. A canary blob proves the unlock succeeded without leaking the DEK. If you forget the passphrase, the 24-character recovery code can re-derive it; if you lose both, the data is unrecoverable — there is no key escrow.

// inside your browser · WebCrypto SubtleCrypto.deriveBits
const dek = await crypto.subtle.deriveBits({
  name: "PBKDF2",
  hash: "SHA-256",
  salt: account.salt,
  iterations: 200_000
}, passphraseKey, 256);
max-privacy · symmetric cipher

AES-256-GCM, client-keyed.

Same AES-256-GCM you'd get in Standard mode — only the key is now in your browser, not on the server. Profiles, hunts, snapshots, and proof-reader traces get sealed before they leave your device. The server stores ciphertext + IV + AAD only. Scoped Claude calls decrypt at request time inside the inference call and discard plaintext after.

// in your browser · before the request leaves
const ciphertext = await crypto.subtle.encrypt({
  name: "AES-GCM",
  iv: random(12),
  additionalData: "account:" + accountId
}, dek, profileBytes);
// upload ciphertext + iv + ad only

Max-Privacy is opt-in for a reason — losing the passphrase means losing access to your encrypted data. We mitigate with a one-time 24-char recovery code shown at enrollment (saved offline by you), and a deliberate type-to-confirm data-wipe path with a 7-day cooldown if you really have lost both. If zero-knowledge is a hard requirement for choosing a hiring tool, pick this mode at onboarding.

05 Audit log

Every mutation, visible to you.

You don't have to trust us when we say "we don't read it." You can see every action the system took on your account. Trigger-written from the database, exported on demand.

What gets logged. Without exception.

Every action that touches your account — match scores, snapshot generations, reach-outs, profile edits — produces an audit entry visible in Settings → Security. The log row is written by a Postgres trigger, not by application code, so it can't be skipped.

  • Match scores — when Claude is given a slice of your profile to score against a role
  • Snapshot generations — when your profile is used to compose a tailored application
  • Sessions — every sign-in, every device
  • Profile edits — anything you (or Claude on your behalf) change about your profile
  • Mutual-yes events — when masks drop on either side of a reach-out

Exportable as JSON via the right-to-access endpoint. The audit-log-coverage test asserts that every mutation path writes a row — if a developer forgets, CI fails.

audit-log · account #4821 · last 60s
14:02:31
matchscore_hunt_match(hunt#eu, listing#sg) → score 9.2
14:02:18
readextract_listing(linkedin · 6 new) no profile access
14:01:45
writesnapshot.create(sourcegraph#4821) cv + cover · proof-read · 920ms
14:00:12
authsession.start device · Auth.js verified
13:58:33
matchscore_hunt_match(hunt#eu, listing#vc) → score 7.4
13:58:11
writeprofile.update(style.async) → true
13:55:02
readscan_source(rss · 0 listings)
trace id 9f3e4b2a8c1d…
written by trigger:audit_log_writer
last fetched 14:02:34 · 3s ago
06 Compliance & audits

Where we are, plainly.

No vague "enterprise-grade" claims. Just the current status of each certification, with dates and links where applicable.

live
CCPA + GDPR

US-based operator with EU sub-processors. CCPA / CPRA right-to-know / delete / correct + GDPR Art. 15-22 rights — endpoints live today. Deletion is real (cascades from accounts, not a soft flag). DPA available on request.

Read your rights →
on the roadmap
SOC-2 Type I

Not contracted yet. We'll engage an auditor and publish the window once the company is incorporated and the audit firm is signed. Customers who need an attested report should email [email protected].

on the roadmap
Third-party pentest

Not contracted yet. A scoped external audit of the auth surface, RLS isolation, and the MCP boundary is on the post-launch roadmap; findings will be published when complete.

live
Open compute

Backend runs on inspectable infrastructure (Coolify-on-bare-metal behind Cloudflare Tunnel). The MCP tool list is publicly enumerable via the discovery endpoint — no closed-box AI ops.

Discovery endpoint →
07 Security FAQ

Questions security teams ask, before legal does.

Got more? Write us — [email protected]. PGP available on request.

01

If you can't read my data, how does Claude do the matching?

In Standard mode (the default), yes — the application has access to the master key, so a determined operator could decrypt your data. We mitigate with per-account row-level isolation and audit logging on every mutation. In Max-Privacy mode, no — the DEK is derived from your passphrase in your browser and never leaves it. Pick during onboarding; switching costs a single re-encrypt and is reversible.

02

What if I lose access to my email?

Today: standard Auth.js magic-link recovery via your email provider. Once we ship client-side key derivation, your email becomes the seed for your DEK — and that recovery story changes. We'll surface the trade-off clearly when that lands.

03

What about employer data?

Identical model. Employer accounts get their own per-account row-level policies and audit log. Match-time decrypt happens in the same scoped Claude call structure. Same SOC-2 roadmap posture (not contracted yet).

04

What about Claude? Does Anthropic see my data?

Inside the scoped match/snapshot call, Anthropic's inference servers process the relevant plaintext slice — that's how the LLM does the work. The call is scoped (just what's needed), short-lived, and logged on our side. Anthropic's enterprise terms apply: no training on user data, retention per the agreement.

05

Can a court compel you to hand over my data?

In Standard mode: yes — we honor lawful requests with the data we hold. In Max-Privacy mode: we'd hand over ciphertext + the audit log; we don't hold the key. We commit to publishing aggregate transparency reports quarterly once we reach 100+ paying users.

06

What's the threat model?

We protect against: cross-tenant leakage (RLS, asserted by tests), drive-by SQL injection (parameterized queries throughout), prompt-injection in scraped content (three-layer sanitization), passive network attackers (TLS), and unauthorized API access (Auth.js sessions + OAuth-issued MCP tokens with audit trails). In Standard mode we do not protect against a compromised hunt.work server reading your data; in Max-Privacy mode we do — the server holds ciphertext only.

07

Have you been audited?

External pentest scheduled Q4 2026. Internal: the RLS isolation test (npm run test:rls) is run on every PR — 20-table corpus asserted to return 0 rows under the app role when the account context isn't set, plus cross-tenant UPDATEs are asserted to fail with SQL state 42501.

08

Bug bounty?

Informal today — write us at [email protected] and we'll work out a payout for valid findings. A formal program is on the roadmap.

Private by policy by default. Private by construction when you opt in.

Standard mode is the default — encrypted at rest, isolated by RLS, audited end-to-end. Max-Privacy mode (one click during onboarding, or any time later) shifts the DEK to your browser so the server holds ciphertext only. Both models ship today.

hunt.work — security & trust model