Skip to content

Example: To-do app

This content is for the 0.2.0-alpha.3 version. Switch to the latest version for up-to-date documentation.

A full to-do feature: sign in, create/toggle/delete encrypted todos, and stay in sync across the user’s devices via the change feed. Plain TypeScript + minimal React — drop the data layer into any framework.

src/todos.ts
import { Client } from "@muhkoo/connect";
export const client = new Client({
apiKey: "mk_live_pk_…",
// baseUrl defaults to the hosted accelerator (https://api.muhkoo.dev).
});
export interface Todo {
title: string;
done: boolean;
createdAt: number;
}
const COLLECTION = "todos";
export async function listTodos(): Promise<Array<{ id: string } & Todo>> {
const ids = await client.storage.list(COLLECTION);
const items = await Promise.all(
ids.map(async (id) => {
const t = await client.storage.get<Todo>(COLLECTION, id);
return t ? { id, ...t } : null;
}),
);
return items
.filter((t): t is { id: string } & Todo => t !== null)
.sort((a, b) => a.createdAt - b.createdAt);
}
export async function addTodo(title: string): Promise<void> {
const id = crypto.randomUUID();
await client.storage.set<Todo>(COLLECTION, id, {
title,
done: false,
createdAt: Date.now(),
});
}
export async function toggleTodo(id: string): Promise<void> {
const t = await client.storage.get<Todo>(COLLECTION, id);
if (!t) return;
await client.storage.set<Todo>(COLLECTION, id, { ...t, done: !t.done });
}
export async function removeTodo(id: string): Promise<void> {
await client.storage.delete(COLLECTION, id);
}
src/Todos.tsx
import { useEffect, useState } from "react";
import { client, listTodos, addTodo, toggleTodo, removeTodo, type Todo } from "./todos";
export function Todos() {
const [todos, setTodos] = useState<Array<{ id: string } & Todo>>([]);
const [draft, setDraft] = useState("");
const refresh = () => listTodos().then(setTodos);
useEffect(() => {
refresh();
// Live-update when todos change on any of the user's devices.
const off = client.storage.on("change", (e) => {
if (e.collection === "todos") refresh();
});
return off;
}, []);
return (
<section>
<form
onSubmit={async (e) => {
e.preventDefault();
if (!draft.trim()) return;
await addTodo(draft.trim());
setDraft("");
refresh();
}}
>
<input value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="New todo…" />
<button type="submit">Add</button>
</form>
<ul>
{todos.map((t) => (
<li key={t.id}>
<label style={{ textDecoration: t.done ? "line-through" : "none" }}>
<input type="checkbox" checked={t.done} onChange={() => toggleTodo(t.id).then(refresh)} />
{t.title}
</label>
<button onClick={() => removeTodo(t.id).then(refresh)}></button>
</li>
))}
</ul>
</section>
);
}

The component assumes a signed-in user. Wire login wherever your auth UI lives:

await client.auth.zk.login(username, password);
// …then mount <Todos />
  • Every todo is encrypted on the device before storage — the server stores ciphertext keyed by a random id.
  • storage.on('change') keeps every open tab/device in sync with no polling.
  • There’s no server-side query, so we list() ids and fetch each. For a big list, keep a small plaintext index (see Storage → Querying).