End-to-end encryption, with the keys on your own devices
Happier keeps one coding session in sync between the computer it runs on and every device you watch it from, and a relay server sits in the middle of that. This page is the architecture under that sentence: which key is made where, what the relay actually holds, what it can still see, and what changes on a server configured to turn encryption off.
How a message is encrypted between your phone and your computer
A message you send from your phone, drawn at the three points it passes through. Everything the agent says back takes the same path in the opposite direction, sealed on your computer before it leaves.
- Your phone
- Sealed here. The message is encrypted on the device you typed it on, under a key that belongs to that session and nothing else — 32 random bytes drawn when the session is created, AES-256-GCM, a fresh nonce per message. The session key is then sealed to your account’s own key with an ephemeral X25519 exchange, and it is the sealed copy the relay is handed.
- The relay
- Carried sealed. What the server writes is a row shaped { t: "encrypted", c: "…" } — base64, plus a sealed key envelope it has no secret key for. It can route that row, count it, keep it and hand it back. It has nothing to open it with, and no endpoint asks it to.
- Your computer
- Opened here. The CLI opens the sealed session key with the account key it was given when you connected the terminal, decrypts, and passes the plain text to Claude Code, Codex, OpenCode or whichever agent is running. The reply makes the same trip in reverse, sealed before it leaves.
What the relay server stores
Encryption is a claim about content, and a claim about content is only half an answer. Here is the other half, at the same level of detail: the columns a server operating this relay can read without a key.
- Ciphertext, and a sealed key envelope
- One row per message, holding base64 and nothing else, plus the session’s wrapped data key. The wrapping is done on a device; no server route produces one.
- Identifiers and shape
- Account, device and session ids, a random tag per session, sequence numbers, timestamps and sizes. This is the part encryption does not cover on any system that syncs, and it is worth being plain about: the relay knows how much you sent and when.
- Which side each message came from
- Every message carries a role — user, agent, event or unknown — supplied by the client alongside the ciphertext. It is a column so a transcript can be paged and counted without being opened. On an encrypted row the server cannot derive it; it can only store what the client filed.
- A deliberately small activity projection
- Whether a session is unread, how many permission requests are waiting, how many questions the agent has asked, whether it is thinking, how the last turn ended. Every one of those is derived state — a counter, a flag or a timestamp the client writes as a session moves — never a word of what was said. It exists so a phone that has been shut for an hour shows a correct badge the moment you open it, and counting is the most the server is given to do that with.
- What you typed, and what the agent replied
- The transcript is the ciphertext. There is no second copy, no server-side index over it, and no summarisation step that would need one.
- Where the session is running
- The project path, your hostname, your home directory and the CLI version are session metadata, and metadata is encrypted with the same session key before the session is created. They are fields inside a sealed blob, not columns the server can sort on.
- Your provider credentials
- A connected account — the Claude or ChatGPT credential a session runs under — is sealed on the device before it is uploaded, under a key derived for that one purpose from your account secret. The relay stores an envelope and hands it back to your own devices.
- Your repository
- Your repository is never synced or mirrored. The agent reads and writes files on the computer it runs on, and that computer stays the only full copy. When you ask for a file, a folder or a diff, it travels the same sealed path as everything else — because you asked for it, not because Happier keeps a copy.
Your encryption keys are created on your own device
There is one secret at the root of your account, and it is created on a device — not issued by a server. Everything else is derived from it by a key tree: an HMAC-SHA-512 root, then one HMAC step per labelled path element. The keypair that opens sealed session keys comes out of that tree under the label "content", and so does a separate key for each other kind of stored blob, so no key is doing two jobs.
This is why a brand-new browser can sign in to your account and still show you nothing from last week. Signing in proves who you are to the relay. Reading an old session needs the key, and the key is not on the relay to send. Restoring the device is the step that moves it — from a device that already has it, or from the secret key you kept.
It also means the loss case is real and worth stating once: if every signed-in device is gone and the secret key is gone with them, the encrypted sessions cannot be recovered by us or by anyone. A relay that could hand them back would be a relay that could read them.
Linking a new device, and how your keys reach it
Adding a device is the moment key material moves, so it is the moment worth understanding. Scanning the QR code raises a request, and that is all it does: the approval happens on a device that is already signed in, and when both screens show a short confirmation code you are meant to compare them before approving.
What the approval actually does is seal the content key to the public key of the device that asked, in an envelope carrying an ephemeral sender key and a fresh nonce. That envelope travels through the relay like everything else does. Connecting a terminal adds one more binding on top: the payload is authenticated against a pairing secret that travelled in the QR code, not through the server, so a relay that swapped the payload could not produce a matching tag.
Storage policy: end-to-end encryption is the default
Everything above describes the default, and the default is not a suggestion the client can talk the server out of. A fresh relay is set to require end-to-end encrypted storage: a plaintext write is refused with a policy error rather than quietly accepted. Clients read the policy from the server’s own features endpoint before they create a session, so the mode a session is in was agreed before its first message.
A server operator has two other settings available. One can allow an account to choose, so encrypted and plaintext sessions coexist on the same relay — it takes both the optional policy and the opt-out allowance, and the allowance is off unless the operator turns it on. The other requires plaintext, for organisations that manage encryption at the infrastructure layer and need the server able to read and process session content. That last one is a real trade and it is worth saying without softening: on a plaintext session the server can read the transcript. That is the point of the setting.
The mode is fixed per session at creation, so one session is never half of each, and changing an account’s mode changes what happens to new sessions rather than reaching back into old ones. Nothing is retroactively decrypted, and nothing is retroactively encrypted either.
The variables that set all three, the at-rest options underneath them and the identity controls around them are the operator’s half of this, and they live on the self-hosting page for teams.
Push notifications are sent by your own computer
A push notification is the one thing that leaves the model behind, because it has to arrive at a phone that has nothing open. Happier’s answer is that your own computer sends them. The server stores your push tokens; the CLI on your own computer reads them back and sends the notification itself, which keeps the relay out of a path it would otherwise have to be told the contents of.
What the payload carries is deliberately thin: the tool or request type, and a short hint where one helps — a file such as src/file.ts, a command name, a count like "3 questions". Raw tool input is not put into it. Enough to decide whether to reach for the phone, on a screen anyone standing behind you can read.
Self-hosted relay: the metadata stays on your hardware too
All of this holds on a relay you do not run. The reason to run your own is the row above about identifiers and shape: encryption does not hide who is talking to whom, or how often, and on your own relay that ledger is yours too. One command puts the relay on hardware you own, and the path becomes your device, your relay, your computer, with nothing in it you have not deployed.
Read the encryption code yourself
There is no certification on this page because there is not one to report, and a security page that gestures at compliance it has not got is worse than one that does not mention it. What is here instead is the code every sentence above was written from, MIT-licensed and readable before you run it: the primitives in packages/protocol/src/crypto, the session encryption in apps/cli/src/api/encryption.ts, the storage policy the server enforces in its features endpoint, and the schema that shows exactly which columns are plaintext.
That is also the honest answer to how you should treat this page: as a map of where to look, not as a promise to be taken on trust. Read the four files, or run the relay yourself and read what it advertises to its clients.
The encryption model reference covers the same ground as procedure — what restore asks of you, what each storage mode means for an account, which flow to reach for when a device cannot read a session yet.

