Selected work

Chatnor

Overview

A Norwegian AI assistant where the browser holds the keys and the servers hold ciphertext.

Details

At a glance

Project type
Encrypted assistant, EU hosted
My role
Set the privacy architecture and its invariants, and built the foundations the rest is held to.
Category
Systems
Status
In development2026

What I did

  • Separated the system into three planes and decided what each is allowed to hold
  • Wrote fifteen binding invariants, and the rule for what happens when a feature conflicts with one
  • Built the guardrails: one redacting logger, lint rules that fail the build, and tests that prove each rule fires
  • Wrote down the limits the product must never claim its way past

Built with

  • TypeScript: The whole system, with the boundaries typed
  • libsodium: Encryption performed in the browser, before anything is sent
  • PostgreSQL with pgvector: Ciphertext, metadata and vectors, with row-level tenant isolation
  • Zod: Every boundary validated, and a guard that rejects a request carrying a key
  • vLLM: Inference on private EU hardware, never exposed to the internet
  • Terraform and Ansible: The infrastructure, described rather than clicked

Next project

Virksom
A diagram of Chatnor's three planes: a control plane and a storage plane in Norway drawn as closed boxes, and between them a confidential processing plane in France drawn open at the bottom, because the plaintext there is held only in memory for the length of one request

The need

A Norwegian business that wants to use an AI assistant on its own work has one question before any other: where does what we type end up, and who could read it.

For most assistants the honest answer is that the content sits in plaintext on somebody else's servers, in another jurisdiction, under terms that can change. For a law firm, a clinic or a municipality, that answer ends the conversation.

Three planes

Chats, projects, files and notes are encrypted in the browser, before anything is sent. The control plane in Norway holds authentication, metadata, ciphertext and vectors. Storage holds ciphertext. Neither ever holds a readable message, and there is no route that would let one: adding a plaintext message persistence route is a named, forbidden change.

The middle plane is the interesting one. An answer has to be computed on something, so during a request the minimum plaintext required is processed on private hardware in the EU, held in memory, never written down and never logged. That is the whole exposure, it is one request long, and the architecture is drawn so it is visible rather than buried.

Decision

Write down what it is not

Context
Every product in this category is sold with the words end-to-end encrypted. For this one that would be almost true, and almost true is the most dangerous kind of security claim: it is the sentence a customer makes a legal decision on.
What I decided
The project keeps a short section called known limits, stated plainly, and it says the awkward things out loud. Browser-delivered cryptography cannot survive a malicious frontend deployment. The transient plaintext during a request is not end-to-end encryption and must never be described as such. Embedding vectors are stored unencrypted and can reveal meaning, so they are disclosed as privacy-sensitive. Someone who legitimately decrypted data before they were removed may already have copies, and rotating a key cannot revoke what a person already knows.
Trade-off
It makes the marketing harder and hands a competitor four sentences to quote. It also means a customer's own security review finds nothing the product had not already told them, which is the only version of this that survives contact with a data protection officer.
The Chatnor sign-in screen: a single email field under the heading "Logg inn" and the line "Vi sender deg en lenke. Ingen passord her."
How it works
  • Fifteen invariants are binding on everyone, including any coding assistant working in the repository. When a requested feature conflicts with one, the rule is to stop and write a decision note, never to weaken the invariant.
  • There is one logger, and it works from an allowlist by field name and by value shape. Tests prove that no planted secret can reach a log line.
  • Lint rules fail the build on console logging, request-body logging, web storage, eval, home-made cryptography and third-party loggers. Every rule has a test that proves the rule itself fires.
  • A guard at the boundary rejects any request carrying a vault password, a recovery code or a master key, so a mistake upstream is refused rather than accepted.
  • Identifiers are type-prefixed, so a mixed-up object type fails to decrypt instead of quietly succeeding.
  • Row-level security is fail-closed, and the tenant-isolation suite runs against a real database rather than a mock.
  • Migrations are raw SQL with checksums and advisory locking. Four environments are separated, validated at start-up, and a process exits rather than running on a bad one.
  • The sign-in link that skips email exists only when the environment is exactly local, and refuses to register itself anywhere else.
  • Anything touching cryptography, authentication, tenant authorisation, the inference gateway or file upload requires focused tests and a human review.

Where it stands

The foundations are built and proven: the guardrails, the contracts, the migrations and the isolation tests; the browser cryptography with its own test vectors; and sign-in with passkeys and the vault lock, whose stopping condition was demonstrated rather than assumed.

The rest of the product is not built. The independent review of the cryptography is deferred to before the first paying customer, and that deferral is written down as a dated decision with its reasoning, not left as a gap somebody might not notice.

What the project is really an argument for is that in a product like this the invariants come first, and the features are whatever can be built without breaking them.