Passkey = Key
Your passkey derives the encryption key via WebAuthn PRF. No recovery key to lose or store.
npm install @ursalock/zustand @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 opaque blobs 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 { create, type StateCreator } from "zustand";import { vault, type VaultOptionsJwk } from "@ursalock/zustand";import type { CipherJWK } from "@ursalock/crypto";
interface NotesState { notes: string[]; addNote: (note: string) => void;}
// After passkey authentication, you get a cipherJwkfunction createStore(cipherJwk: CipherJWK) { const storeCreator: StateCreator<NotesState> = (set) => ({ notes: [], addNote: (note) => set((s) => ({ notes: [...s.notes, note] })), });
return create(vault(storeCreator, { name: "my-notes", cipherJwk, server: "https://vault.example.com", getToken: () => client.getToken(), }));}
// Sync across devicesawait store.vault.sync();import { useSignIn } from "@ursalock/client";
const { signIn } = useSignIn(vaultClient);
// User taps passkeyconst result = await signIn({ usePasskey: true });
if (result.success) { // result.credential.cipherJwk → encryption key // result.credential.jwt → auth token initStore(result.credential.cipherJwk);}| Package | Description | Size |
|---|---|---|
@ursalock/crypto | Encryption primitives (AES-256-GCM) | ~4 KB |
@ursalock/zustand | Zustand middleware | ~6 KB |
@ursalock/client | Auth client + React hooks | ~7 KB |
@ursalock/server | Self-hosted backend | ~8 KB |