Todo Example

This capsule shows personal todo lists for protected guests and signed-in accounts. Each browser session starts with its own list. Google sign-in keeps that list with the account.

What It Shows

Server Pattern

queries: {
  todos: query(async (ctx) => {
    const { userId } = ctx.auth.requireIdentity();
    return ctx.db.todos
      .withIndex("by_owner", (q) => q.eq("ownerId", userId))
      .order("desc")
      .collect();
  })
}

Use this pattern whenever rows belong to a single user. The client should not receive rows it does not own.

For mutations, fetch the row and check ownership before changing it:

const { userId } = ctx.auth.requireIdentity();
const todo = await ctx.db.todos.get(id);
if (!todo || todo.ownerId !== userId) {
  return;
}

await ctx.db.todos.update(id, { done });

The userId() field makes reference transfer automatic after a verified guest upgrade. No hook is needed for todos. Guest todos join any existing account todos. The field does not replace ownership checks.

Set auth: { requireSignIn: true } in the server to block data access until sign-in. For a single account-only operation, use ctx.auth.requireSignedIn() instead. The client uses isGuest and isSignedIn for UI state.

Clearing browser data or an expired guest session ends guest access. Sign in before that happens to keep account access to the todos. Local todos reset when the dev server restarts.

Run It

Run the checked-in example:

npx lakebed dev examples/todo

Open:

http://localhost:3000

To compare two named local test users, open:

http://localhost:3000/?lakebed_guest=alice
http://localhost:3000/?lakebed_guest=bob

Named overrides cannot upgrade to an account. Remove the query parameter to test guest sign-in and transfer. If you set a CLI override with npx lakebed auth as alice, run npx lakebed auth reset first.

Then inspect state:

npx lakebed db dump --port 3000
npx lakebed logs --port 3000