Passkey = Key
Your passkey derives the encryption key via WebAuthn PRF. No recovery key to lose or store.
npm install @ursalock/client @ursalock/cryptoPasskey = 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 passkeyconst vaultClient = new VaultClient({ serverUrl: "https://vault.example.com" });const result = await signIn({ usePasskey: true });
// 2. Get vault and derive keysconst 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 DocumentClientconst docClient = new DocumentClient({ serverUrl: "https://vault.example.com", vaultUid, encryptionKey: keys.encryptionKey, hmacKey: keys.hmacKey, getAuthHeader: () => vaultClient.getAuthHeader(),});
// 4. Store and retrieve encrypted documentsconst 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 passkeyconst 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 serverconst 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);});| Package | Description | Size |
|---|---|---|
@ursalock/crypto | Encryption primitives (AES-256-GCM, HKDF) | ~4 KB |
@ursalock/client | Auth + DocumentClient + React hooks | ~8 KB |
@ursalock/server | Self-hosted backend | ~8 KB |
@ursalock/agent | Agent 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.