Data and security

How we protect your data

What happens to a client’s documents from the moment they are uploaded, written from what the code does. Where something is a property of a platform we use, a setting rather than a guarantee, or not built yet, it says so. Checked against the code on 4 September 2026.

Hosting and regions

The console runs on Vercel, with its functions pinned to Sydney by the project’s configuration. The database and file store are a Supabase project in Sydney (ap-southeast-2). A case is checked inside a Vercel function in Sydney.

Both regions are configuration honoured by the platforms; the code does not verify at runtime where it is running, and no failover region is configured. Vercel’s builds and its platform logs are operated from the United States even though the functions run in Sydney, which is why the code keeps document bytes, extracted text and credentials out of every log line.

The two storage buckets — one for documents, one for reports — are private, with no policy that would let a browser reach either, and a 25 MB ceiling per file.

In transit

Connections between your browser and the console, and from the console to our database, our storage, the ABN Lookup service and any model provider, use HTTPS/TLS as provided by the platforms. Every outbound address the code uses is https://.

To be exact about what that means: the application does not enforce TLS itself. It sets no redirect from HTTP and no HSTS header of its own — that is Vercel’s — and encryption to the database depends on the connection string and Supabase’s pooler rather than on a check in the code. The session cookie is httpOnly, SameSite=Lax and marked secure in production.

At rest

Uploaded documents are encrypted with AES-256-GCM as they arrive: a 32-byte key, a fresh random 12-byte nonce for every object, and a 16-byte authentication tag. The application facts the broker declares are encrypted the same way.

One key per case. It is generated once, before the first file lands — not one per firm and not one per document.

The case key is wrapped under a master key, through a per-case key derived from it (HMAC-SHA256 of the master key and the case identifier, then AES-256-GCM over the case key), so a wrapped key from one case will not open under another. The wrapped key is stored as an object beside the ciphertext, deliberately not as a database row, because database backups outlive a purge.

On the current deployment the master key is an environment variable, not a key held in a hardware module. The code records the trade in its own words: it is something a person with access to the deployment could copy. The hardware-key path exists in the code and is not deployed. Whatever disk-level encryption Supabase applies underneath is theirs, not ours, and we do not describe it as ours.

What is stored about a case

The database holds the case (its reference, status, who created it and when, its retention choice and due date); each document’s original name, size, SHA-256 hash and state; each finding (which check, its tier and severity, its title, a short evidence record, and its status); the report record; the stages a run passed through; an audit log; purge receipts; sessions; and credentials.

No column holds a document’s text or content, with one recorded exception: the case label is filled with the applicants’ names, read off the documents, so a broker can find their own file. A client’s name outlives the documents it came from. A second, smaller one: a finding’s evidence can hold short values lifted from a document — an employer name, two amounts, a transaction description, a masked TFN — capped at 160 characters per value and 800 in total.

Separately, a fingerprint registry is designed to outlive a case. It holds a keyed hash of each document (an HMAC under a secret of ours), a count of the distinct cases it appeared in, and two dates at day precision. No firm, case, person or filename. Entries expire after two years, and matching across firms is off by default.

Destruction and the purge receipt

At the end of every case, failed ones included, the documents are purged. Each document object is deleted and then read back to confirm it is gone; then the encrypted declared facts; then, last and only if nothing before it failed, the wrapped case key. The purge runs outside the code path that handles a failed run, so a case that dies halfway still ends in destruction.

One purge receipt per document records the firm, case and document, its kind, its SHA-256 (of the original, computed before encryption), its size, when it was uploaded, when it was purged, which storage backend held it, and whether the deletion was verified. Receipts can be read by administrators only and can be neither changed nor deleted.

The database rows are not deleted: the document’s storage key is cleared and its state becomes purged; findings, the report record, stages, audit entries and receipts persist, and so do filenames and hashes. A nightly job reports storage objects that belong to no case; it deletes nothing. Removal is a deliberate, manual command.

Keeping documents

Retention is chosen once per case and locked when the first file lands. The default is destroy, and a missing value never means keep. Choose keep and nothing is destroyed: the documents stay encrypted, the wrapped key is kept, and a due date seven years out is recorded on the case.

Deletion at that date is a manual step today. Nothing in the code destroys a retained case when its due date passes, and the nightly job does not look at retention. A retained case writes an audit entry rather than a purge receipt.

The report and its retention

The report is a PDF: the verdict; every finding with its evidence and next step; the checks that passed; the checks that could not run; a register of the documents (name, kind, pages, SHA-256); what the broker declared; which model was used, if any; and the destruction time. It also carries two kinds of page picture. Where a scanned page has an area whose grain does not match the rest of the paper, a faded image of that page is reproduced with the area outlined, so a reader can see what was pointed at — that happens on any case, including one whose documents are destroyed. And on a retained case only, a picture of every page, with tax file numbers covered. It is stored in the report bucket with a seven-year retention date and served only to a signed-in, MFA-verified member of the firm that owns the case. Nothing yet deletes a report after seven years.

The report archive is not immutable at the platform level. The code refuses to overwrite a report, but somebody holding the storage service key could. The integrity check that does exist is the SHA-256 fingerprint of the report, stored in the database and printed in the report’s footer: if the two do not match, the file has changed.

Tax file numbers

A tax file number is validated in memory and discarded; it is never sent to a model and never stored in full. Where the checksum fails, the finding records the last three digits so you know which number to re-check.

It is painted out of every page image before any model, classifier or exhibit sees it, and the cover is verified — a page whose cover cannot be verified is not rendered at all. A number is treated as a TFN only when the document labels it as one; a bare run of nine digits deliberately is not, and so is not covered.

The AI model

What is sent: rendered page images only — PNG at 120 DPI, at most four pages per document — from the sanitised copy with tax file numbers covered. No case identifier, client name, filename or broker identity travels with them. The model’s reading is advisory and never decides a verdict.

When: only if your firm has chosen a model deployment in the console and stored its own key. Without both, no model is called, and the report says: “Model used for reading: none – every figure was read from the document’s own text”.

The key: set by your administrator in the console and sealed under the master key with the same envelope client documents use, bound to your firm. Only its last four characters are ever shown, nothing can read it back out, and it can be removed outright.

Offshore: the console lists deployments, not brands. Some run in Australia; others run in the United States. Choosing one outside Australia requires the administrator to type ACKNOWLEDGE, with the countries named in the prompt; the database refuses the choice without that acknowledgement, and the acknowledged countries are written to the audit log.

Training: we do not train any model on your documents, and the model is called under your firm’s own provider account and terms. To be plain about the limit of that: the code sets no “do not train” or zero-retention flag on a request. What applies is the agreement between your firm and the provider.

The only other outbound call is to ABN Lookup, in Australia, which receives the ABN and nothing else.

Access

There is no sign-up page. An administrator invites a person, and the invitation is activated with a token that sets a password and a second factor together. There is no password-only account.

The second factor is a time-based one-time code (six digits, thirty seconds), checked by one gate on every case and admin route. Enrolment fills an empty slot only and cannot replace an existing secret, so a stolen password is not enough to swap out the factor standing in its way.

Passwords are stored as scrypt hashes with a 16-byte per-user salt, compared in constant time, in a separate credentials table with no read policy. Sign-in gives the same error for an unknown email and a wrong password.

A session is a 32-byte random token in an httpOnly cookie; the server stores only its SHA-256 and enforces a twelve-hour lifetime. When a person is removed, all of their sessions end.

Firms are isolated from one another, and the second-factor gate is enforced, by the application code, which connects to the database as the service role. Row-level policies exist in the database but are not the path that enforces the app’s own access.

Backups, incident handling and logging

Backups: there is no backup code of ours. Supabase’s backups are a platform matter; their existence, frequency and retention are not visible from the code, so we make no claim about backup encryption, retention windows or point-in-time recovery.

Incidents: there is no automated incident process, and a written response plan is an open item rather than a finished one. What applies regardless is the law: the Notifiable Data Breaches obligations under the Privacy Act bind the firm, and we will meet them.

Logging: an append-only audit log records document ingest, model provider selection and removal, purges, purge failures and sweeps, and can be neither edited nor deleted. Application logs carry no document content. How long the platforms keep their own logs is not controlled by our code.

What we don’t claim

  • No SOC 2 or ISO certification.
  • No guarantee about backups.
  • No 24×7 monitoring, and no promise about how long logs are kept.
  • No “immutable” or “write-once” archive.

If any of these matters to your firm’s decision, ask, and we will say where things stand.

← Back to Factum
How we protect your data — Factum