Skip to main content
The architecture · /security

What protects you, precisely.

Some promises here are properties of the system. The rest are in writing. We tell you which is which.

Document version 2.0Last reviewed 1 Jul 2026 · Next review 1 Jan 2027
The five guarantees
Guarantee 01

Your partner only ever sees what you both said yes to.

When both partners have finished, the comparison runs on our servers, in a privileged context neither browser can reach. What returns to your client — and to your partner's — is the intersection only: the things you both said yes or maybe to. Your unmatched answers are not styled and hidden, not blurred, not paywalled. They are simply never sent to your partner.

server-side reveal · what each client receives
// When both partners have submitted, on the server:
async function computeReveal(partnershipId) {
  // privileged server context — no client can reach into this
  const [a, b] = await loadBothResponseSets(partnershipId);

  const overlap = sharedQuestions
    .filter(q => isMutual(a[q.id], b[q.id]));
    // mutual = yes·yes, yes·maybe, maybe·maybe

  return overlap; // the only thing either client is ever sent
}
Guarantee 02

Encrypted in transit, encrypted at rest, scoped to your rows.

Your answers travel over TLS 1.2 or higher and live in Supabase Postgres, encrypted at rest with AES-256 — platform-managed keys, rotated by people whose whole job that is. On top of the encryption, row-level security scopes every user-facing query to your own rows: your session cannot read your partner's answers, or anyone else's. What we do not have is client-side encryption, and we will not pretend otherwise — the protections here are the boring, load-bearing kind.

row-level security · policy excerpt
-- row-level security · every user-facing table
alter table quiz_responses enable row level security;

create policy "own rows only" on quiz_responses
  for select using (auth.uid() = user_id);

-- in transit: TLS 1.2+ · at rest: AES-256 (platform-managed)
-- your session token cannot read your partner's rows
Guarantee 03

Export and deletion are one click, in Settings.

Neither requires emailing anyone. Export gives you an instant JSON download of everything we hold — profile, responses, matches, journal. Deletion is permanent and irreversible: it removes your auth user, cascades through every table that references you, and cancels your Stripe subscription as part of the same flow. Deleted data drops out of our daily backups as the 30-day retention window expires. There is no archive, and there is no “in case you change your mind” — if you change your mind, you sign up again.

settings · export and deletion, as shipped
// Settings → Export my data (instant JSON download)
GET /api/account/export
// → profile, responses, matches, journal

// Settings → Delete my account (permanent, immediate)
async function deleteAccount(userId) {
  await cancelStripeSubscription(userId);
  await auth.admin.deleteUser(userId); // cascades through every table
}
// Daily backups expire on a 30-day window. No archive.
Guarantee 04

We will never train AI models on your answers.

This is not a soft commitment we might revisit during a strategy offsite. It is a written restriction in our data processing agreement — and we want to be precise about what kind of promise it is: contractual, not cryptographic. We could not quietly change our minds; the clause survives termination and cannot be amended without notice to every active customer and an opt-out window. Your answers are product data, never training data.

policy clause · DPA §4.2
// excerpt from the BothWant Data Processing Agreement, §4.2
// available at /legal/dpa, executed by BothWant Editorial.

PROHIBITED USES. Processor shall NOT:
  (a) use Customer Data to train, fine-tune, evaluate,
      or otherwise improve any machine-learning model,
      whether owned by Processor or by a third party;
  (b) grant any license to Customer Data that would
      permit such training by a sub-processor;
  (c) retain or repurpose Customer Data beyond the
      operational purposes described in §3.1.

// This clause survives termination. It cannot be amended
// without notice to all active customers and an opt-out window.
Guarantee 05

We do not sell, share, or transfer your data to advertisers.

We have no advertiser SDKs in our codebase. No Meta pixel. No Google Ads tag. No TikTok pixel. No LinkedIn Insight tag. The only third-party scripts on bothwant.com are the ones we list publicly: Stripe (checkout), Resend (transactional email), and PostHog (product analytics with PII masking enabled). You can verify this. The build is open to inspection in the browser, and our sub-processors page lists every vendor we touch.

verification · grep for ad-network domains in codebase
$ rg 'connect.facebook.net|googletagmanager|doubleclick|tiktok|linkedin\.com\/insight' .
# (no matches)

$ rg 'pixel|fbq|gtag|ttq|_linkedin_partner_id' --type js --type ts
# (no matches)

$ cat package.json | jq '.dependencies | keys[]' | grep -i 'pixel\|analytics\|tracking\|ad'
"posthog-js" # the only match — and PII masking is enforced in posthog.config.ts
How it works

The architecture, step by step.

01 · Client (you)
Answers leave the browser over TLS.

You answer alone, in your own session. Everything travels to our servers over TLS 1.2+, behind strict security headers (CSP, HSTS) and an age gate at the door.

↪ browser → wire
02 · Database
Stored encrypted at rest, scoped by row.

Answers live in Supabase Postgres, encrypted at rest with AES-256. Row-level security means your session can read your rows and no one else's. Rate limiting sits in front of the API.

↪ wire → postgres
03 · Server-side reveal
The comparison runs in a privileged server context.

When both partners have answered, the server compares the two sets and computes the mutual overlap — yes-yes, yes-maybe, maybe-maybe. Neither client participates, and neither client can call into it.

↪ postgres → server
04 · Returned matches
Only the overlap comes back.

Each partner's client receives only the intersection. Your unmatched answers — yours alone, the embarrassing ones — are never sent to your partner. They are not in the payload at all.

↪ server → both partners
Threats this design defends against
Your partner's curiosity

The threat model couples actually care about. The reveal runs server-side and returns only the mutual overlap — your unmatched answers are never in your partner's payload. There is nothing to find in dev tools, because nothing was sent.

Database breach

Data is encrypted at rest (AES-256) and in transit (TLS 1.2+). Passwords exist only as salted hashes held by our auth provider. Card numbers never touch our servers — Stripe holds those. Row-level security and rate limiting narrow what any single stolen credential could reach.

BothWant employee curiosity

There is no dashboard where any of us — engineer, support — can browse what couples have said yes to. Production access is restricted to operating the service, and the DPA above makes “we don't look” a contractual obligation, not a mood.

What this is not

We are not making you safer than you already are.

A few honest limits. We do not replace a therapist's judgment, and we cannot recognize a pattern that requires one. We do not prevent harm in relationships where harm is already happening; we do not detect coercion through a quiz. If a partner pressures another into using BothWant, the architecture cannot tell.

We are a US software company subject to US legal process, and we will respond to lawful subpoenas the same way every other US software company does. What limits what we can be compelled to produce is not cryptography — it is minimalism. We collect little, we keep it only while you keep an account, and deletion is real, immediate, and ages out of our backups within thirty days.

Si algo en esta página está significativamente mal, queremos saberlo. Hay un camino para reportar vulnerabilidades a continuación.