Skip to content

Intake path

How a resident's bill gets from their phone to an extracted record, with nobody outside the house ever able to read it.

Options

Option Who holds the document Verdict
In person at meetings, scanner and paper consent slip The keeper, on a device that goes home Start here. No service to build, consent on paper, a code card handed over.
A form that lands on the homelab through an encrypted hop Ciphertext on GKE briefly; plaintext only on the homelab Next, once the receiver exists.
A dedicated email inbox The mail provider No. It makes a consumer mailbox a processor of health documents.
GitHub issues, Discord, any chat bot, any public channel A third party's logs and an agent's context Never.
From the household's own records, kept and redacted on a Demicracy node at home, sent through the same encrypted hop The household, on its own machine Later. The hop and the receiver are unchanged; what changes is that redaction happens where the records already live. See Integrations.
Local drop-off to a community node: a Knarr-backed Matrix room run by neighbours on their own hardware, standing in for the receiver That community's node, not a cloud Later, maybe. For a community that trusts its own nodes over a third-party cloud. The payload stays encrypted to the homelab key either way. See Integrations.

The encrypted hop

sequenceDiagram
    participant R as Resident's device
    participant G as Receiver (GKE)
    participant H as Homelab (tailnet)
    R->>R: redact; encrypt in the browser to the homelab public key
    R->>G: upload ciphertext (document, facility choice, consent)
    G-->>R: code card (withdrawal + follow-up), no account
    H->>G: pull ciphertext over Tailscale
    H->>H: decrypt, OCR, PII pass, human review, extract record
    H->>G: delete ciphertext
    H->>H: delete original after 30 days; record notes deletion

Encryption happens on the resident's device, in the browser, with the homelab's public key embedded in the form page (WebCrypto or a sealed-box library; chosen at M0.5). Everything the resident tells us, the document, the facility they picked from the list, and the consent tick, goes inside the one encrypted payload, so the receiver never holds a facility name beside an upload time. The receiver then holds only ciphertext: it accepts a blob, issues a code, and waits to be pulled from. It is small and boring on purpose, and its configuration contains the public key only. If a browser cannot run the encryption step, the form says so and points to the in-person path rather than falling back to a plaintext upload.

That guarantee is exactly as strong as the form page the resident loaded. A page served by a compromised receiver, or altered in transit, could swap the key or post plaintext elsewhere. So the form page is static and lives in Git, served from GitHub Pages rather than from the receiver, so every change to it is a reviewed commit; the public key's fingerprint is printed on the in-person materials and the site's methods page so a resident or a volunteer can compare; and the receiver is a separate origin that only accepts posts. None of that makes a browser upload as safe as handing a paper to the keeper at a meeting, and the instructions say so.

Codes, not accounts

A submitter gets a code at submission, printed on a card in person or shown once on the form. The code lets them withdraw the record or answer a follow-up question ("was the bill later reduced?"). It is tied to the record, not to a person; the mapping from code to household is never kept. This is the Ting pattern; see Integrations.

The code is the only credential, so it is treated as one. Generated by a cryptographically secure random source (proposal: twelve characters from a 32-symbol alphabet, about 60 bits, grouped for reading aloud), which is far more than the online rate limit can be made to guess, and not enough on its own against an attacker holding a copy of the database. So the stored form is a memory-hard key derivation (Argon2id) of the code with a unique random salt per code and a server-side pepper; the salt and the KDF parameters are stored beside the hash so they can be tuned later, and the pepper lives in the service's secret store, never in the database. A database copy without the pepper is useless, and with it each guess still costs a deliberate amount of memory and time, per code. Codes do not expire while their record exists, which lengthens the window a leaked database would be attacked in, and the pepper is what makes that acceptable. Checked online through a rate limit per source and a global one, with lockout and an alert to the keeper on bursts; no expiry while its record exists, revoked when the record is withdrawn or deleted, and revocable by the keeper on a report of loss. A wrong or revoked code produces the same answer as a right one would after withdrawal ("no record"), so probing confirms nothing. Follow-up questions never echo the record's contents back; they ask, they do not show. Both intake paths issue codes the same way; the in-person path pre-prints them on the cards from the same generator.

Our redaction pass

Every submission, redacted or not, goes through OCR, pattern detection for member IDs, claim numbers, dates of birth, addresses, phone numbers, and account numbers, named-entity detection, and the GDD PII guard, then a human look before any extracted record is published. The tools are evaluated on the Redaction page.

The statement to submitters: we redact as able and we never publish the document itself, but anything you upload to the internet is on the internet, so redact first.

Open

  • Whether the receiver is a new service or a Ting endpoint; Ting's existing code generation and rate limiting are the first thing to check against the controls above.
  • Where the key fingerprint is shown so that comparing it is something a resident would actually do.
  • Key custody for the homelab private key, and what happens to in-flight ciphertext if the key is rotated.
  • Whether a second keeper (the two-person rule) is needed before the form opens, or only before volunteers join.