Tombstone
A personal data command center. Multi-user Google OAuth (narrow gmail.metadata scope: headers, never bodies) discovers every account tied to your Gmail, cross-references breach data against LeakCheck and HaveIBeenPwned, flags forgotten "zombie" accounts, and uses AI agents (Groq Llama 3.3 70B) to draft CCPA / GDPR / DPDP deletion letters. Every letter stays a draft until the user approves it under explicit Active Consent, then hands off to their own Gmail compose window, or to the company's privacy webform when no address is published. The whole thing is a mobile-responsive multi-page app, and a no-login public breach checker leads onboarding so value comes before the OAuth ask.
Note: Google OAuth is in testing mode while the app goes through Google's production verification and CASA Tier 2 security assessment, so login currently requires test-user access - the public breach checker and the /demo product tour work for anyone, no login required.
Architecture
How it works
The home page opens with a no-login breach check: enter any email, tick consent, and it's checked against LeakCheck and HaveIBeenPwned with a per-breach risk score - real value before any OAuth ask, with Gmail offered as an upgrade. Plain-language privacy promises (headers only, never bodies; disconnect erases everything) and an expandable preview of Google's exact consent screen sit right on the page, backed by a dedicated /privacy page. The product is a mobile-responsive multi-page app: a marketing site (Home, How it works, About, FAQ, Privacy) plus a server-guarded app (Dashboard, Accounts, To-do, Profile), with a no-login /demo.
Users sign in with Google OAuth (testing mode), granting the narrowest gmail.metadata scope - headers and labels, never message bodies. Refresh and access tokens are encrypted with Fernet (key from TOKEN_ENCRYPTION_KEY) and stored per-user in Postgres - never on disk, never in the cookie, which holds only a non-enumerable UUID user_id. The display name is read locally from the id_token (no extra API call), and logout revokes the token directly with Google.
/api/discover writes a row to a Postgres jobs table and returns immediately. worker.py polls with FOR UPDATE SKIP LOCKED, claims one job at a time, and retries up to 3x on failure. Jobs survive process restarts and redeploys - the core fix over the v1 design, where a daemon-thread scan simply vanished if the process restarted mid-run.
The worker reads only From, Subject, and Date headers - never bodies - through gmail_client.py, the only module allowed to import googleapiclient. A Quick scan caps recent mail (newest-first) to stay inside the access-token lifetime; a Deep scan walks the whole inbox; a Stop button cancels mid-run and keeps what was found. Root-domain merging (bigbasket.in + .com), sending-subdomain collapse (send.grammarly.com → grammarly.com), a 2+ email threshold, and personal/finance/infra exclusions produce a clean list. Re-scans are incremental via a per-scan watermark, filtered client-side because the metadata scope forbids Gmail's q/after: search.
With explicit consent, the user's own email (and only their own) is checked against LeakCheck and cross-referenced with the HaveIBeenPwned breach catalog, cached in a shared Postgres api_cache table (24h TTL, 5-min failure cooldown) so the web and worker processes share one freshness clock. Accounts with no activity in 180+ days (six months) are flagged "zombie"; breached services the user never emailed are injected into the dashboard so nothing slips through.
A single call_llm() wrapper - originally spec'd for the Claude API, swapped to Groq's Llama 3.3 70B behind that one function for a free tier - powers four pipelines: domain classification (batched 25 at a time, cached globally), a 20-minute breach action plan ranked by current risk, deletion letters whose statutes are hardcoded templates (CCPA §1798.105, GDPR Article 17, India's DPDP Section 12, or statute-free generic wording) with the LLM writing only the context paragraph and post-checked so it can never invent a citation, and step-by-step deletion walkthroughs from a vendored JustDelete.me dataset (2,543 services). Every pipeline has a non-AI fallback, so the app degrades gracefully with no API key.
Deletion letters are always written as drafts first - status='draft', consent_given_at NULL, nothing enqueued. Only an explicit "Approve" click stamps the consent timestamp, appends to an atomic append-only audit log, and enqueues a deletion_request job - which the worker refuses to process if consent is missing, as a permanent failure rather than a retry. The full automated send pipeline is built and tested: an atomic approved -> sending claim that makes a duplicate legal letter impossible across crashes and retries, reply tracking (headers only, with bounce and auto-reply filtering) against the statutory window (CCPA +45 days, GDPR +1 month), and escalation letters that are always drafted, never auto-sent.
That pipeline is deliberately dormant in production. The gmail.send grant is gated behind an env flag that is off, so Google's verification submission covers exactly one restricted scope (gmail.metadata) and no sensitive scope at all. The live path is a manual handoff: the approved letter opens prefilled in the user's own Gmail compose window and they press send. Fewer permissions asked for, and the user sees the letter before it leaves. Re-enabling automated sends later costs a re-verification, not a second security assessment.
A drafted letter is worthless without a destination, and only 466 of the 2,543 directory entries publish a privacy address. So resolution is a cost-ordered cascade, cheapest first, stopping at the first hit: the vendored directory (free, offline, in git), then a global Postgres cache shared across all users, then a website scrape on a hard 6-second budget so a click can never hang, then an LLM recall pass, and only as a last resort a paid web-search tier at roughly a cent per domain. The first user to retire an uncovered company pays discovery once; everyone after reads the cache.
Two details that matter more than the cascade. Misses are cached honestly: a null result is only persisted when a tier actually ran and none errored, so an outage or a missing key never poisons the cache for a fortnight. And no address is ever invented - results are validated for shape and same-organization domain, because a plausible-looking wrong address is worse than an empty field. When there is genuinely no published email, the modal falls back to a vendored directory of 195 privacy webforms (Meta, LinkedIn, Microsoft and PayPal all accept requests only this way), and failing that, a targeted search. The modal always offers exactly one primary action, re-picked live as the user edits the recipient.
Technologies
Feedback
Tombstone is actively evolving. If you've tried the demo or have thoughts on the breach-check flow, a couple of minutes here genuinely steers the roadmap.