Skip to main content

Private Chat

This cookbook builds a private chat on SKALE — no one can read your messages except the people in the conversation. Messages are encrypted before they ever reach the blockchain, and they stay encrypted onchain. Only the chat participants hold the keys to decrypt them.
Normal chat apps either store messages in plain sight on the blockchain or hand encryption over to a server you have to trust. This example keeps messages locked end-to-end. No servers, no trusted third parties — just cryptography built into the chain itself.

How it Works

This tutorial needs a SKALE chain that supports private transactions. The demo runs on SKALE on Base Testnet. Check Programmable Privacy Availability for which chains support this.
Prerequisites:
  • Node.js 22+ and yarn/npm
  • A SKALE chain with privacy support
  • Basic familiarity with Solidity and TypeScript

Step 1: Clone the Project

Clone the bite-solidity repository. It contains multiple examples that demonstrate different programmable privacy features.
Install Dependencies & Build:
Deploy the Contract:
Fund Your Wallet: This process consumes gas tokens. Get free testnet ETH from the SKALE Base Sepolia Faucet.
Expected Output:
Project Structure:

Step 2: Smart Contract Overview

The contract is EncryptedMessenger.sol. It stores sessions and messages as ciphertext, and only handles plaintext inside the onDecrypt callback triggered by the protocol.
The helper in bite-solidity is called encryptECIES(). In this guide, we describe that step as re-encryption because that is the practical outcome: the contract receives protected data inside the callback, then locks it again for a specific user or session.
Core Data Structures:

Step 3: User Registration & Deposit System

Before a session can be created, both users register a long-term public key. The contract uses that key later to re-encrypt the shared session secret separately for each user. Registration:
Off-Chain (TypeScript):
Deposit System: The contract includes a pre-funding mechanism so users don’t need to attach Gas Tokens to every message transaction. The contract deducts from the deposited balance when a CTX is submitted.
Without deposits, every sendMessage call requires the user to calculate and attach the exact Gas Tokens amount for callback gas. Deposits improve UX by letting the contract handle that internally.
Users can withdraw their deposited Gas tokens at any time by calling withdraw() function.

Step 4: Create a Session with Encrypted Transactions and CTX

This is where the chat creates a shared session key. The public half can be stored openly, but the private half is encrypted offchain and sent into the contract through CTX so it never appears as plaintext in a normal transaction. The Session Secret: A fresh ephemeral key pair is generated as the session key. The session’s public key is passed as plaintext (it is the shared session encryption key), while the private key is the secret — it must travel into the contract securely. Off-Chain (TypeScript) — Encrypt the Session Secret:
On-Chain — Submitting the CTX:
What happens:
  1. encryptedSessionKey contains the protected session private key
  2. No single validator can decrypt it alone
  3. In the next block, the protocol resolves the CTX and calls onDecrypt

Step 5: Session Callback - Re-encrypt for Each User

When the protocol calls onDecrypt, the contract immediately re-encrypts the session secret for each participant using the public key they registered earlier.
Re-encryption flow inside the callback:
The session private key is only ever plaintext inside the onDecrypt callback. It is never stored in readable form onchain, only as ciphertext personalized for each user.

Step 6: Send Messages with Encrypted Transactions

Each message follows the same pattern. The app encrypts the message before submission, then the contract receives it through CTX. Off-Chain (TypeScript) — Encrypt the Message:
On-Chain — Submitting the Message CTX:

Step 7: Message Callback - Re-encrypt and Store

When the message CTX resolves, the protocol calls onDecrypt with the decrypted message bytes. The contract then re-encrypts them with the session public key and stores the ciphertext.
Complete message flow:

Step 8: Read and Decrypt Messages Offchain

To read chat history, each participant first unlocks their own copy of the session key, then uses that session key to unlock stored messages. Decryption Helper for Re-encrypted Data (TypeScript):
Step 1 — Retrieve and decrypt the session key:
Step 2 — Decrypt a stored message with the session key:
Full offchain read path:

Step 9: Demo UI Setup

A minimal Next.js demo app lives in demo/. It provides a side-by-side chat interface for two server-managed demo accounts. No browser wallet is required — the server signs all transactions using private keys from .env. Setup:
Environment Variables: Demo Project Structure:
This demo is custodial by design — the server holds both user private keys and signs transactions on their behalf. It is intended for local testing and demonstrations only, not production use.

Step 10: Run the Application

Open http://localhost:3000. The UI provides two side-by-side panes (User 1 / User 2), each with:
  1. Deposit Credits — Pre-fund callback gas so messages don’t require attaching Gas Tokens each time
  2. Register — Publish your secp256k1 public key onchain (enabled after depositing)
  3. Create Session — Trigger the CTX that bootstraps the shared session secret (enabled once both users are registered)
  4. Chat Window — Messages are shown as truncated ciphertext by default; click the unlock button to decrypt them inline
  5. Send — Type a message and send it via CTX
After the session is created (one block for the CTX callback), messages can be sent. Each message is also resolved in the next block after sendMessage is called.
Resources: