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 init

Otherwise create the capsule in a new directory:

bunx @revnu/fuast new my-app
cd my-app

Pick 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(), and json(). Chain .optional() or .default(value). json() cannot be indexed.
  • Type errors block deploys. fuast check and fuast deploy run the TypeScript checker on the whole capsule.
  • Nothing needs an account: not building, not running locally, not the first deploy. Without a login, fuast deploy creates 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, and ctx.workspace fail 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/dev and survives restarts. fuast dev --reset wipes it.

App structure and APIs

  • server/index.ts default-exports capsule({...}). Server files import only fuast/server and relative files.
  • client/index.tsx exports function App(). Client files import only fuast/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 call createClient<typeof app>() once at module scope. Its useQuery, useMutation, useAction, and usePaginatedQuery infer names, arguments, and results.
  • Database calls are async. Await or return every one. Declare indexes with .index(name, fields) and read with withIndex(name, q => ...), then .order("asc" | "desc") and one terminal: collect(), take(n), first(), paginate(opts), or count(). Use the built-in by_creation index for unfiltered lists. Do not use the legacy where, orderBy, limit, or all methods: they do not exist in v2, and the type check in fuast check rejects them.
  • insert returns the new row id. Rows carry id, createdAt, and updatedAt.
  • 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) or ctx.auth.requireSignedIn() (account only). Never accept a user id, email, or name as an argument. Filter user-owned rows by the caller's userId and re-check ownership before every update or delete.
  • Declare user references with userId(), never string(). When a guest signs in, every userId() 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 } in capsule() to block guests everywhere. On the client, gate data components on canAccessApp(). Hiding a button is not an access check.
  • If auth.error is set, show it with a retryAuth() button and <SignInButton />. Keep data components unmounted until access works.
  • Server secrets live in .env.fuast.server and are read with ctx.env.NAME inside handlers. Never put secrets in client/ or shared/.
  • 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.storage stores files on the server, and schedules run cron jobs. See teams, email, storage, and schedules.
  • Use the router from fuast/client for 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 fetch calls carry the app's cookie automatically. Other callers send the fuast identity token from getIdentity().token, or a personal token from fuast token create (headless agents), in the X-Fuast-Token header. App-scoped deploy tokens never identify a caller. The Authorization header 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 against ctx.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 to query, mutation, or action. Build args from string(), number(), boolean(), and id("table"). With args, 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.svg or favicon.ico at the root. Use client.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 undefined until its first result, usually within a few milliseconds. While it is undefined, 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().isLoading is 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 dev

It 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 query

Fix 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 deploy

deploy 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-k3v9q2m7xa

Give 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 with auth: { requireSignIn: true } cannot be tried until it is claimed. Use ctx.auth.requireIdentity() for data a guest should own.
  • ctx.email.send(), ctx.team.table(), and ctx.workspace.members() fail with an error that starts "Claim this app to use" (code unclaimed). 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 fetch have 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 as

Verify 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-k3v9q2m7xa

fuast 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(), and ctx.workspace.members() fail with "This app is restricted and cannot …" (code restricted).
  • 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 allowed

When 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 deploy stops with code preview_unclaimed and asks you to choose. fuast deploy --claim claims that preview for the logged-in account, keeps its data, and deploys to it. fuast deploy --new leaves the preview alone and deploys the folder as a new app. It never claims by itself.
  • In CI or another headless environment, set FUAST_TOKEN to a token the person created with fuast token create --name ci --app <slug>.
  • fuast deploy --no-unclaimed (or FUAST_UNCLAIMED=0) never deploys without an account. It stops with Not logged in instead.
  • If the platform has previews turned off, deploy says so and tells you to run fuast 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.