# fuast agent instructions

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](https://fuast.com/docs/mcp.md) 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](https://fuast.com/docs/mcp.md#the-fuast-skill). It is optional: nothing on this page needs it.

## Start here

You need [Bun](https://bun.sh) 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:

```sh
bunx @revnu/fuast init
```

Otherwise create the capsule in a new directory:

```sh
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](https://fuast.com/docs/support.md) for supported platforms, compatibility, and persistence.
- Read [limits](https://fuast.com/docs/limits.md) 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](#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](https://fuast.com/docs/limits.md#unclaimed-previews).
- 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](https://fuast.com/docs/teams.md), [email](https://fuast.com/docs/email.md), [storage](https://fuast.com/docs/storage.md), and [schedules](https://fuast.com/docs/schedules.md).
- 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](https://fuast.com/docs/auth.md#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](https://fuast.com/docs/agent-tools.md).
- 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](https://fuast.com/docs/capsule-api.md#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](https://fuast.com/docs/database.md#dashboard-counts).

## Run and verify

Run every command from the capsule directory. Start dev in a terminal that stays open:

```sh
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:

```sh
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](https://fuast.com/docs/testing.md).

## Deploy, hand over, and verify

Deploy as soon as `fuast check` passes. Do not ask the person to log in first:

```sh
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:

```txt
✓ 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](#after-the-person-claims-the-app).
- Stored rows, storage, daily requests, and outbound `fetch` have smaller limits. See [limits](https://fuast.com/docs/limits.md#unclaimed-previews).

`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:

```sh
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:

```sh
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:

```txt
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:

```txt
! 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:

```sh
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](https://fuast.com/docs/capsule-api.md): every server and client API.
- [Database](https://fuast.com/docs/database.md): indexes, sort rules, pagination, schema changes.
- [Auth](https://fuast.com/docs/auth.md): accounts, guests, sign-in policy, endpoint credentials.
- [Teams](https://fuast.com/docs/teams.md): workspaces, sharing, members, team tables.
- [Custom domains](https://fuast.com/docs/domains.md): serve an app at a domain the user owns (`fuast domains add`).
- [Test and debug](https://fuast.com/docs/testing.md): `fuast run`, `inspect --query`, logs, `run-many`.
- [Agent tools](https://fuast.com/docs/agent-tools.md): expose handlers to browser agents.
- [MCP server](https://fuast.com/docs/mcp.md): build and deploy from an agent without a terminal (Claude, ChatGPT, Cursor, VS Code).
- [Limits](https://fuast.com/docs/limits.md): every limit, its error, and what to change.
- [Security](https://fuast.com/docs/security.md): what fuast guarantees and what your code must check. [Unclaimed previews](https://fuast.com/docs/security.md#unclaimed-previews) says what a preview can and cannot do.
- [Support](https://fuast.com/docs/support.md): platforms, compatibility, persistence, getting help.
- [Storage](https://fuast.com/docs/storage.md), [schedules](https://fuast.com/docs/schedules.md), [email](https://fuast.com/docs/email.md).
- [Reference](https://fuast.com/docs/reference.md): every CLI command, inspection route, and env var.
- [Migration to v2](https://fuast.com/docs/migration-v2.md): for capsules written against the old fuast API.
- [Docs index](https://fuast.com/llms.txt): every page, so you can fetch only the one you need.
