Privacy policy · updated 14 August 2026

What Applied reads, and what it keeps.

The only Google permission it asks for

https://www.googleapis.com/auth/gmail.readonly

Applied is a job-search tracker. It reads your Gmail to work out which of your applications moved — applied, interview, assessment, offer, rejection — so you do not have to keep the list by hand. This page says exactly what it fetches, what it writes down, where that lives, and how to get rid of it. Every statement here describes the code running at getapplied.vercel.app, not an intention.

01Who runs Applied

Applied is built and run by Ayush Yadav, one person, as a personal project. It is in invite-only beta. There is no company behind it and no staff. Anything you want to ask, correct or have deleted goes to aesh.03.23@gmail.com, and a person reads it.

02The Google permission it asks for

To read mail, Applied requests exactly one Google scope: gmail.readonly. That permission lets it read your messages and their labels. It cannot send mail, delete mail, modify a message, or change any Gmail setting — not because Applied promises to behave, but because the grant does not carry those rights.

You approve it on Google’s own consent screen — your Google password is typed there, on Google’s page, and never reaches Applied — and Google hands Applied a token afterwards. Applied does not ask Google to merge in permissions you granted elsewhere (include_granted_scopes is deliberately off), so this grant contains that one scope and nothing else.

Signing in is separate from mail access. If you sign in with Google, that goes through Supabase’s Google provider and Applied requests no scopes of its own; what comes back is your Google account’s email address, which is how Applied knows who you are. You can also sign in with an email address and password instead.

You can withdraw mail access at any time — in Settings, or from Google’s own permissions page. Section 8 says exactly what each route does.

03What it fetches from Gmail

This section used to say something else. Until 14 August 2026, it said the app asked Gmail for metadata only and never requested a message body, and that was true. It is no longer true: the app now reads the body. What changed, and what did not, is written out below rather than quietly edited away.

The hosted app asks Gmail for the full message in bounded batches: the Subject, From and Date headers, Gmail’s own snippet, and the body text.

The body is read to classify the message and then discarded. It is never written to the database, never returned by any endpoint, and never logged. Only Gmail’s short snippet is stored — the same snippet that was stored before this change, unchanged in length or origin.

The reason is accuracy, and it is specific. Gmail’s snippet stops at roughly 200 characters, and a rejection email spends those characters being polite — “Thank you for your interest in…”, “It means a lot to us that you would consider…”. The sentence carrying the decision falls off the end. Measured against the four real rejections in the author’s own mailbox: one was recognisable from the snippet and three were not, and one of those three had no snippet at all. Reading the body recognises all four.

“Read and discarded” is a claim about code, so it is enforced by code. backend/tests/test_body_is_never_persisted.py runs a scan whose message bodies contain a marker string, then searches for it in every column of every table in the schema, in every log record the scan emits, and in the response of every endpoint it touches — including GET /gmail/inbox, the endpoint that does the reading. The test fails if the marker appears anywhere.

A marker alone would not be enough, and saying so is part of the claim. A body that got stored but stopped short of the marker would pass a search for it, so the test also asserts that what is stored equals Gmail’s snippet rather than merely lacking the marker. Every one of these checks was watched failing against a deliberately leaked body before it was trusted.

A scan covers a date range you choose. A full rebuild also searches archived mail, because a scan that is allowed to remove applications has to be able to see everything it is judging.

04What it stores

One row per message, in a table called emails. Rather than describe it, here is the whole row — including the three columns the schema declares and the hosted app never fills.

emails

one row per message

user_id
the account that owns the row — every query is filtered by it, and Postgres row-level security enforces the same restriction underneath
source_account
which mailbox it came from
message_id
Gmail’s identifier for the message
thread_id
Gmail’s identifier for the conversation
subject
the subject line, as it was sent
sender_name · sender_email
who it came from
received_at
when it arrived
body_snippet
Gmail’s own preview of the message — still Gmail’s, never re-derived from the body the classifier reads. The column stops at 500 characters; measured in the production database on 14 August 2026, the snippets held there averaged 197 characters and the longest was 201.
classified_as
the verdict — applied, interview, assessment, offer, rejection, follow-up, other, or “needs review”
classification_confidence
how sure the classifier was, 0 to 1
classification_method
which layer produced the verdict
user_corrected · is_reviewed
whether you overrode the verdict, and whether you have looked at it
application_id
the application this message was filed under
created_at · updated_at
when Applied wrote the row and last touched it

Declared by the schema · never written

body_text
the full plain-text body
body_html
the full HTML body
raw_headers
the raw message headers

Those three stay empty, and that is now the load-bearing promise rather than a consequence of one. The app does fetch a body (section 3); it does not keep it. Checked against the production database on 14 August 2026: across all 52 rows, body_text, body_html and raw_headers were empty on every one. Enforced by backend/tests/test_body_is_never_persisted.py, which fails if a body reaches any column here, the training table, a log line, or any response the scan touches. It runs in CI on every pull request that touches backend/. This repository has no branch protection, so a failing run is a signal the author has to read rather than a mechanical block on merging — and a change to this page alone does not run it.

So the honest summary is not “Applied stores your emails”. It stores a subject line, a sender, a timestamp, Gmail’s own short preview, and what the classifier decided about them.

05Where it runs, and who never sees it

The web app and the API run on Vercel. Your rows live in a Postgres database hosted by Supabase, which also holds your sign-in. Google is where the mail comes from. Those services hold the data because they are what the app runs on; it is not sent anywhere else.

No large language model and no third-party AI service ever sees your mail. In the hosted app, classification is the rules layer alone — regular expressions matched against the subject and the snippet, in the same function that fetched them. The machine-learning stack is not merely unused there, it is not installed: the cloud deployment excludes PyTorch, SetFit and sentence-transformers outright. The embeddings and the fine-tuned model you can read about elsewhere on this site run in your own browser, on your own CPU, or on a developer’s machine — never on your mail on a server.

Applied’s use of information received from Google APIs adheres to the Google API Services User Data Policy, including the Limited Use requirements.

There is no analytics, no tracking pixel, no advertising and no third-party script on any page — the fonts are served from this origin too. Nobody is buying, selling or sharing your data, because there is nobody to sell it to.

06The rest of what it holds

Beyond the mail rows, Applied keeps: your account — an email address, and a password that Supabase Auth stores and Applied never sees; your preferences — display name, notification and classification settings, kept in your Supabase user record; and the applications the tracker builds, whether derived from mail or typed in by you.

Your Gmail token is stored encrypted (Fernet) in a table called user_credentials, one row per account. It is never rendered in the browser, never written to a log and never put in a URL.

Two things stay on your device and are never sent to the server: your light/dark choice, under jt-theme in local storage, and the marker the dashboard uses to remember which rows you had already looked at.

The public demo sets two cookies of its own, applied-demo-notifications and applied-demo-ambient, both scoped to /demo. They hold the toggles you flip on the demo settings page — the two notification switches and the sidebar's ambient-mail switch — so the demo pages can show what they do. Unlike the two above they are sent to the server — that is how the demo pages read them — but they never reach the signed-in app, they carry no identity, and they are gone when you close the browser.

Applied’s own log lines record identifiers and outcomes — a user id, a count, whether a sync failed — not the contents of your messages.

07How long it is kept

Nothing expires on its own. There is no retention timer and no scheduled job that deletes anything — the deployment declares no cron at all. What Applied has written down stays until you remove it, by the routes in section 8.

One exception works the other way: a rebuild scan can delete applications it had previously derived from mail, when the mail no longer supports them. Anything you edited or settled yourself is left alone.

08Disconnecting and deleting

Disconnect Gmail (Settings → Gmail) revokes the grant at Google and deletes the stored token. Applied stops reading mail immediately. The messages it has already classified stay in your account.

Delete your account (Settings → Account) revokes the Google grant if one is connected, then asks the API to purge every row you own — from email_embeddings, contacts, interviews, emails, applications, training_data, sync_state and user_credentials — and only then deletes the sign-in account itself. The revocation comes first because the stored token is the only thing that can revoke it, and it is gone a moment later.

Two limits, stated because they are true. First, the revocation is best-effort: if Google cannot be reached it is not retried, and the deletion goes ahead regardless rather than trapping you in an account you asked to leave. You can always confirm or remove the grant yourself on Google’s permissions page. Second, the purge is not best-effort and deliberately so: if the API cannot be reached, nothing is deleted — your sign-in account is left intact, your data is untouched, and you are told to try again. It used to work the other way, and the rows could outlive the account that owned them.

If a deployment tells you deletion is not enabled — Settings says so before it asks you to confirm, not after — email aesh.03.23@gmail.com and it will be done by hand.

09Training, and what is forbidden

Your mail is never pooled with anyone else’s to train a shared model. Google’s Workspace policy permits data from a restricted scope like gmail.readonly to train only a model personalised to a single user, with no co-mingling — and the code enforces that rather than trusting itself: every training entry point requires a user id, the corpus is filtered by it, and the loaded rows are re-checked so that a corpus spanning two users raises instead of training.

In the hosted app, no model trains at all — there is nothing installed to train one with. When you correct a verdict, Applied records the correction as your own example (the subject and the same snippet) in training_data. It is yours, it is scoped to you, and deleting your account deletes it.

10On-device import

The import page (/import) is the other way to try Applied on your mail, and it is the inverse of everything above: nothing reaches a server at all. You hand it a mail export — a Google Takeout .mbox, a single .eml, or a JSON batch — and the file is read and classified entirely in your browser tab. The page makes no network request with your file, and it works signed out, with no Google connection.

What runs there is the classifier’s fast first pass — deterministic rules, in the tab. The machine-learning model does not run on that page. Nothing is written down anywhere: the results live in the tab’s memory and are gone when you clear them or close the tab.

11Changes, and how to reach a human

If this policy changes, the date at the top changes with it, and the reason is a change in the code — this page is maintained as a description of the system, not as a document that drifts away from it.

Everything else — a question, a correction, a deletion, a complaint — aesh.03.23@gmail.com.