This is a description of what the software does, not legal advice, and no lawyer has read it. If you run an instance for other people you are the one answerable for it — the wording, the retention periods, the lawful basis, and whether any of it is enough where you live. What this document can honestly do is tell you exactly what the code stores, so that whatever you publish is true. It was written by reading the schema, and the export button below produces the same list; if the two ever disagree, the export is right and this file is out of date.
Every table named here is in app/db.py. Nothing is hidden in a service somewhere else: there is one SQLite file, on the machine the operator runs this on.
Nothing about you is stored by this site. The dashboard works fully signed out; that shared workspace is stored under user id 0 and belongs to nobody. Your browser is given no cookie by this site until you sign in.
Analytics are a separate question, and you are asked before anything happens. If this instance has Google Analytics configured, a banner appears on your first visit. Until you press Accept, nothing is fetched from Google, no identifier is set, and no visit is reported. Press *No thanks* and that stays true for good — the answer is remembered so you are not asked again.
If you do accept, Google Analytics receives the pages you look at, roughly where you are (from your IP address, which it truncates), which site you arrived from, and your browser and screen size. It sets identifiers in your browser to recognise the same visitor across pages and visits. Google is a separate company and processes this under its own terms. Two of its settings are switched off here: allow_google_signals and allow_ad_personalization_signals, which are the ones that feed advertising products rather than visitor counts.
You can change your mind at any time — *Change your analytics choice* at the foot of every page. That forgets the answer and reloads, so nothing further is sent.
Nothing about your account, your list or your scores is ever sent to Google. Analytics sees a page being visited, not who you are here or which companies you follow.
| What | Where | Encrypted? |
|---|---|---|
| Username | users.username | No — it is looked up on every request |
| Password | users.password | Not stored. Only a PBKDF2-HMAC-SHA256 hash, 600,000 iterations, unique salt |
| Email address | users.email | Yes, AES-256-GCM, when SCORER_SECRET_KEY is set |
| Email lookup key | users.email_index | A keyed HMAC, so the address can be found without being readable |
| Google account id | users.google_sub | No — looked up on every sign-in. Only if you choose Google |
| When you signed up | users.created_at | No |
The password is never stored, in any recoverable form, and neither are recovery codes. They are one-way hashes. Nobody running the server can read your password, and no restore from backup can produce it.
| What | Where | Encrypted? |
|---|---|---|
| Companies you follow | watchlists | No — every score is joined against it on every read |
| Weights, anchors, which measures count | settings | No |
| Saved setups | favourites.payload | Yes |
| Computed scores | snapshots | No — arithmetic on public filings |
So "user 7 follows NVDA" is readable to anybody holding the database file. That is stated plainly rather than buried: the ticker is what every read joins on and what every fetch works from, and a column that must be searched cannot be hidden from the thing searching it.
| What | Where | Encrypted? |
|---|---|---|
| Whether it is a bug or a feature | reports.kind | No |
| What you wrote | reports.body | No |
| When you sent it | reports.created_at | No |
| Your account id, only if you were signed in | reports.user_id | No |
That is the whole row. Nothing is collected that you did not type — not your browser, not your address, not the page you were on, not which companies you follow. A bug report usually carries all of that, and it would make the report easier to act on; it is left out because this document lists every stored field and collecting quietly would make it false.
Send one signed out and there is nothing in it that points at you. Send one signed in and your account id is stored, so the operator can reply — that is the only reason it is there.
Reports are not deleted when an account is deleted. The account id is cleared, so what remains is an anonymous note about the software. A bug is not personal data about you, and losing the report would mean losing the reason a fix was made.
| What | Contents | Kept for | |
|---|---|---|---|
sessions | sha256 of the cookie, never the cookie | 30 days, or until you sign out | |
known_devices | sha256 of a browser cookie, first and last seen, last IP address, browser string | Until the account is deleted, or you use *Sign out everywhere else* | |
login_failures | `IP address \ | username`, and a timestamp | 15 minutes for sign-ins, 1 hour for sign-ups, everything dropped after 24 hours |
email_tokens | sha256 of the link that was mailed | 1 hour. The used row is kept so an old link can be told "already used" rather than "invalid" | |
mail_queue | Recipient and body, both encrypted | Deleted the moment it is sent. A message that could not be delivered is kept so somebody can see why |
The IP address in known_devices is there for one purpose: so a sign-in from a browser the server has not seen can tell you where it came from. It is what makes the notice worth reading.
| What | Where | Encrypted? |
|---|---|---|
| The date | visits.day | No |
| Which page | visits.path | No |
| How many times it was asked for | visits.views | No |
Three columns, and there is no fourth. No address, no cookie, no session, no browser string, and nothing derived from any of them. One row says "on this date, this page was read this many times" — and there is no way, from this table, to tell whether that was one person or a hundred.
That is the whole design rather than an omission. The question it answers is whether the site is growing, and a count per day answers it exactly. Storing an identifier as well would buy a unique-visitor figure and cost the only property this table has: there is nothing in it about you, so there is nothing to leak, nothing to hand over, and nothing to delete when you ask.
Requests the dashboard makes to itself are not counted, and neither are crawlers. The user agent is read to tell one from the other, and then dropped.
No analytics service is involved in this. It is a counter in the same SQLite file as everything else, readable only by whoever runs the server.
If the operator has set SCORER_LOG_FILE, security events are written to disk and rotate after 5 MB, five files kept — so roughly the last 25 MB of events, and no longer.
Written: an account being created, deleted or exported (by user *id*), a sign-in lockout, a rate limit being hit, mail failing. IP addresses appear on the events where the address is the whole point.
Never written: email addresses, usernames, session cookies, reset links, passwords. A log is the one place personal data escapes by accident, because it gets pasted into issues and kept long after the database row was deleted.
The web server in front of this keeps its own access log, which is not covered here and usually does record addresses and pages. That one is the operator's to configure.
| Who | What they get | When |
|---|---|---|
| SEC EDGAR | A ticker or company id, and the operator's contact address in the request header | Only when the operator refreshes. Never triggered by a visitor on a published instance |
| Yahoo Finance | A ticker symbol | When a price is refreshed |
| Google Analytics | Pages visited, approximate location, referring site, browser | Only after you press Accept. Never before, and never if you decline |
| Google sign-in | That you are signing in | Only if you choose *Sign in with Google* |
| The mail provider | Your address, and what the message says | Only when a message is actually sent to you |
| The payment provider | That a browser opened the donation page: its own cookies, and your address if you choose to give one *to them* | Only if you press Donate. Never on arrival, and never if you do not |
The SEC and Yahoo are asked about *companies*, and the question is the same whoever asked it — neither is told who you are or that you have an account here. Google Analytics is told that a page was visited, and nothing about your account or your list.
The donation page is loaded on a click, not on arrival. Until you press Donate, nothing is fetched from the payment provider, no frame is opened and it is told nothing. Press it and its page opens here, under its own terms and its own cookies — the referrer is withheld, so it is not told which page you came from, and it is the only outside host this site will load at all. No payment is ever handled here. There is no field on this site that takes a card number, no such column in the database, and no credentials for any payment provider: what you give, and what they know about it, is between you and them. If you transfer to a bank account instead, that happens in your own bank and this site never sees it.
Nothing is sold. There is no advertiser here: this is a program that reads filings. What Google does with what it collects is governed by its own terms, which is the trade you are being asked to accept or decline.
Stored SEC filings and share prices are not deleted with an account. They are public data, shared by everybody using the instance, and not one person's to take away — somebody else following the same company would lose it too.
Encryption at rest covers a leaked *file*: a backup in the wrong place, a copied database, a resold disk. It does nothing against somebody who has taken over the running server, because the process must read these fields to work. Anyone who tells you otherwise is selling something. Whether the disk itself is encrypted, and who can log into the machine, is up to whoever runs it.