Ayush · Yadav the record room · case file 4 of 7

¶ case file 4 of 7 · applied — filed 2026-02 · last verified 2026-08

status: shipped — live in invite-only beta; a restricted gmail scope caps the app at 100 test users, and the hosted classifier runs layer 1 only

consigned at applied — ¶ 04 · 08:47 · waybill: sorted mail → manifest

Applied

A job tracker that reads the search out of the inbox. Connect Gmail and Applied fetches your mail, names each message, and turns the noise into a pipeline of real applications you can act on. It runs as a hosted web app, live in invite-only beta — and the demo and the on-device import need no invite and no account.

role
designer and sole engineer — web app, backend, and classifier
timeframe
2026-02 to present
stack
Next.js 16 · TypeScript · PostgreSQL · Python · SetFit · ONNX Runtime
system card
/system-card ↗

[ problem ] · § as found

The status of a job search scatters across Gmail, employer systems, and one-off messages. A spreadsheet can’t keep up: updates get missed, rows get retyped, and the record drifts from the truth. The first answer was a desktop app, which meant the record only existed on one machine.

constraints —

  • Ask for the least Gmail access that can work: read-only, and keep the message body out of the database.
  • Classify noisy inbox messages into useful application states.
  • Make row isolation the database’s job, not the query writer’s.
  • Fit the whole classifier into a serverless slot, or be honest about which layers didn’t fit.
  • Never let a routine sync delete an application the current scan happened to miss.
inboxrulese5 similaritygated setfitappliedinterviewrejectedneeds-review

fig. 1 — the sorting line, settled: rules ⟶ e5 similarity ⟶ gated setfit. the clay gate is where the model defers to review.

a drawn plate, at rest — not a screenshot. the gates are the repository’s real three-layer path; the marks are illustrative and carry no counts. the measured numbers live in the receipts below.

[ architecture ] · § fig. 2, inked

Gmail hands over the message, the classifier names it and the body goes no further, Postgres files the verdict under an identity the database itself checks, and a Next.js dashboard reads it back. One backend package serves the whole path; the two heavier classifier layers run outside it, in the browser export, because they do not fit the serverless slot.

Gmail

gmail.readonly — nothing wider

read-only

Message fetch

Subject, From, Date, snippet, and a body it never stores

subject, snippet, body

Classifier

Rules on the hosted path; e5 + SetFit in the export

verdicts, scoped by user

Postgres

RLS on, FORCE’d, per-transaction JWT claims

rows the role may see

FastAPI

One package, deployed as a serverless function

pipeline state, over the caller’s jwt

Next.js

Pipeline board, review queue, stat tiles

no identity bound, no rows — the guc is unset and rls denies

fig. 2 — the pipeline, inked. clay marks the gate: where a check can stop the run.

[ decisions ] · § as filed

d1 — fetch gmail metadata, never message bodies · superseded

Filed 2026-07-26: the subject line, the sender, and Gmail’s own snippet are enough to name an application email.1

1 tradeoff — A body-blind classifier gives up signal on ambiguous mail, and buys a privacy boundary that holds without being trusted.

d2 — run the hosted classifier on the rules layer alone · accepted

torch, sentence-transformers, and SetFit do not fit a serverless function slot — not the size limit, and not the cold start.2

2 tradeoff — The hosted verdict is weaker than the full cascade. The alternative was pretending otherwise, so the limit is written into the code, the tests, and the boundary rows below.

d3 — enforce row isolation in postgres, not in the handlers · accepted

Application-level `WHERE user_id = …` is one forgotten clause away from a leak.3

3 tradeoff — Every transaction pays a `set_config` round trip, and the app must run as a role that cannot bypass its own policies.

d4 — make routine sync additive; keep destruction behind a button · accepted

A bounded scan of a large inbox will miss applications it already found, and re-syncing wiped them.4

4 tradeoff — Stale rows survive until an explicit rebuild, which is the cheaper failure.

d5 — read the message body in flight, and store none of it · accepted

Gmail’s own snippet averages 186 characters and an ATS rejection spends them on its preamble, so the sentence carrying the decision falls off the end. Of four real rejections, three could not be decided from the snippet at all, and one of those has no snippet whatsoever. The classifier had never once filed a rejection without a human.5

5 tradeoff — The privacy boundary stops being a property of what is requested and becomes a property of the code: the object every persist path receives has no body field, bodies travel beside it and fall out of scope when the request ends, and a test drives a real scan with a sentinel in every body and asserts it reaches no column of any table, no log record, and no response of any endpoint the scan touches. What is stored is unchanged, which is the claim that had to survive: Gmail’s own snippet, the sender, the subject, the date, and the verdict.

[ validation ] · § the receipts

at a glance — 11 of 11 terminate in pinned artifacts

validation

walks the receipts below, top to bottom, and marks each row this page can verify: a check where a pinned artifact resolves, a ring where the trail ends in an on-page capture, a dash where a claim is described only. walked once, the result settles here and stays.
  1. 01claim: Connecting Gmail asks for gmail.readonly and nothing wider — and the consent step deliberately does not merge previously granted scopes.

    method: read the OAuth router and the settings field it draws its scope list from, at the pinned commitdate: 2026-07-26

  2. 02claim: The classifier is three layers — rules, e5 embeddings, SetFit — and SetFit stays off until its training gates are met.

    method: ML strategy doc, read against the backend sourcedate: 2026-07-26

  3. 03claim: The hosted fetch reads metadata only — Subject, From, Date, and Gmail’s own snippet. Full message bodies are never downloaded on the web path.

    method: read the cloud Gmail client: messages.list plus batched messages.get(format="metadata"), read-only, no mutationdate: 2026-07-26

  4. 04claim: The backend suite runs at the pinned commit: 305 tests passed, 0 skipped, under the test/null-keyring environment. It read 278 passed and 10 skipped until 2026-08-03. The +27 is 10 CORS origin-policy tests, 7 benchmark-guard tests, and the 10 Postgres RLS tests that used to be the skips — those now provision their own postgres:16 rather than waiting on a database URL nobody supplied.

    method: `pytest tests -q` in backend-ci run 31152038153 at the pinned head. The Postgres RLS module is included in that count and no longer skips: it starts its own postgres:16 through testcontainers instead of waiting on a database URL nobody supplied.date: 2026-08-07

  5. 05claim: Rules and deterministic hybrid v3 gates both passed on 96 samples with macro-F1 0.9791.

    method: committed baseline, deterministic profile — protocol in the method slipdate: 2026-03-03

  6. 06claim: The macOS Debug target built locally with xcodebuild against the JobTracker scheme, and the desktop app was in the repository at the pinned commit. It was de-scoped on 2026-08-12 and deleted; this pinned tree is the record of it, not a description of the repository today.

    method: local build — no build artifact is published; the pinned source tree is public and linkeddate: date not recorded

    [local — verified on request]

  7. 07claim: The dashboard renders real applications: it reads the summary and application endpoints server-side and draws a pipeline board, a stage funnel, and a review queue from what comes back.

    method: read the page component and the router it calls, at the pinned commitdate: 2026-07-26

  8. 08claim: Row isolation is enforced by Postgres, not by the handlers: the app runs as a role that cannot bypass RLS, every transaction sets request.jwt.claims locally, and user_credentials is FORCE’d so even the table owner is held to the policies.

    method: read the engine’s begin-listener and the three RLS migrations at the pinned commitdate: 2026-07-26

  9. 09claim: The hosted classifier runs the rules layer alone. On the serverless path it returns after layer 1 even when the rules were unsure — embeddings and SetFit are never imported there.

    method: read the cloud short-circuit in the hybrid classifier and the import-hygiene test that holds itdate: 2026-07-26

outcomes

  1. 10claim: Job updates land in a trackable pipeline instead of a spreadsheet — and now in a browser instead of on one Mac.

    method: the product’s own workflow, described — not a usage metricdate: 2026-07-26

    [public]

  2. 11claim: All three classifier layers are built to run in a browser, as an int8 ONNX export of the same model. That build is separate from the hosted web app, and its verdict is not the one getapplied.vercel.app returns.

    method: ported the local classifier and exported it quantized; the export script and the browser build it produces are both inspectabledate: 2026-07-26

rows marked [local — verified on request] were run by me on personal or demo data · ci rows link the public run · repo pins are the exact commits verified 2026-08.

what i’m NOT claiming —

  • I’m not claiming the hosted app runs the full three-layer classifier. On Vercel it runs the rules layer only — deliberately, because the model stack does not fit the function slot. Embeddings and SetFit run in the int8 browser export; they used to run on the desktop client too, and that client was de-scoped and deleted on 2026-08-12.
  • I’m not claiming Applied is generally available. Connecting your own Gmail is invite-only, and the reason is not positioning: gmail.readonly is a Google restricted scope, so until the app clears Google’s OAuth verification and an independent security assessment it may authorise at most 100 test users, each added by address on the consent screen. What needs no invite and no account is the demo — the whole interface over synthetic mail — and the import path, which classifies a Google Takeout export on the reader’s own device.
  • This bullet used to say CI could not prove the RLS policies enforce, because the Postgres suite skipped unless a live database URL was supplied and no workflow supplied one. That stopped being true on 2026-07-31 and the disclaimer outlived it. backend-ci.yml now runs an rls-postgres job against a postgres:16 service, sets JOBTRACKER_TEST_PG_ADMIN_URL, and fails if that URL is missing rather than letting the module skip quietly — so all ten tests execute on every push. A stale disclaimer is the same broken receipt as a stale boast, and the harder one to catch, because nobody audits a claim that costs its author something.
  • I’m not citing the repository’s README or docs/WEB_ARCHITECTURE.md as evidence for the web app. At the commit this file pins, both described apps/web as an unwired scaffold with a placeholder dashboard — they were behind the code, and this file cites the code. The README has since been rewritten as the product’s own record; it is not back-cited here, because these receipts are pinned and a pin is not re-read to suit a later document.
  • No production email-volume or user numbers are claimed. Source, migrations, and test runs are shown publicly; private email and application records are not shown.

[ corrections ] · § the register

  • erratum · 2026-07-26

    Until today this file described a native macOS app whose “web beta is a scaffold”, and pinned every receipt at 3225eb4. That stopped being true: Applied ships as a hosted web app at getapplied.vercel.app. The caveat is retired because it is false now, not because it was wrong then — and the receipts are re-pinned to 36a2f54, the public head of the branch that carries the web app. The scaffold claim’s own successor is receipt 07.

  • erratum · 2026-07-26

    The backend-suite row read 182 tests, audited at the old pin. Re-run at 36a2f54 on 2026-07-26 it is 271 passed and 10 skipped. Nothing was retracted — the tree grew, and the number moved with it. The 10 skips are named in the row rather than folded into the total.

  • note · 2026-07-26

    The repository was renamed yadava5/jobtracker ⟶ yadava5/applied. GitHub still redirects the old links, so nothing this file ever published is broken; the pins name the current repository instead of relying on a redirect. The case-file route stays /projects/jobtracker/ for the same reason.

  • erratum · 2026-08-02

    The backend-suite row read 271 passed and 10 skipped. Re-run on the provenance audit at the current head 0f2b63f: 278 passed, 10 skipped. Same movement as the 182 ⟶ 271 correction above and for the same reason — the tree grew seven tests, nothing was retracted, and the skips did not move. The row is re-pinned to the commit the new count was taken at; every other Applied receipt keeps 36a2f54, which is still where those source audits were done.

  • note · 2026-08-02

    Provenance audit: the classifier claims were re-derived by running the code rather than reading about it. The rules gate passed live at 96 samples, macro-F1 0.9791, 2 misclassified — and the deterministic hybrid gate returned the identical numbers, which is the direct evidence for the attribution correction recorded earlier: the file named “hybrid” measures the regexes alone. The 201-rule figure was recomputed by importing jobtracker.classifier.rules and summing the pattern lists across all seven categories — 106 strong, 26 weak, 69 negative, plus 14 ATS domains. The eval set was counted from its own JSONL: 96 samples, 8 classes, 12 each. Every one of those figures matched what this file already said.

  • erratum · 2026-08-15

    This file described a desktop app that no longer exists. The macOS client was de-scoped on 2026-08-12 and deleted from the repository — apps/macos went, and with it a second, unmounted set of FastAPI routers — so five present-tense claims here were false: the role line, fig. 2’s SwiftUI branch and the edge feeding it, the architecture summary’s “the desktop branch is where the two heavier classifier layers still live”, the rules-layer trade-off, and the boundary row that sent embeddings and SetFit “to the desktop path”. All five are corrected: layers 2 and 3 run in the int8 browser export, and nowhere else. Nothing is retracted and nothing is re-pinned — receipt 06 still cites the macOS build, because it was true at 36a2f54 and that tree still resolves. What changed is the tense: a pinned receipt speaks for its commit, and prose speaks for today.

  • note · 2026-08-15

    Applied is a product now, not a study, and this file leads with it: the run’s rail sends a reader to the live app and the System Card before the case file. Two disclosures come with that. It is licensed proprietary, all rights reserved — the repository stays public so the privacy and isolation claims above can be read against the code, not so the code can be reused. And access is an invite-only beta, for the reason the access boundary row gives rather than a reason anyone chose.

  • erratum · 2026-08-15

    Four sentences here said Applied fetches Gmail metadata and never message bodies: the access constraint, the architecture summary, fig. 2’s fetch node, and decision d1. They stopped being true on 2026-08-15, and so did the edge feeding the classifier, which said the classifier is handed a subject and a snippet. The decision was real and it was reversed on evidence rather than on preference. Gmail’s own snippet averages 186 characters, an ATS rejection spends them on its polite preamble, and of four real rejections three could not be decided from the snippet at all; one has no snippet whatsoever, and the classifier had never once filed a rejection without a human correcting it. Applied now fetches the body, classifies it in flight, and drops it. What is stored did not change: Gmail’s own snippet, the sender, the subject, the date, and the verdict. Bodies stay out by the shape of the code rather than by a promise, and tests/test_body_is_never_persisted.py drives a real scan with a sentinel in every body, then asserts the sentinel reaches no column of any table, no log record, and no response of any endpoint the scan touches; it also asserts the stored snippet equals Gmail’s, because a sentinel search alone passes for a body prefix that stops short of it. On the day of the change the classifier filed two real rejections by itself, reading its own confidence at 0.90 and 0.95, which it had never once managed before. Those two figures are the model’s own outputs on 2026-08-15 and not a count of anybody’s mail: the boundary row that refuses production email volumes and user numbers stands, and neither message is shown. d1 keeps its argument and is marked superseded, because that section is filed as filed; the date it was taken is added at the head of it, so the row cannot be read as a claim about today, and d5 states what runs instead. Nothing is retracted and nothing is re-pinned: receipt 03 still says the hosted fetch reads metadata only, because that was true at 36a2f54 and that tree still resolves. What changed is the tense, and a pinned receipt speaks for its commit while prose speaks for today.

  • note · 2026-08-15

    The Hugging Face Space that hosted the full three-layer classifier went private today, so every link to it returns 401. The references are gone from this file rather than left to fail in a reader’s browser, and the prose no longer offers a hosted demo it cannot deliver. What stands in its place is the receipt the claim always rested on: the browser build itself, ml/browser/site at the pinned commit, where index.html, app.js and the 22.8 MB int8 weights sit together and can be read. Nothing about the export is retracted; the place it was hosted stopped being public, and that is a smaller fact than the one the row asserts.

[ appendix ] · § artifacts, indexed

artifact index —