How it works Terms Security Docs

What Verbatim records, and the one record it cannot erase.

Privacy · 2026-08-04

Verbatim holds the documents a regulated company puts into it and writes a permanent trail of every decision taken over them. This page says which fields exist, who can read them, and where a deletion request runs into a wall.

Verbatim is a pre-production build by one person. It has no customers. The repository carries two corpora, both labelled. One is invented -- a fictional commission, a fictional docket, a fictional company -- and the eval numbers are measured against it, because its traps were built on purpose. That is the one the setup command loads. The other is 102 real public filings retrieved from eight state commissions, each with the URL it came from and a hash of the bytes; a script loads one pair of them, and nothing runs that script for you. Neither corpus is confidential: one is fiction, the other is what the commissions publish. Nobody's customer documents are here.

Nothing on this page describes a control the code does not have. Where a buyer would expect one and it does not exist, the page says so instead of implying it.

What the product stores

Field by field, as the tables actually stand. Nothing here asks for a payment card, a date of birth, a telephone number or a home address, and no column exists to put one in.

account

Your email address, your display name, a scrypt hash of your password with its own random salt and its own cost settings, the state of the account, when it was made, when you last signed in, how many sign-ins failed in a row, and the time any lock runs to. The password itself is never written down.

session

One row for each sign-in. It holds a SHA-256 of the session token and never the token, when the session started, when it expires, when it was last used, the address the request arrived from, the browser's user-agent line, and — once it ends — the time it ended and the reason.

documents

Each filing you load, kept byte for byte with a SHA-256 beside it, and the passages cut from it with their character offsets. The product re-reads those bytes every time it checks a citation, so it cannot discard them and still work.

your work

Proceedings, changes, claims and their citations, escalations, projects, research threads, plans, findings, questions, deliverables, obligations and their owners, and anything typed into the chat. Every row names the company it belongs to.

the trail

A row for every decision, with who, what, when and from which address. That one has a section of its own below, because it is the field a privacy policy usually leaves out.

The trail records your address, and that is the point

Every decision the system takes writes a row naming the person as a display string, the account id, whether a person or a machine acted, the login session it happened under, the IP address the request arrived from, and the time in UTC. All of it sits inside the row's hash, not merely beside it, so nobody can rename the actor afterwards and leave the record looking sound.

Failed sign-ins are recorded as well as successful ones, so anyone probing accounts leaves a trace. The row names the address that was typed and never the password tried against it. An entry that does not look like an address at all is stored as a marker instead, because a password typed into the wrong box would otherwise be kept for ever.

The address is the one the connection itself reported. The product does not read the X-Forwarded-For header, because a header the caller writes is not evidence.

Why say this so plainly: an audit trail is the product, not a side effect of it. A regulator asking who said what, when, and from where is the question Verbatim exists to answer. A policy that quietly omitted the address would be hiding the main feature.

What cannot be erased

The trail is append-only twice over. Application code has no path that updates or deletes an audit row — the attempt raises. And a SHA-256 chain runs over every field of every row, per company, numbered from one with no gaps, so a row altered or removed by other means is detected: a gap in the sequence is a deletion.

Several laws give a person the right to have their data deleted. That right and this design pull against each other, and pretending otherwise would be dishonest. A deletion request cannot take your audit rows out, because removing one breaks the chain that makes every remaining row worth reading.

What stays. Your email address as the actor on rows you caused. Your account id, your session id and the IP address on the rows the sign-in path wrote. What each decision was and why.

What the trail still says about a record after that record is deleted. The audit row naming a thing outlives it, and goes on pointing at an id that no longer resolves. A reader still learns that a record with that id existed, what kind of thing it was, when each decision about it was taken, who took it, from which address, and the reason given. They do not learn what it said — the words of a chat turn, the text of a complaint, the address a share link was opened from. Those live only in the row that went. That is the honest shape of erasure against a log that cannot forget, and it is less than erasure; calling it anything else would be the failure this product exists to stop.

Two addresses no deletion can reach at all. A failed sign-in writes the address that was tried into the trail as the actor, and as the subject too when no account matched it. An invitation's row quotes the invited address in its reason. Both sit inside the hash. Every other table in the database could be emptied and those two lines would still be there.

What is true today. Six purge functions exist, in app/state/retention.py, and they are tested. They cover the tables holding an address, an email or a person's own words: sessions, share opens, chat transcripts, feedback and invitations. A scheduler can now call them, and by default it does not delete anything. app/jobs/runner.py reaches them, and three separate settings stand between that and a deleted row: no job loop starts at all unless VERBATIM_JOBS_ENABLED is true, the purge runs as a dry run unless a second setting asks for the other thing, and purge_all checks that flag again itself rather than trusting its caller — because 0 is falsey, and a configuration typo that armed a delete would be the worst bug this page could hide. Whichever way it lands, the container log says so on every start. There is still no erasure path for a user, a document, a claim or a proceeding: suspending an account keeps the row, revoking a role keeps the row, and signing out keeps the session row and marks it revoked with a reason. The next section sets out the whole schedule and where it stops.

Placeholder — required before production use A written erasure procedure: what a lawful request can and cannot reach, who decides, how the answer is given, and in what time. The schedule below is code; it is not that procedure, and it does not answer a request from a person.

How long each thing is kept

A retention schedule is usually a table in a document, which is to say a set of claims nobody can check. This one is code. Every table in the database carries a window and a reason beside it in app/state/retention.py, and a test walks the schema and fails on a table nobody has given a window — so a table added next week cannot arrive quietly with no answer. A window with no reason is a number somebody changes without thinking, so the reason is part of the rule rather than a note about it.

The rows holding an address, an email or a person's own words get the shortest windows. The rows holding your filings get no clock at all, and the reason is below.

the trail

Never. Every row's hash covers the row before it, so removing one does not delete a record — it destroys the evidence value of every record after it, and a gap in the numbering is itself read as a deletion. The section above says what that costs.

sessions

90 days after the session expires. An expired session row answers one question — was this sign-in used, from which address, on which browser — and after a quarter no investigation is still asking it. It holds an IP address and a browser line, so it gets the shortest window of anything kept at all. Measured from the expiry rather than from a sign-out, because the expiry is the last moment the token could have worked.

share opens

90 days for the address, 365 days for the row. Two different things sit in it: the address of somebody who never signed in, and the record of what a recipient was shown and whether the citation held at that moment. The address comes out at ninety days and the row stays; the row goes at a year. Where the address has been taken out the field says so rather than going blank, because blank on that column already means the server never recorded one.

chat

365 days after the last thing said, or after it was opened where nobody ever spoke. What you typed is yours. One exception, and it announces itself: a conversation holding a turn somebody complained about waits until the complaint goes, so that a reviewer never opens a complaint and finds the thing complained about missing. The report names the row doing the holding.

feedback

365 days after it was written, whether or not anybody triaged it. Keeping untriaged rows for ever would turn the queue nobody worked into an archive nobody agreed to. The backlog item it was triaged into survives and carries the substance without your words.

invitations

90 days after it stops being acceptable. It holds the email address of somebody who may never have had an account here, which makes it the row least likely to belong to a customer at all. The trail row saying they were invited outlives it, and quotes that address.

everything else

No clock. Documents, passages, proceedings, changes, claims, escalations, projects, findings, deliverables, obligations, approval routes, accounts and role grants are kept while the account is open. They are what the product is for — verification re-reads the stored bytes every time it checks a citation, so discarding them would turn every claim into an unverifiable assertion. There is no account-closure event anywhere in this build, so there is nothing for a clock to start from, and no code here pretends otherwise.

Every purge is a dry run unless the caller asks for the other thing, and the dry run reports exactly what would go. Every real purge writes a trail row naming the rule, the table, the count and the cutoff before it removes anything, because a deletion nobody recorded cannot be told apart from a breach. That row names nothing out of the rows it deleted: an address copied into an append-only log to record its own deletion has been moved rather than removed. Running the purge twice is safe, and the second pass writes nothing.

Where the schedule stops. It reaches the rows in Verbatim's own database and nothing else. Whatever web server sits in front of the application keeps its own request log, and no code here reads or removes that. Nor does a purge reach a copy of the database taken before it ran.

Placeholder — required before production use Somebody to read it. A scheduler exists and can run these purges, and nothing in the repository arms either of the two settings that would let it delete anything. What is still missing is the other half: a daily dry run for each company whose report goes to whoever is accountable, before the destructive run is ever turned on. Nothing sends that report to anyone. That difference, between a control and a promise, is why this is a box and not a sentence saying the schedule is enforced.

Who else sees it

One company is sent your text: Anthropic, through the assistant. A second holds the disk it all sits on — the machine is rented, and the sub-processors page names it and the two others behind the domain and the certificate.

  • The assistant sends your text to a model vendor. The chat panel sits on every screen. Each turn goes from app/web/views/chat.py through app/chat/engine.py to app/chat/agent.py, which calls Anthropic's API. What travels: the question you typed, your display name, the company's name, the permission codes you hold, and whatever the model looked up — obligations and who owns them, open escalations, the approval route, claims whose citation verifies, and up to 600 characters of filing text on each side of a change. A claim whose citation does not verify is never sent as a statement. With no ANTHROPIC_API_KEY set, nothing is sent and the panel says it is unavailable. This page said no model call ran for most of a day after one did; the sub-processors page now carries the detail and this bullet is checked against the same files.
  • No analytics, no trackers, no advertising, no tag manager, no session recorder.
  • No content delivery network, no third-party fonts, no third-party stylesheets, no third-party scripts. This site and the application load nothing from another host. Two web addresses appear in the front end and neither is fetched: the SVG namespace identifier, which is a name rather than a request, and an example address printed in a form's placeholder.
  • No payment processor, because nothing is for sale yet.

If you write to the address at the foot of this page, the message sits in an ordinary Gmail mailbox. That is Google's service, not mine, and their terms govern it.

Placeholder — required before production use A data processing agreement with Anthropic, and a legal entity to sign one. Text is going there today with nothing agreed — no commercial terms, no retention arrangement, no statement about training on inputs. The sub-processors page is the list; the contracts behind it do not exist.

The cookie

One cookie, named verbatim_session. It carries a random 256-bit token and nothing else — no identifier the product follows you with, no profile, no preferences. The token resolves a row in the database, which is what lets a sign-out end the session on the server rather than merely clear your browser.

It is set HttpOnly, so a script cannot read it; SameSite=Lax, which is the whole of the cross-site request defence on this build; Secure, so it does not travel over plain HTTP; and it expires with the session, twelve hours after you sign in.

One narrow exception: for plain HTTP to your own machine, which is what the local run serves, the Secure flag comes off, because Safari would otherwise drop the cookie and the login page would not work. The login page prints on the page when it has done that.

What the demonstration contains

The repository carries two corpora and the setup command loads one. That one is invented: a fictional commission, a fictional docket, a fictional utility, fictional people. No text in it is copied from a real filing, and nobody's real documents are in it. It is one docket in three versions, and it is what a workspace set up the documented way holds.

Beside it sit 102 real regulatory filings downloaded from eight state commissions' own public document systems, each with a note recording where it came from and when. Those are public record and no customer supplied them. A separate script, scripts/ingest_real.py, loads one pair of them — a witness's direct testimony in a Kentucky rate case, filed and then corrected — so the product can be seen working against a document a person really filed. Nothing runs that script for you: neither the setup command nor the deployment start-up calls it. So unless somebody runs it by hand, all 102 sit on disk unread.

Where it runs, and who to ask

The data sits in a single SQLite file on whichever machine runs the product. Since 2026-08-04 one of those machines is a virtual machine rented from DigitalOcean, which serves verbatim.citelocal.ai; the sub-processors page says what that company holds and names the other two. There is no encryption at rest beyond whatever the host's disk provides, and no backup has been taken of any deployment. The security page lists what is built and what is not, without softening either.

Write to jsahasi@gmail.com. One person reads it.

Placeholder — required before production use A named legal entity, a registered address, the country whose law governs this document, and a privacy contact belonging to that entity. None of those exist yet, and inventing them would be the exact failure this product argues against.

This page changes when the code changes, and the date at the top moves with it.