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 todoA 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
todostable withtext,done: boolean().default(false), andownerId: userId(), indexedby_ownerandby_owner_done. - A
todosquery filtered by the caller fromctx.auth.requireIdentity(). addTodo,setTodoDone,removeTodo, andclearDonemutations that check ownership before every change.- WebMCP
tooloptions on every handler, so a browser agent can list, add, complete, and clear todos. Handlers with toolargstake one object, and the client calls them with one object. - A
GET /api/statusendpoint that counts the caller's open todos withcount(). - Two client routes:
/for the list and/status, which calls the endpoint withX-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 devOpen ?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/statusSet auth: { requireSignIn: true } to require an account. Use ctx.auth.requireSignedIn() instead to require one for a single operation.