Browse the docs
Start
Agent start guide
Everything an AI agent needs to build, verify, deploy, and share a fuast app, in the order it needs it.
fuast runs small full-stack TypeScript apps called capsules and shares them with your coworkers like a doc. One CLI gives you a typed server, a Preact client, a reactive database, sign-in, workspaces, storage, schedules, email, and hosting. You write two files, run fuast dev, check the data, then fuast deploy.
You do not need an account, and you never wait for a person to log in. fuast deploy puts the app online as a preview and prints a claim link. Give that link to the person you are building for. They open it, sign in, and the app is theirs.
If you have no terminal, connect to the MCP server at https://fuast.com/mcp instead: its tools check, deploy, test, and share an app from file contents you send.
To have fuast at hand in every later session, the person can run curl -fsSL fuast.com/install | sh. It connects the MCP server to Claude Code, Codex, and Gemini CLI and saves the fuast skill. It is optional: nothing on this page needs it.
Start here
You need Bun 1.2 or later. Capsules do not need node_modules, and you never install app dependencies.
If the current directory is empty or a fresh repository, make it the capsule:
bunx @revnu/fuast initOtherwise create the capsule in a new directory:
bunx @revnu/fuast new my-app
cd my-appPick a starter with --template todo|guestbook|signature|approvals|live-demo (default todo). init keeps hidden files, README.md, LICENSE, AGENTS.md, and CLAUDE.md, and appends fuast's lines to an existing AGENTS.md, CLAUDE.md, and .gitignore. If the directory holds anything else, init stops and writes nothing, because fuast treats the whole directory as one app. New capsules get a git repository and first commit unless they are already inside one or you pass --no-git.
Read the capsule's AGENTS.md before editing. CLAUDE.md imports it with @AGENTS.md. For an existing capsule, skip creation and follow its AGENTS.md.
The rest of this page uses fuast for the CLI. If it is not on your PATH, use bunx @revnu/fuast.
Limits to check first
- fuast is in early access. Read support for supported platforms, compatibility, and persistence.
- Read limits before sizing an app. A handler can return at most 1000 rows and read at most 5000. An app stores at most 100000 rows and 64 MiB by default.
- App code cannot import npm packages or Node built-ins. Do not install dependencies, add a bundler, or add CSS files.
- Database fields:
string(),number(),boolean(),id("table"),userId(), andjson(). Chain.optional()or.default(value).json()cannot be indexed. - Type errors block deploys.
fuast checkandfuast deployrun the TypeScript checker on the whole capsule. - Nothing needs an account: not building, not running locally, not the first deploy. Without a login,
fuast deploycreates an unclaimed preview that lasts 72 hours. A person makes it permanent by opening the claim link. See deploy, hand over, and verify. - An unclaimed preview has no owner yet, so it has no workspace: everyone who opens it is a guest,
ctx.email,ctx.team, andctx.workspacefail with "Claim this app to use …", and schedules and jobs wait until it is claimed. Its quotas are smaller. See limits. - Once claimed, an app is private. Only the owner can open it until you run
fuast share. - Guests exist only on public apps and unclaimed previews. Private and workspace apps always have a signed-in account behind every browser call.
- Local data lives in
.fuast/devand survives restarts.fuast dev --resetwipes it.
App structure and APIs
server/index.tsdefault-exportscapsule({...}). Server files import onlyfuast/serverand relative files.client/index.tsxexportsfunction App(). Client files import onlyfuast/client,preact,preact/hooks,preact/jsx-runtime, and relative files.shared/is pure TypeScript used by both sides: validation, constants, types. No fuast imports, DOM, Node, or env there.- In the client,
import type app from "../server/index"and callcreateClient<typeof app>()once at module scope. ItsuseQuery,useMutation,useAction, andusePaginatedQueryinfer names, arguments, and results. - Database calls are async. Await or return every one. Declare indexes with
.index(name, fields)and read withwithIndex(name, q => ...), then.order("asc" | "desc")and one terminal:collect(),take(n),first(),paginate(opts), orcount(). Use the built-inby_creationindex for unfiltered lists. Do not use the legacywhere,orderBy,limit, orallmethods: they do not exist in v2, and the type check infuast checkrejects them. insertreturns the new row id. Rows carryid,createdAt, andupdatedAt.- Queries and actions cannot write. Write in mutations, writable endpoints, and schedules. Each mutation is one transaction that rolls back if it throws.
- Get the caller with
ctx.auth.requireIdentity()(guest or account) orctx.auth.requireSignedIn()(account only). Never accept a user id, email, or name as an argument. Filter user-owned rows by the caller'suserIdand re-check ownership before every update or delete. - Declare user references with
userId(), neverstring(). When a guest signs in, everyuserId()value moves to their account.userId()does not grant access by itself. - Shared data is a query without an owner filter. Never invent a fake shared user like
"global". - Set
auth: { requireSignIn: true }incapsule()to block guests everywhere. On the client, gate data components oncanAccessApp(). Hiding a button is not an access check. - If
auth.erroris set, show it with aretryAuth()button and<SignInButton />. Keep data components unmounted until access works. - Server secrets live in
.env.fuast.serverand are read withctx.env.NAMEinside handlers. Never put secrets inclient/orshared/. - Team features:
ctx.workspace.members()lists the people in the app's workspace,ctx.team.table(name)reads and writes tables every app in the workspace shares,ctx.email.send()sends email,ctx.storagestores files on the server, andschedulesrun cron jobs. See teams, email, storage, and schedules. - Use the router from
fuast/clientfor pages (Router,Routes,Route,Link,useParams). There is no file-based routing. - Use
endpoint({ method, path }, handler)for webhooks and other HTTP clients. Keep endpoint paths (/api/...) separate from page paths./api/call,/api/me,/storage/..., and/__fuast/...are reserved. - Endpoint credentials: same-origin browser
fetchcalls carry the app's cookie automatically. Other callers send the fuast identity token fromgetIdentity().token, or a personal token fromfuast token create(headless agents), in theX-Fuast-Tokenheader. App-scoped deploy tokens never identify a caller. TheAuthorizationheader belongs to your app: fuast passes it through untouched and never treats it as a fuast identity on an endpoint, so use it for webhook secrets you check againstctx.env. Endpoints run for any caller, even on private apps. See endpoint credentials. - To let browser agents use the app, pass
{ tool: { description, args } }as the second argument toquery,mutation, oraction. Buildargsfromstring(),number(),boolean(), andid("table"). Withargs, the handler takes one object and the client calls it with that object. Write tool descriptions for an agent: say what the tool does, what it returns, and where its inputs come from ("Get todo ids from the todos tool"). See agent tools. - For instant-feeling interactions,
client.useMutation(name).withOptimisticUpdate((store, ...args) => store.setQuery(query, queryArgs, updater))predicts the result until the server confirms it. Most forms do not need it. See optimistic updates. - The only static asset is the favicon:
favicon.svgorfavicon.icoat the root. Useclient.storage.upload()for user files.
UI rules
- Style with complete Tailwind class names in JSX. fuast compiles the CSS from your client files. Write
done ? "line-through" : "", never`text-${color}-500`, and use inline styles for values computed at runtime. Do not add CSS files, CSS modules, PostCSS, or a separate Tailwind build. - The design is yours and the person's. fuast has no house style and nothing to match. If the app should follow the visitor's light or dark setting, pair your colours with
dark:variants. - A query hook returns
undefineduntil its first result, usually within a few milliseconds. While it isundefined, render nothing for that section. Do not render "Loading...", spinners, or skeleton rows. - When data arrives, render it inside
<div className="transition-opacity duration-200 starting:opacity-0">so it fades in. Put that wrapper only in the loaded branch. - Treat an empty array as its own state. Say what is missing and how to add it. Never write
?? []before rendering a list, because it flashes the empty state while data loads. - Keep the form input when a write fails, and show the error next to the form. After a successful write, clear the input only if it still holds the text you submitted, so typing during the round trip is not lost.
- Query results are read-only and shared. Copy before sorting:
[...rows].sort(...). - Do not show a global loading screen while
useAuth().isLoadingis true. Render nothing.
External data and schedules
Call global fetch(url, init) inside a handler, never at module scope. Every handler kind can fetch. A mutation, writable endpoint, or schedule can fetch and write rows in the same call, so external data never needs to pass through the browser. Fetch time counts against the handler's 10 s wall-clock limit (5 s for actions) and holds the app's write lock, so ingest one small batch per call. A database rollback cannot undo an external request.
For periodic work, declare a schedule: schedules: { sync: schedule("*/15 * * * *", async (ctx) => { ... }) }. Cron is five fields in UTC. Schedules run as the system identity, so ctx.auth.userId is null. Keep a cursor row in a table so each run continues where the last one stopped.
Read budgets apply to the whole handler, and a loop of paginate() calls does not escape them. For totals larger than one handler can read, keep summary rows updated during ingest. Store timestamps as number() epoch milliseconds. See the schedule-driven ingest example.
Run and verify
Run every command from the capsule directory. Start dev in a terminal that stays open:
fuast devIt prints the local URL (default http://localhost:3000) and rebuilds the server and client on every save. Type errors show in that terminal and as an overlay on the app page. In another terminal:
fuast check # build + type check + contract check. Run it before every deploy.
fuast run todos # call a query, mutation, or action headlessly; prints JSON
fuast run addTodo '{"text":"Buy milk"}' # one JSON value per argument; tool handlers take one object
fuast db dump --port 3000 # current rows in every table
fuast logs --port 3000 # ctx.log output and handler errors with source-mapped stacks
fuast inspect --port 3000 # handlers, schema, tools, endpoints, schedules, read metrics
fuast inspect --query todos # rows read vs returned for one queryFix every error from fuast check and every handler error in fuast logs before deploying. Exercise each handler you wrote with fuast run or the browser, then confirm the rows with fuast db dump instead of assuming a write happened. If inspect --query shows a query reading far more rows than it returns, add an index that matches its filter.
Locally you are the signed-in user dev. Test other people with ?fuast_user=alice and ?fuast_user=bob in separate tabs, and test guests with ?fuast_guest=1. For headless checks, fuast auth as alice makes fuast run call as alice, and fuast auth reset restores dev. For private per-user data, check that alice cannot see bob's rows. See test and debug.
Deploy, hand over, and verify
Deploy as soon as fuast check passes. Do not ask the person to log in first:
fuast check
fuast deploydeploy repeats the local checks, uploads the source, type checks and builds it on the platform, applies schema changes, and puts the app online. If a schema change would delete a populated table, the deploy stops and tells you the --delete-table TABLE=COUNT flag to confirm it.
Without a login, the app goes online as an unclaimed preview, and deploy prints:
✓ deployed Lunch Orders as an unclaimed preview (dep_4k2…)
Preview: https://preview-k3v9q2m7xa.fuast.com/?fuast_preview=fup_…
Claim: https://fuast.com/claim/fuc_…
Expires: 2026-11-14T14:26:35.267Z (in 72 hours) unless claimed
Give the claim link to the person you are building for: they open it, sign in, and the app becomes theirs.
Inspect it from this folder with its slug: fuast logs preview-k3v9q2m7xaGive the person both links, and say which is which:
- The claim link makes the app theirs. They open it, sign in or create an account, and choose Claim. The app becomes a private app in their workspace, with the data it already holds. It works once. Give it only to the person the app is for.
- The preview link shows the app. It first opens a fuast page that says this is an unclaimed preview and when it is deleted, then the app. It cannot claim anything, so it is safe to pass on.
fuast deploy --json returns the same as { ok, unclaimed: true, url, claimUrl, expiresAt, slug, deployId, limits, redeploy, next }, where url is the preview link and next is the instruction above.
While the app is unclaimed:
- It is deleted 72 hours after the first deploy unless someone claims it. Redeploying does not extend that.
- It is not public and not listed anywhere. Only the preview link opens it.
- Its address is random (
preview-…). You cannot choose it. The person can pick the real address when they claim. - Everyone who opens it is a guest. Sign-in is off, so
ctx.auth.requireSignedIn()fails, and an app withauth: { requireSignIn: true }cannot be tried until it is claimed. Usectx.auth.requireIdentity()for data a guest should own. ctx.email.send(),ctx.team.table(), andctx.workspace.members()fail with an error that starts "Claim this app to use" (codeunclaimed). Catch it or expect it in your checks.- Schedules and delayed jobs are registered but do not run. Jobs queued while the app is unclaimed are cancelled when it is claimed, so do not rely on one to finish later.
- Claiming does not switch all of that on. See after the person claims the app.
- Stored rows, storage, daily requests, and outbound
fetchhave smaller limits. See limits.
deploy saves an edit token for the preview in ~/.fuast/previews.json on this machine, filed under the folder's path. It is what lets this machine redeploy and inspect the preview. Do not print it or send it to anyone. Nothing is written to the project folder, and the CLI never reads a credential from it: a .fuast/unclaimed.json that arrives with a repository is ignored with a warning. Run every later command from the same folder on the same machine:
fuast deploy # updates the same preview: same links, same expiry
fuast claim # prints the claim link and the preview link again
fuast status # logged in or not, and what this folder is deployed asVerify the preview the same way you verified dev. Inspection commands target fuast dev unless you name the app, so pass the slug deploy printed:
fuast inspect preview-k3v9q2m7xa
fuast run todos preview-k3v9q2m7xa
fuast logs preview-k3v9q2m7xa
fuast db dump preview-k3v9q2m7xafuast run calls a preview as a guest. Exercise the feature you built and read the logs. Then report the claim link, the preview link, when the preview expires, and the checks you ran.
After the person claims the app
The app now deploys with their account. Your next fuast deploy stops and says so:
fuast: This app was claimed by ana@co.com and now deploys with that account. Run `fuast login`, then `fuast deploy` again: it will update preview-k3v9q2m7xa and link this folder to it in fuast.json.Run fuast login. It prints a short code and opens a browser page. A person must sign in there, check that the code matches, and approve. You cannot approve it yourself: tell the person what to do and wait for the command to finish (it waits up to 10 minutes). Set BROWSER=none to print the URL without opening a browser.
Then run fuast deploy again. It deploys to the claimed app and writes fuast.json with its slug. Commit fuast.json so later deploys update the same app. From here the app is an ordinary one: verify it with its slug from fuast.json.
A claimed app starts restricted. The person owns it, but fuast does not assume they trust code an anonymous agent deployed. Until they do:
ctx.email.send(),ctx.team.table(), andctx.workspace.members()fail with "This app is restricted and cannot …" (coderestricted).- Schedules and delayed jobs do not run.
- Everything else is normal: the app is private to its owner and has the normal limits.
Deploying does not end the restriction, not even with the person's login. Every fuast deploy to a restricted app still works, and ends with a line that says so:
! preview-k3v9q2m7xa is restricted: workspace features are off (email, team tables, the member list, schedules and jobs). Deploying does not turn them on. The owner or a workspace admin turns them on with `fuast trust preview-k3v9q2m7xa`, or with "Trust this app with my workspace" at https://fuast.com/dashboard/apps/preview-k3v9q2m7xa.--json has the same as restricted: true, restrictedNotice, and trust: { command, dashboard }. Only an explicit choice by the owner or a workspace admin ends it:
- The person ticks "Let this app use my workspace" under Options on the claim page.
- The person presses "Trust this app with my workspace" on the app's page in the dashboard.
fuast trust <slug>, run with the owner's login. If you are logged in as the person and the app needs email, team tables, the member list, schedules, or jobs, tell them what it grants and run it when they agree.fuast untrust <slug>turns it off again.
If the app needs none of those, leave it restricted. It works the same.
The app is private after the claim. Share when the person asks:
fuast share --visibility workspace # everyone at their company
fuast share --visibility private --emails ana@co.com # specific people
fuast share --visibility public # anyone with the link, guests allowedWhen you are already logged in
Check with fuast auth status. With a login, or with FUAST_TOKEN set, fuast deploy creates the app in that account straight away: private, with no preview and no claim link. The first deploy writes fuast.json. Commit it.
- If this machine deployed an unclaimed preview from the folder earlier, a logged-in
fuast deploystops with codepreview_unclaimedand asks you to choose.fuast deploy --claimclaims that preview for the logged-in account, keeps its data, and deploys to it.fuast deploy --newleaves the preview alone and deploys the folder as a new app. It never claims by itself. - In CI or another headless environment, set
FUAST_TOKENto a token the person created withfuast token create --name ci --app <slug>. fuast deploy --no-unclaimed(orFUAST_UNCLAIMED=0) never deploys without an account. It stops withNot logged ininstead.- If the platform has previews turned off,
deploysays so and tells you to runfuast login.
Read when needed
- Capsule API: every server and client API.
- Database: indexes, sort rules, pagination, schema changes.
- Auth: accounts, guests, sign-in policy, endpoint credentials.
- Teams: workspaces, sharing, members, team tables.
- Custom domains: serve an app at a domain the user owns (
fuast domains add). - Test and debug:
fuast run,inspect --query, logs,run-many. - Agent tools: expose handlers to browser agents.
- MCP server: build and deploy from an agent without a terminal (Claude, ChatGPT, Cursor, VS Code).
- Limits: every limit, its error, and what to change.
- Security: what fuast guarantees and what your code must check. Unclaimed previews says what a preview can and cannot do.
- Support: platforms, compatibility, persistence, getting help.
- Storage, schedules, email.
- Reference: every CLI command, inspection route, and env var.
- Migration to v2: for capsules written against the old fuast API.
- Docs index: every page, so you can fetch only the one you need.