@encra/react
React hooks for end-to-end encryption. Three hooks cover the most common use cases: real-time chat, file transfer, and form submission. All share the same cryptographic identity — one key pair per user, stored in IndexedDB.
Installation
npm install @encra/react@encra/react requires react ≥ 18 as a peer dependency.@encra/core is bundled automatically.useE2EChat()
Real-time end-to-end encrypted messaging over WebSocket. Handles key generation, server registration, connection management, Double Ratchet encryption, and automatic reconnection with exponential backoff.
import { useE2EChat } from '@encra/react'
function Chat({ userId, recipientId }) {
const {
messages,
isReady,
isConnecting,
sendMessage,
error,
} = useE2EChat({
apiKey: process.env.NEXT_PUBLIC_ENCRA_API_KEY!,
userId,
})
return (
<div>
{isConnecting && <p>Connecting…</p>}
{error && <p>Error: {error.message}</p>}
{messages.map((msg, i) => (
<div key={i} className={msg.from === userId ? 'mine' : 'theirs'}>
<strong>{msg.from}</strong>: {msg.text}
</div>
))}
<button
disabled={!isReady}
onClick={() => sendMessage(recipientId, 'Hello!')}
>
Send
</button>
</div>
)
}Options
| Prop | Type | Default | Description |
|---|---|---|---|
| apiKey* | string | — | Your Encra API key from the dashboard. Prefix with NEXT_PUBLIC_ in browser apps. |
| userId* | string | — | Unique identifier for the current user. Used as the key registration ID. |
| serverUrl | string | Encra server | Key server URL. Defaults to the Encra managed server. Override for self-hosting. |
| onError | (err: Error) => void | — | Called on recoverable errors such as decryption failures or WebSocket errors. |
Return values
| Prop | Type | Default | Description |
|---|---|---|---|
| messages | Message[] | — | Array of decrypted messages (sent + received), newest last. Capped at 200. |
| isReady | boolean | — | True once the key pair is registered and the WebSocket is connected. |
| isConnecting | boolean | — | True while connecting or reconnecting after a disconnect. |
| sendMessage | (to: string, text: string) => Promise<void> | — | Encrypt and send a message to another userId. Throws if not connected. |
| error | Error | null | — | Set if key registration fails. Recoverable errors are emitted via onError instead. |
interface Message {
from: string // sender's userId
text: string // decrypted plaintext
timestamp: number // Date.now() at receipt
}E2EChatProvider
Avoid passing apiKey and serverUrl to every hook by wrapping your app in the provider. Each useE2EChat call still requires a userId.
import { E2EChatProvider } from '@encra/react'
export default function Layout({ children }) {
return (
<E2EChatProvider apiKey={process.env.NEXT_PUBLIC_ENCRA_API_KEY!}>
{children}
</E2EChatProvider>
)
}useE2EFile()
Encrypt and decrypt File or Blob objects end-to-end. Uses the same X25519 key pair as useE2EChat — no extra setup needed. Files up to 50 MB are supported. The encrypted bytes can be uploaded anywhere (S3, your own server, IPFS) — only the recipient can decrypt them.
import { useE2EFile } from '@encra/react'
function FileTransfer({ userId }) {
const { encryptFile, decryptFile, isReady, error } = useE2EFile({
apiKey: process.env.NEXT_PUBLIC_ENCRA_API_KEY!,
userId,
})
async function handleUpload(file: File, recipientId: string) {
// Encrypt on the sender's device
const encrypted = await encryptFile(file, recipientId)
// encrypted.ciphertext — Uint8Array, upload this
// encrypted.nonce — Uint8Array, store alongside ciphertext
// encrypted.name — original filename
// encrypted.mimeType — original MIME type
// encrypted.size — original size in bytes
await uploadToServer(encrypted)
}
async function handleDownload(encrypted, senderId: string) {
// Decrypt on the recipient's device
const file = await decryptFile(encrypted, senderId)
const url = URL.createObjectURL(file)
// file.name and file.type are restored automatically
}
}Options
| Prop | Type | Default | Description |
|---|---|---|---|
| apiKey* | string | — | Your Encra API key. |
| userId* | string | — | Current user's ID. Shares the same key pair as useE2EChat and useE2EForm. |
| serverUrl | string | Encra server | Key server URL. Override for self-hosting. |
| onError | (err: Error) => void | — | Called on any initialisation error. |
Return values
| Prop | Type | Default | Description |
|---|---|---|---|
| encryptFile | (file: File | Blob, to: string) => Promise<EncryptedFile> | — | Encrypt a file for a recipient. Throws if the file exceeds 50 MB or the recipient's key cannot be fetched. |
| decryptFile | (encrypted: EncryptedFile, from: string) => Promise<File> | — | Decrypt an EncryptedFile. Returns a File with the original name and MIME type restored. |
| isReady | boolean | — | True once the key pair is initialised and registered. |
| error | Error | null | — | Set if key registration fails on mount. |
interface EncryptedFile {
ciphertext: Uint8Array // XSalsa20-Poly1305 ciphertext
nonce: Uint8Array // random 24-byte nonce
name: string // original filename
mimeType: string // original MIME type
size: number // original size in bytes
}
// Maximum accepted file size
import { MAX_FILE_BYTES } from '@encra/react'
// MAX_FILE_BYTES === 50 * 1024 * 1024 (50 MB)ℹ Metadata is plaintext
name, mimeType, and size are stored in plaintext so the recipient can display a preview before downloading. Only the file bytes are encrypted. If you need filename privacy, overwrite name before storing.useE2EForm()
Encrypt individual form field values before submission. Each field is encrypted independently with a unique random nonce, using an X25519 shared secret derived from the sender and recipient's key pairs. Field names (keys) are sent in plaintext — only the values are encrypted.
Ideal for HIPAA forms, legal submissions, secure surveys, or any case where your server must store data but must not be able to read it.
import { useE2EForm } from '@encra/react'
function MedicalForm({ userId }) {
const { encryptFields, decryptFields, isReady } = useE2EForm({
apiKey: process.env.NEXT_PUBLIC_ENCRA_API_KEY!,
userId,
})
async function handleSubmit(formData: FormData) {
const payload = await encryptFields(
{
name: formData.get('name') as string,
ssn: formData.get('ssn') as string,
notes: formData.get('notes') as string,
},
'doctor-userId', // recipient who can decrypt
)
// payload.name = { ciphertext: '...', nonce: '...' }
// payload.ssn = { ciphertext: '...', nonce: '...' }
// payload.notes = { ciphertext: '...', nonce: '...' }
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(payload),
})
}
// On the recipient's side:
async function handleRead(payload, patientId: string) {
const fields = await decryptFields(payload, patientId)
console.log(fields.ssn) // '123-45-6789'
console.log(fields.notes) // 'Private notes'
}
}Options
| Prop | Type | Default | Description |
|---|---|---|---|
| apiKey* | string | — | Your Encra API key. |
| userId* | string | — | Current user's ID. Shares the same key pair as useE2EChat and useE2EFile. |
| serverUrl | string | Encra server | Key server URL. Override for self-hosting. |
| onError | (err: Error) => void | — | Called on any initialisation error. |
Return values
| Prop | Type | Default | Description |
|---|---|---|---|
| encryptFields | (fields: Record<string, string>, to: string) => Promise<EncryptedFields> | — | Encrypt a flat object of string values for a recipient. Each field gets a unique nonce. |
| decryptFields | (encrypted: EncryptedFields, from: string) => Promise<Record<string, string>> | — | Decrypt an EncryptedFields object. Returns the original key→plaintext mapping. |
| isReady | boolean | — | True once the key pair is initialised and registered. |
| error | Error | null | — | Set if key registration fails on mount. |
// Each field is independently encrypted
type EncryptedFields = Record<string, {
ciphertext: string // URL-safe base64
nonce: string // URL-safe base64
}>💡 Field names are plaintext
"hiv_status"), hash them before passing toencryptFields.TypeScript types
import type {
// useE2EChat
Message,
WireEvent,
UseE2EChatOptions,
UseE2EChatResult,
E2EChatConfig,
// useE2EFile
EncryptedFile,
UseE2EFileOptions,
UseE2EFileResult,
// useE2EForm
EncryptedFields,
UseE2EFormOptions,
UseE2EFormResult,
} from '@encra/react'Examples
useE2EChat with NextAuth / Better Auth
import { useSession } from 'next-auth/react'
import { useE2EChat } from '@encra/react'
function Chat({ recipientId }) {
const { data: session } = useSession()
const { messages, isReady, sendMessage } = useE2EChat({
apiKey: process.env.NEXT_PUBLIC_ENCRA_API_KEY!,
userId: session?.user?.email ?? '',
})
}Send on Enter
const [text, setText] = useState('')
<input
value={text}
onChange={e => setText(e.target.value)}
onKeyDown={async e => {
if (e.key === 'Enter' && text.trim() && isReady) {
await sendMessage(recipientId, text)
setText('')
}
}}
/>useE2EFile with S3 upload
import { useE2EFile } from '@encra/react'
function SecureUpload({ userId, recipientId }) {
const { encryptFile, isReady } = useE2EFile({
apiKey: process.env.NEXT_PUBLIC_ENCRA_API_KEY!,
userId,
})
async function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0]
if (!file || !isReady) return
const enc = await encryptFile(file, recipientId)
// Serialize for upload — ciphertext and nonce are Uint8Arrays
const form = new FormData()
form.append('ciphertext', new Blob([enc.ciphertext]))
form.append('nonce', new Blob([enc.nonce]))
form.append('meta', JSON.stringify({
name: enc.name, mimeType: enc.mimeType, size: enc.size,
}))
await fetch('/api/upload', { method: 'POST', body: form })
}
return <input type="file" onChange={handleChange} disabled={!isReady} />
}⚠ One key pair per user
useE2EChat, useE2EFile, anduseE2EForm — share the same IndexedDB key pair for a given userId. A user has one cryptographic identity across all Encra hooks.