Ovillo Download
How it works · Architecture

How it works

Architecture

Ovillo is a web page that runs inside an Electron window on your Mac, backed by a small server that only listens on your own computer. This page is for the curious and for developers: it names each part, says what talks to what, and shows what is on disk.

The parts

PartWhereWhat it does
The pageindex.html, app.mjs, styles.css, src/*.mjsAll the work on your documents: reading, decrypting, the record, search, briefs, review and exports. Plain ES modules, no build step. It is the only place that ever holds your passphrase and the vault key.
The local serverserver.mjsListens on 127.0.0.1 only. Serves the page's own files, stores ciphertext on disk, runs Sign in with ChatGPT, and passes assistant requests on. It cannot read your record.
The desktop shelldesktop/main.cjsAn Electron app. It starts the same server.mjs as a utility process on a free loopback port, opens the window, locks down what the window may reach, and checks for updates.
The reader helper (optional)offload, bundled in the Apple silicon appReads documents with Apple's on-device models. Only runs on Apple silicon with macOS 27. Everything it does has a built-in fallback. See Reading documents.

What talks to what

  1. You work in the page, inside the Electron window.
  2. The page encrypts and decrypts on its own. It sends ciphertext, and nothing else, to the local server to be saved.
  3. The local server writes ciphertext files under the data folder and answers the page. For the assistant it forwards the already scrubbed conversation to the provider you chose.
  4. The reader helper, when present, is started by the server and fed document bytes through a pipe, in memory. It has no network access.
  5. The desktop shell is the only part that contacts anything else: the update check to GitHub, from its own session, never from the page.

The page and the server are the same code whether you run the desktop app or start the server from a terminal. Families should use the desktop app: only it has the request filter, the helper and updates.

The vault on disk

The vault is a file, not browser storage. The data folder is ~/.health-picture. The name is kept on purpose from before the app was called Ovillo, so existing vaults carry on.

~/.health-picture/
  vault.json            the first person's encrypted record
  blobs/blob-<uuid>     that person's encrypted documents and originals
  people.json           ids and nicknames, in plain text
  people/p-<16 hex>/    one folder per further person, with its own vault.json and blobs/

One vault per person, each with its own passphrase. Records are never merged or moved between people, and the server keeps at most 20 people. Every vault and blob request names the person it is for, and the server refuses a person who is not in people.json.

Files are written whole to a side file, with permissions that only your user can read, and renamed into place, so a crash never leaves half a file. Saves are optimistic: each save carries the fingerprint of the file it was based on. If the file changed in another window, the server answers with a conflict and the page locks and tells you, instead of overwriting. See Privacy and security for the cryptography.

The local server's routes

Static files are answered for GET and HEAD only, from a fixed list of top-level files, the modules directly in src/ (the src/providers/ folder is server-only and never served) and the bundled vendor/ assets. Everything under /api/ is a small fixed set:

RoutePurpose
/api/vaultRead, save and delete the encrypted record.
/api/blobs/<id>Read, save and delete encrypted document files.
/api/people, /api/infoThe people list, and where the files live.
/api/agent/stepThe only route out to an assistant provider.
/api/models, /api/byok/checkList models and check an address, for the same two providers.
/api/auth/chatgpt/* and the loopback sign-in callbackSign in with ChatGPT. Sign-in attempts live in server memory for ten minutes and are handed over once. Nothing is written to disk.
/api/readPasses document bytes to the reader helper, when there is one.

Any other path or method gets a refusal.

The request filter and the page policy

Three layers keep other software, and other websites, away from the server and the page from reaching out.

  1. The API guard. Every API route first checks that the Host is 127.0.0.1 or localhost with the right port (this blocks DNS rebinding), that the request carries the app's own header, and, for anything that changes data, that the Origin is the page itself and the body is JSON or binary. The custom header and body type force a browser preflight that the server never answers, so another site cannot call the API.
  2. The page's content security policy. Every response carries one. Scripts, styles, workers, images and connections are allowed only from the app itself (plus blob: and, for images, data:). Framing and plugins are off, and the referrer is never sent. The page cannot make a request to any other address, which is why even a hostile document could not phone home from the page.
  3. The window's filter. The Electron window cancels every request that is not the local server, a data: address or one of its own blob: addresses. Links you click open in your normal browser. Sign in with ChatGPT happens there too.

The only network routes

Ovillo has no account, no analytics and no server of its own. There are exactly two ways data can leave the Mac:

There are no runtime downloads of fonts or models: the reading engines and models ship inside the app and are loaded from its own files.

Session and locking

While unlocked, the key and the decrypted record live in the page's memory. The page counts "sessions" with an epoch number. Locking raises it, clears the record and key, empties form fields and timers, and revokes local document addresses. Any import, save or restore that was still running from an older session is refused when it tries to finish, so it cannot write into a locked vault.

How the page draws your documents

Text from documents, filenames and your own notes is put on screen as plain text, never as HTML. The page does not insert document content as markup. The one generated HTML string is the appointment brief you download, and every value in it is escaped. PDFs open in an in-app viewer drawn on a canvas, since the browser's own PDF plugin is not used.