# Todo example

```sh
bunx @revnu/fuast new my-todos --template todo
```

A personal todo list. Each person, guest or account, sees only their own todos. On a public app, a guest's todos move to their account when they sign in.

## What it shows

- A `todos` table with `text`, `done: boolean().default(false)`, and `ownerId: userId()`, indexed `by_owner` and `by_owner_done`.
- A `todos` query filtered by the caller from `ctx.auth.requireIdentity()`.
- `addTodo`, `setTodoDone`, `removeTodo`, and `clearDone` mutations that check ownership before every change.
- WebMCP `tool` options on every handler, so a browser agent can list, add, complete, and clear todos. Handlers with tool `args` take one object, and the client calls them with one object.
- A `GET /api/status` endpoint that counts the caller's open todos with `count()`.
- Two client routes: `/` for the list and `/status`, which calls the endpoint with `X-Fuast-Token`.
- `shared/todo.ts`, a text cleanup helper both sides use.

## Server pattern

```ts
todos: query(
  async (ctx) => {
    const { userId } = ctx.auth.requireIdentity();
    return ctx.db.todos.withIndex("by_owner", (q) => q.eq("ownerId", userId)).order("desc").collect();
  },
  { tool: { description: "List the current user's todos, newest first." } }
)
```

```ts
setTodoDone: mutation(
  async (ctx, args) => {
    const { userId } = ctx.auth.requireIdentity();
    const todo = await ctx.db.todos.get(args.todoId);
    if (!todo || todo.ownerId !== userId) throw new Error("Todo not found.");
    await ctx.db.todos.update(args.todoId, { done: args.done });
  },
  { tool: { description: "Mark a todo done or not done.", args: { todoId: id("todos"), done: boolean() } } }
)
```

Use this shape whenever rows belong to one person. The client never receives rows it does not own, and every write re-checks the owner.

## Client pattern

```tsx
const todos = client.useQuery("todos");

{todos === undefined ? null : todos.length === 0 ? (
  <EmptyState title="No todos yet" hint="Add your first one above." />
) : (
  <div className="transition-opacity duration-200 starting:opacity-0">...</div>
)}
```

The add form keeps the draft if the write fails and clears it on success only if it still holds the submitted text.

## Try it

```sh
bunx @revnu/fuast dev
```

Open `?fuast_user=alice` and `?fuast_user=bob` in two tabs: their lists are separate. Open `?fuast_guest=1` to try a guest. Then:

```sh
bunx @revnu/fuast db dump --port 3000
curl http://localhost:3000/api/status
```

Set `auth: { requireSignIn: true }` to require an account. Use `ctx.auth.requireSignedIn()` instead to require one for a single operation.
