@encra/client
Framework-agnostic Encra client for Node.js, Svelte, Vue, vanilla JS — anywhere React hooks don't fit. Same features as @encra/react with an event-emitter API instead of hooks. Multi-device, persistent ratchet state, automatic reconnection.
💡 Using React?
@encra/react instead — it wraps EncraClient in idiomatic hooks. This package is for non-React environments.Installation
npm install @encra/clientConstructor
import { EncraClient } from '@encra/client'
const client = new EncraClient({
apiKey: process.env.ENCRA_API_KEY!,
userId: 'alice',
// serverUrl is optional — defaults to Encra managed server
})| Prop | Type | Default | Description |
|---|---|---|---|
| apiKey* | string | — | Your Encra API key from the dashboard. |
| userId* | string | — | Unique identifier for the current user. Used as the key registration ID. |
| serverUrl | string | https://api.encra.dev | Key server base URL. Override for self-hosting. |
connect / disconnect
connect() generates or restores the key pair from IndexedDB, registers the public key with the server, and opens the WebSocket connection. It resolves once the socket is open and the ready event fires.
await client.connect()
// Later, clean up
client.disconnect()| Prop | Type | Default | Description |
|---|---|---|---|
| connect() | Promise<void> | — | Initialise crypto, register key, and open WebSocket. Safe to call once. |
| disconnect() | void | — | Close the WebSocket, cancel reconnects, and clear in-memory state. Does not delete IndexedDB data. |
| isReady | boolean | — | True when the WebSocket is connected and ready to send. |
| isConnecting | boolean | — | True while connecting or reconnecting. |
| deviceId | string | null | — | Stable device ID (UUID) generated once per browser/device. Available after connect() resolves. |
| messages | Message[] | — | All messages received in this session (max 200), newest last. |
| error | Error | null | — | Set if key registration fails during connect(). |
Events
EncraClient is a typed event emitter. Subscribe with on() and unsubscribe with off(). Both return this for chaining.
client
.on('ready', () => console.log('connected, deviceId:', client.deviceId))
.on('connecting', () => console.log('reconnecting…'))
.on('disconnected', () => console.log('lost connection'))
.on('message', (msg) => console.log(msg.from, ':', msg.text))
.on('error', (err) => console.error('error:', err.message))
.on('wire', (ev) => console.log('wire', ev.direction, ev.ciphertext.slice(0, 12)))| Prop | Type | Default | Description |
|---|---|---|---|
| ready | () => void | — | Fired when the WebSocket connects and registration succeeds. |
| connecting | () => void | — | Fired when a reconnection attempt starts (after a disconnect). |
| disconnected | () => void | — | Fired when the WebSocket closes. Reconnection begins automatically. |
| message | (msg: Message) => void | — | Fired when an incoming message is successfully decrypted. |
| error | (err: Error) => void | — | Fired on decryption failures or WebSocket errors. Connection is not closed. |
| wire | (event: WireEvent) => void | — | Low-level event fired for every sent/received ciphertext. Useful for debugging. |
sendMessage
Encrypts text with a Double Ratchet step and sends it to all registered devices of to. Each device receives an independently encrypted copy.
// Wait for 'ready' before sending
client.on('ready', async () => {
await client.sendMessage('bob', 'Hello Bob!')
})| Prop | Type | Default | Description |
|---|---|---|---|
| sendMessage(to, text) | Promise<void> | — | Encrypt and deliver text to all devices of the recipient. Throws if the WebSocket is not open. |
encryptFile / decryptFile
Encrypt a File or Blob for a recipient. The result contains one encrypted copy per registered device. The recipient calls decryptFile — the correct device entry is picked automatically. Maximum file size is 50 MB.
// Sender
const encrypted = await client.encryptFile(file, 'bob')
// encrypted.name — original filename
// encrypted.mimeType — original MIME type
// encrypted.size — original size in bytes
// encrypted.devices — one entry per device of 'bob'
// Store / transmit encrypted (JSON-safe after converting Uint8Arrays to base64)
// Recipient
const file = await client.decryptFile(encrypted, 'alice')
// Returns a File with original name and type restored| Prop | Type | Default | Description |
|---|---|---|---|
| encryptFile(file, to) | Promise<EncryptedFile> | — | Encrypt a File or Blob for all registered devices of 'to'. Throws RangeError if file exceeds 50 MB. |
| decryptFile(encrypted, from) | Promise<File> | — | Decrypt an EncryptedFile from 'from'. Automatically finds this device's entry. Throws DecryptionFailedError if not found. |
encryptFields / decryptFields
Encrypt a flat object of string values — form submissions, medical records, PII — for a recipient. Field names are plaintext; only values are encrypted. Each device of the recipient gets an independently encrypted copy with per-field unique nonces.
// Sender
const encrypted = await client.encryptFields(
{ name: 'Alice', ssn: '123-45-6789', notes: 'Confidential' },
'doctor'
)
// encrypted.devices — one entry per device of 'doctor', each with per-field ciphertext
// Recipient (after receiving encrypted via your transport)
const fields = await client.decryptFields(encrypted, 'alice')
console.log(fields.ssn) // '123-45-6789'
console.log(fields.notes) // 'Confidential'| Prop | Type | Default | Description |
|---|---|---|---|
| encryptFields(fields, to) | Promise<EncryptedFields> | — | Encrypt a Record<string, string> for all devices of 'to'. Each field gets a unique random nonce. |
| decryptFields(encrypted, from) | Promise<Record<string, string>> | — | Decrypt an EncryptedFields object from 'from'. Returns the original key→plaintext mapping. |
TypeScript types
import type {
EncraClientOptions,
Message,
WireEvent,
EncryptedFile,
EncryptedFields,
DeviceKey,
} from '@encra/client'
// Message shape
interface Message {
from: string // sender's userId
text: string // decrypted plaintext
timestamp: number // Date.now() at receipt
}
// WireEvent — low-level sent/received ciphertext
interface WireEvent {
direction: 'sent' | 'received'
ciphertext: string // URL-safe base64
nonce: string // URL-safe base64
timestamp: number
}
// Maximum file size
import { MAX_FILE_BYTES } from '@encra/client'
// MAX_FILE_BYTES === 50 * 1024 * 1024Examples
Svelte
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import { EncraClient } from '@encra/client'
import type { Message } from '@encra/client'
export let userId: string
export let recipientId: string
let client: EncraClient
let messages: Message[] = []
let isReady = false
let text = ''
onMount(async () => {
client = new EncraClient({
apiKey: import.meta.env.VITE_ENCRA_API_KEY,
userId,
})
client
.on('ready', () => { isReady = true })
.on('message', (msg) => { messages = [...messages, msg] })
await client.connect()
})
onDestroy(() => client?.disconnect())
async function send() {
if (!text.trim() || !isReady) return
await client.sendMessage(recipientId, text)
text = ''
}
</script>
{#each messages as msg}
<p><strong>{msg.from}</strong>: {msg.text}</p>
{/each}
<input bind:value={text} on:keydown={(e) => e.key === 'Enter' && send()} />
<button on:click={send} disabled={!isReady}>Send</button>Node.js (backend-to-backend)
import { EncraClient } from '@encra/client'
// Useful for server-side notifications: send encrypted alerts to a user
const bot = new EncraClient({
apiKey: process.env.ENCRA_API_KEY!,
userId: 'system-bot',
})
bot.on('ready', async () => {
await bot.sendMessage('alice', 'Your export is ready.')
bot.disconnect()
})
await bot.connect()Vue 3 (Composition API)
import { ref, onMounted, onUnmounted } from 'vue'
import { EncraClient } from '@encra/client'
import type { Message } from '@encra/client'
export function useEncra(userId: string) {
const client = new EncraClient({ apiKey: import.meta.env.VITE_ENCRA_API_KEY, userId })
const messages = ref<Message[]>([])
const isReady = ref(false)
client
.on('ready', () => { isReady.value = true })
.on('message', (msg) => { messages.value = [...messages.value, msg] })
onMounted(() => client.connect())
onUnmounted(() => client.disconnect())
return { messages, isReady, sendMessage: client.sendMessage.bind(client) }
}