Browse the docs

Examples

Todo example

The default template. Per-user todos with ownership checks, indexes, WebMCP tools, client routes, and a GET endpoint.

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

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." } }
)
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

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

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:

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.