Skip to content

ursalock

Passkey-powered E2EE document storage. No recovery key. Self-hostable.
Terminal window
npm install @ursalock/client @ursalock/crypto

Passkey = Key

Your passkey derives the encryption key via WebAuthn PRF. No recovery key to lose or store.

Zero-Knowledge

Data encrypted client-side before leaving browser. Server stores encrypted ciphertext only.

Cross-Device Sync

Same passkey = same encryption key. Sync seamlessly across devices.

Self-Hostable

Single Docker image, SQLite storage. Your data, your server.

import { VaultClient, DocumentClient } from "@ursalock/client";
import { deriveVaultKeys } from "@ursalock/crypto";
// 1. Authenticate with passkey
const vaultClient = new VaultClient({ serverUrl: "https://vault.example.com" });
const result = await signIn({ usePasskey: true });
// 2. Get vault and derive keys
const res = await vaultClient.fetch("/vault/by-name/my-app");
const { uid: vaultUid } = await res.json();
const masterKey = base64urlToBytes(result.credential.cipherJwk.k);
const keys = await deriveVaultKeys(masterKey, vaultUid);
// 3. Create DocumentClient
const docClient = new DocumentClient({
serverUrl: "https://vault.example.com",
vaultUid,
encryptionKey: keys.encryptionKey,
hmacKey: keys.hmacKey,
getAuthHeader: () => vaultClient.getAuthHeader(),
});
// 4. Store and retrieve encrypted documents
const notes = docClient.collection<Note>("notes");
const doc = await notes.create({ title: "Hello", content: "World" });
const fetched = await notes.get(doc.uid);
console.log(fetched.content); // { title: "Hello", content: "World" }
import { useSignIn } from "@ursalock/client";
const { signIn } = useSignIn(vaultClient);
// User taps passkey
const result = await signIn({ usePasskey: true });
if (result.success) {
// result.credential.cipherJwk → master key for key derivation
// result.credential.jwt → auth token
await initVault(result.credential);
}
import { create } from "zustand";
const useStore = create<AppState>((set) => ({
notes: [],
addNote: (note) => set((s) => ({ notes: [...s.notes, note] })),
}));
// Pull from server
const docs = await docClient.collection<AppState>("app-state").list({ limit: 1 });
if (docs[0]) {
useStore.setState(docs[0].content);
}
// Push changes (debounced)
useStore.subscribe((state) => {
debouncedSync(state);
});
PackageDescriptionSize
@ursalock/cryptoEncryption primitives (AES-256-GCM, HKDF)~4 KB
@ursalock/clientAuth + DocumentClient + React hooks~8 KB
@ursalock/serverSelf-hosted backend~8 KB
@ursalock/agentAgent SDK for AI/bot access~3 KB

Share derived keys with AI agents so they can read/write your encrypted data:

import { deriveVaultKeys, bytesToBase64 } from "@ursalock/crypto";
// Derive keys from your passkey (done once in the browser)
const masterKey = base64urlToBytes(cipherJwk.k);
const keys = await deriveVaultKeys(masterKey, vaultUid);
// Share these with your agent:
// - API key (created via POST /auth/api-keys, scoped to vault + collection)
// - bytesToBase64(keys.encryptionKey)
// - bytesToBase64(keys.hmacKey)

The agent uses @ursalock/agent to encrypt/decrypt client-side. The server never sees plaintext.

Agent Access Guide