Confidential Chat
A minimal Next.js chat whose messages are encrypted in the browser to a confidential-hardware enclave, after the browser has verified that enclave's attestation. A small server route holds the API key and forwards ciphertext it cannot read, through Nito to an enclave running Tinfoil's published router code. The whole protocol fits in three short files.
Quick start
Requires Node.js 24, version 24.22.1 or later.
1. Create the app
npx -y create-solana-dapp@latest -t gh:solana-foundation/templates/community/confidential-chatIf the dependencies were not installed for you, run npm install in the new folder.
2. Get a Nito API key
- Sign up at app.trynito.ai/signup with an email address, Google, or X.
- The API is prepaid, on any plan including Free. Add funds on Billing.
- Open the Developer Console at app.trynito.ai/api, choose API keys, then Create key. Name it, pick an expiry, and set a spend cap (optional, but do). Nito needs a funded organization and an owner or admin role to create a key.
- Copy the key. It starts with
z_sk_and is shown only once.
3. Paste it
cp .env.example .env.localSet NITO_API_KEY=z_sk_... in .env.local. Only the server reads it.
4. Run
npm run dev5. Chat
Open http://localhost:3000 and send a message. The status line shows each step as it happens. Under each reply, the page shows what it verified: the repository and release tag of the enclave's code, and the release digest. It also shows the host the request was routed to, which is not part of the proof.
How it works
browser 1. asks your server for the enclave's attestation
2. verifies it, then encrypts the chat to the enclave's key
│
▼ ciphertext
your server adds the API key and forwards the same bytes
│
▼ ciphertext
Nito relays them to the enclave
│
▼
enclave decrypts, runs the model, encrypts the reply
│
▼ ciphertext
browser decrypts the replyThree files hold the protocol:
lib/enclave.tsis the browser's side.verifyEnclavechecks the attestation bundle with@tinfoilsh/verifier, offline, against the repository pinned in that file. If the check fails, nothing is encrypted or sent.sealedChatencrypts an OpenAI-style chat request to the verified key withehbp, sends it, and decrypts the reply.app/api/attestation/route.tsfetches the current attestation from Nito and returns only the bundle.app/api/chat/route.tsaccepts only ciphertext of a bounded size, checks the two headers it forwards, adds the API key and the model header, and passes the encrypted reply back with its response nonce. It never reads or logs the body.
app/page.tsx is the chat screen, and lib/model.ts names the model (GPT-OSS 120B) under the two names Nito and the enclave use for it. The protocol is documented in Nito's Encrypted Requests page.
What this protects and what it does not
- Protected: your server and Nito carry the prompts and replies as ciphertext. Neither can read them.
- Not hidden: that a message was sent, when, and how large the request and reply were. Your server sees each visitor's IP address. Nito sees the time, the model, your account, and the size of each encrypted request.
- What you rely on:
- The page code you serve. The browser runs whatever your server sends it, including the code that verifies and encrypts. A changed page could send plaintext.
- The hardware vendor's attestation. The verifier trusts the chip maker's signature on the enclave's report.
- Tinfoil's published router code. Verification shows the enclave runs a published release of it, not that the release is free of bugs.
- The attestation is not tied to a fresh challenge. Verification shows the key belongs to a genuine enclave running a published release, not that the attestation was produced just now.
- The enclave reads the prompt. It has to, to run the model.
Before you ship this
This template leaves these out on purpose:
- Rate limiting and bot protection. Anyone who can reach
/api/chatspends your key. - Sessions or sign-in, if only your own users should spend it.
- A spend cap on the key. Nito lets you set a daily, monthly, or overall cap when you create a key. Set one.
- Streaming. Replies arrive whole.
- An output-token cap the server can enforce. The cap (
MAX_COMPLETION_TOKENSinlib/enclave.ts) is inside the encrypted request, so a hostile caller can send any value. The server cannot read the request. Only the byte cap, rate limits, and the key's spend cap bound what a caller can spend.
If something fails
- "The server has no API key":
NITO_API_KEYis missing from.env.local, or the server started before you set it. Restartnpm run dev. - "Nito answered with an error (HTTP 401)": the key is missing, mistyped, expired, or revoked.
- "Nito answered with an error (HTTP 429)": the account has no funds, or you are over a rate limit. See Nito's Quickstart.
- "The enclave failed verification, so nothing was sent": the attestation did not verify against the pinned repository. No prompt left the browser.
Scripts
npm run dev # development server
npm run build # production build
npm run lint # ESLint
npm run typecheck # TypeScript