# Capsule API

This page is the API an agent uses to write a fuast app. The database has its own page: [database](https://fuast.com/docs/database.md).

## File layout

```txt
server/index.ts      default export capsule({...})
client/index.tsx     export function App()
shared/              pure TypeScript for both sides
```

Split big apps into more files under `server/`, `client/`, and `shared/` with relative imports. `server/` may import `fuast/server`. `client/` may import `fuast/client`, `fuast/ui`, `preact`, `preact/hooks`, and `preact/jsx-runtime`. `shared/` may import only relative files. Nothing may import npm packages or Node built-ins. `fuast check` rejects violations.

## capsule()

`server/index.ts` default-exports one `capsule()` call. It accepts these keys and nothing else:

| Key | Type | Purpose |
| --- | --- | --- |
| `name` | `string` | Browser tab title. Defaults to `"fuast app"`. |
| `favicon` | `string` | Relative path to an `.svg` or `.ico` file, at most 64 KiB. Without it, fuast uses `favicon.svg` or `favicon.ico` at the capsule root, if present. |
| `auth` | `{ requireSignIn?, onGuestUpgrade? }` | App-wide sign-in policy and guest merge hook. See [auth](https://fuast.com/docs/auth.md). |
| `schema` | `Record<string, TableDefinition>` | Tables declared with `table()`. |
| `queries` | handler map | Read-only, reactive handlers declared with `query()`. |
| `mutations` | handler map | Read-write transactional handlers declared with `mutation()`. |
| `actions` | handler map | Read-only, non-reactive handlers declared with `action()`. |
| `endpoints` | endpoint map | HTTP routes declared with `endpoint()`. |
| `schedules` | schedule map | Cron jobs declared with `schedule()`. See [schedules](https://fuast.com/docs/schedules.md). |

Handler names must be unique within their map. A query and a mutation may share a name, but a name used as a [tool](https://fuast.com/docs/agent-tools.md) must be unique across all kinds.

## Define the server

```ts
import { action, boolean, capsule, mutation, query, string, table, userId } from "fuast/server";
import { cleanTodoText } from "../shared/todo";

export default capsule({
  name: "Todos",
  auth: { requireSignIn: false },

  schema: {
    todos: table({
      text: string(),
      done: boolean().default(false),
      ownerId: userId()
    }).index("by_owner", ["ownerId"])
  },

  queries: {
    todos: query(async (ctx) => {
      const { userId } = ctx.auth.requireIdentity();
      return ctx.db.todos
        .withIndex("by_owner", (q) => q.eq("ownerId", userId))
        .order("desc")
        .collect();
    })
  },

  mutations: {
    addTodo: mutation(async (ctx, text: string) => {
      const { userId } = ctx.auth.requireIdentity();
      const clean = cleanTodoText(text);
      if (!clean) throw new Error("Write a todo first.");
      return ctx.db.todos.insert({ text: clean, ownerId: userId });
    }),

    setTodoDone: mutation(async (ctx, id: string, done: boolean) => {
      const { userId } = ctx.auth.requireIdentity();
      const todo = await ctx.db.todos.get(id);
      if (!todo || todo.ownerId !== userId) throw new Error("Todo not found.");
      await ctx.db.todos.update(id, { done });
    })
  },

  actions: {
    summarize: action(async (ctx) => {
      const { userId } = ctx.auth.requireIdentity();
      const open = await ctx.db.todos
        .withIndex("by_owner", (q) => q.eq("ownerId", userId))
        .collect();
      return { open: open.filter((t) => !t.done).length };
    })
  }
});
```

The server is the authority:

- Queries decide which rows a caller may read.
- Mutations validate every argument before writing.
- Mutations re-check ownership before changing or deleting an existing row.
- Identity comes from `ctx.auth`, never from arguments.

## Handlers

`query(fn, opts?)`, `mutation(fn, opts?)`, and `action(fn, opts?)` wrap a handler so TypeScript infers its `ctx` and arguments.

- Without `opts.tool.args`, the handler is `(ctx, ...args) => result`. Clients call it with the same positional arguments: `addTodo("milk")`.
- With `opts.tool.args`, the handler is `(ctx, args) => result`, where `args` is one object typed from the declared fields. Clients call it with that object: `addTodo({ text: "milk" })`.

`opts = { tool?: { description: string, args?: Record<string, Field> } }`. The `tool` option also exposes the handler to browser agents. See [agent tools](https://fuast.com/docs/agent-tools.md).

Arguments and results must be JSON values. Results go through JSON on the way to the client, so a `Date` arrives as a string and `undefined` object fields disappear. Return plain objects, arrays, strings, numbers, booleans, and `null`.

## Handler capabilities

| Capability | Query | Mutation | Action | Endpoint | Schedule |
| --- | --- | --- | --- | --- | --- |
| Read `ctx.db` | Yes | Yes | Yes | Yes | Yes |
| Write `ctx.db` | No | Yes | No | Writable only | Yes |
| Transaction | Snapshot | One write transaction | Snapshot per read | Like query or mutation | One write transaction |
| Reactive (clients re-run on change) | Yes | No | No | No | No |
| Global `fetch()` | Yes | Yes | Yes | Yes | Yes |
| `ctx.env`, `ctx.log`, `ctx.auth` | Yes | Yes | Yes | Yes | Yes |
| `ctx.storage.url()` | Yes | Yes | Yes | Yes | Yes |
| `ctx.storage.get/stat/delete` | No | Yes | Yes | Yes | Yes |
| `ctx.storage.put` | No | Yes | Yes | Writable only | Yes |
| `ctx.email.send()` | No | Yes | Yes | Yes | Yes |
| `ctx.team.table(name).all()` | Yes | Yes | Yes | Yes | Yes |
| `ctx.team` writes | No | Yes | Yes | Writable only | Yes |
| `ctx.workspace.members()` | Yes | Yes | Yes | Yes | Yes |
| `ctx.scheduler` ([jobs](https://fuast.com/docs/jobs.md)) | No | In the transaction | Saved when it ends | In the transaction (read-only: saved when it ends) | In the transaction |
| Daily quota | Request | Request + mutation | Request + mutation | Request (+ mutation if writable) | Not counted |
| Time limit | 5 s CPU, 10 s wall clock | 5 s CPU, 10 s wall clock | 5 s ([action limit](https://fuast.com/docs/limits.md#action-limits)) | 5 s CPU, 10 s wall clock | 5 s CPU, 10 s wall clock |

Endpoints are writable unless `readOnly` is true, which is the default for `GET` and `HEAD`. The `auth.onGuestUpgrade` hook gets a writable `ctx.db` and nothing that leaves the database: no `fetch`, email, storage writes, or team writes.

Writes on one app are serialized. A mutation, writable endpoint, or schedule holds the app's write lock for its whole run, including time spent in `fetch`. Keep outbound calls short and ingest small batches.

Rollback covers only the database. If a mutation sends an email, uploads a file, writes a team row, or calls an API and then throws, those side effects stay. Do them last.

Queries can fetch, but an external change does not re-run a subscription. Store external data in rows (from a mutation, endpoint, or schedule) when the UI should update reactively.

## Handler context

```ts
ctx.auth        // caller identity and guards. See auth.md.
ctx.db          // tables. See database.md.
ctx.env         // Record<string, string>
ctx.log         // structured logs
ctx.storage     // server-side files. See storage.md.
ctx.email       // send email. See email.md.
ctx.team        // workspace-shared tables. See teams.md.
ctx.workspace   // workspace members. See teams.md.
ctx.scheduler   // run a mutation or action later (not in queries). See jobs.md.
ctx.job         // { id, name, attempt, maxAttempts, scheduledAt, scheduledBy } when running as a job
```

`ctx.auth` fields:

| Field | Type | Notes |
| --- | --- | --- |
| `userId` | `string \| null` | `null` without a session (external HTTP callers, schedules, jobs). |
| `isGuest` | `boolean` | Guest session on a public app. |
| `isSignedIn` | `boolean` | fuast account. `isAuthenticated` is an alias. |
| `provider` | `"guest" \| "fuast" \| null` | |
| `displayName` | `string` | Always present. `"Guest"` for guests, `"system"` for schedules and jobs. |
| `email`, `emailVerified`, `picture` | optional | Profile data for accounts. Not a credential. |
| `workspace` | `{ id, name }` optional | The caller's workspace, for accounts. |
| `requireIdentity()` | method | Returns the identity with a non-null `userId`. Throws code `unauthenticated` with `Sign in or start a guest session first.` |
| `requireSignedIn()` | method | Same, but guests also throw, with code `sign_in_required`. |

## Endpoints

Use endpoints for webhooks, health checks, and anything that calls your app over plain HTTP.

```ts
import { endpoint, json, text } from "fuast/server";

endpoints: {
  stripe: endpoint({ method: "POST", path: "/webhooks/stripe" }, async (ctx, req) => {
    if (req.headers.get("x-webhook-secret") !== ctx.env.STRIPE_WEBHOOK_SECRET) {
      return text("unauthorized", { status: 401 });
    }
    const event = await req.json<{ type: string }>();
    await ctx.db.events.insert({ type: event.type });
    return json({ ok: true });
  }),

  item: endpoint({ method: "GET", path: "/api/items/:id" }, async (ctx, req) => {
    const item = await ctx.db.items.get(req.params.id);
    return item ? json(item) : text("not found", { status: 404 });
  })
}
```

`endpoint(route, handler)`:

- `route.method`: any HTTP method, upper-cased for you.
- `route.path`: starts with `/`. Segments like `:id` become `req.params.id`.
- `route.readOnly`: defaults to `true` for `GET` and `HEAD`, `false` otherwise. A non-boolean value throws at build time. Set `readOnly: false` explicitly for a `GET` that writes.

The endpoint is served at `<app url><path>`, for example `https://my-app.fuast.com/webhooks/stripe`. From your client, call it with `fetch(appUrl("/webhooks/stripe"))` (see [calling your endpoints](#calling-your-endpoints)). `<app url>/e/<path>` is an alias for older callers. A request whose method and path match an endpoint gets the endpoint, even when a client route has the same path. Keep API paths (`/api/...`) apart from page paths so reloads and shared links work. The paths `/api/call`, `/api/me`, `/storage/...`, and `/__fuast/...` are reserved.

The request object:

| Member | Type |
| --- | --- |
| `method` | `string` |
| `path` | `string`, without the app base |
| `url` | `string`, the full URL |
| `headers` | `{ get(name), has(name), entries() }` |
| `query` | `URLSearchParams` |
| `params` | `Record<string, string>` from `:param` segments |
| `text()`, `json<T>()`, `bytes()` | body readers, each a `Promise` |

Endpoints run for any caller. The app's sharing mode controls the UI and the query, mutation, and action calls, not endpoints, so webhooks work on private apps. `ctx.auth` is set only when the caller sends a fuast identity the app accepts (same-origin session cookie or `X-Fuast-Token`). Check webhook secrets yourself. With `auth.requireSignIn: true`, endpoints reject callers without a signed-in identity. See [endpoint credentials](https://fuast.com/docs/auth.md#endpoint-credentials).

### Endpoint responses

| Helper | Result |
| --- | --- |
| `json(value, opts?)` | Status 200, `Content-Type: application/json; charset=utf-8`. |
| `text(value, opts?)` | Status 200, `Content-Type: text/plain; charset=utf-8`. `httpText` is an alias. |
| `empty(opts?)` | Status 204, no body. |
| `redirect(url, opts?)` | Status 302, `Location: <url>`. |

Every helper takes `{ status?, headers? }`. Return one of these, not a global `Response`. A handler that throws returns `500` with `{ "error": "<message>" }` and logs the error.

## Logging

```ts
ctx.log("plain message", { any: "data" });    // same as info
ctx.log.info("order created", { id });
ctx.log.warn("payment retried", { attempt });
ctx.log.error("payment failed", { reason });
```

Entries are `{ at, level, message, data, source, handler }`. Handler errors are logged automatically with `source: "system"` and the stack in `data.stack`. In `fuast dev` the stack is mapped back to your source files (`server/index.ts:42:11`). Read them with `fuast logs`. The buffer keeps the newest 1000 entries or 256 KiB per app. See [limits](https://fuast.com/docs/limits.md).

## Errors

Throw an `Error` to reject a call. The message reaches the client: `useQuery` throws it to the nearest `ErrorBoundary`, and mutation and action promises reject with it. Write messages a person can act on: `"Todo not found."`, not `"invalid"`.

Errors carry a `code` when fuast raised them: `unauthenticated`, `sign_in_required`, `forbidden`, `fuast_quota_exceeded`, `DESTRUCTIVE_SCHEMA_CHANGE`. Branch on codes, not message text.

## Shared code

```ts
// shared/todo.ts
export const MAX_TODO = 160;
export function cleanTodoText(value: string): string {
  return value.trim().slice(0, MAX_TODO);
}
```

Use the same helper on the client for instant feedback and on the server for the real check.

## Client

```tsx
import {
  appUrl, basePath, canAccessApp, createClient, ErrorBoundary, getIdentity, Link, navigate, retryAuth,
  Route, Router, Routes, SignInButton, signIn, signOut, useAuth, useLocation,
  useNavigate, useParams
} from "fuast/client";
import type app from "../server/index";

const client = createClient<typeof app>();
```

`createClient<typeof app>()` returns `{ useQuery, useMutation, useAction, usePaginatedQuery, storage }` with handler names, arguments, and results inferred from the server. Import the server with `import type` so no server code reaches the browser.

| API | Behavior |
| --- | --- |
| `client.useQuery(name, ...args)` | Subscribes over the app's WebSocket. Returns `undefined` until the first result, then re-renders when a write changes what the query read. Throws query errors to the nearest `ErrorBoundary`. |
| `client.useMutation(name)` | Returns `(...args) => Promise<result>`. Chain `.withOptimisticUpdate(update)` to predict the result. See [optimistic updates](#optimistic-updates). |
| `client.useAction(name)` | Returns `(...args) => Promise<result>`. |
| `client.usePaginatedQuery(name, args, { initialNumItems })` | Returns `{ page, isDone, continueCursor, loadMore(), reset(), status }`. `status` is `"loading"` (no first page yet), `"canLoadMore"`, `"loadingMore"` (`loadMore()` in flight), or `"exhausted"` (`isDone`). See [pagination](https://fuast.com/docs/database.md#pagination). |
| `client.storage` | `upload(file, { public? })` and `delete(key)`. See [storage](https://fuast.com/docs/storage.md). |

Standalone `useQuery`, `useMutation`, `useAction`, and `usePaginatedQuery` are also exported for callers that supply their own types.

Query results are deep-frozen in `fuast dev` to catch accidental mutation. Copy before sorting or editing.

A connection runs at most 4 mutations and actions at once. Later calls queue in order, up to 1000. Calls that run at the same time can finish in any order, so await a call before one that depends on it. A queued call that was not sent yet fails with an error ending in "the server did not apply it" if the connection closes or the signed-in user changes. Read the current state and call again if it still applies. Batch many row writes into one mutation instead of one call per row.

After a mutation succeeds, the queries it affected on that connection update before the mutation's promise resolves, so the next render already shows the write.

Query updates are incremental. When a write changes a subscribed result, the server sends only the changed part, and the client rebuilds the full result before your component renders. Your code always sees a complete, fresh value. Nothing to configure.

If the session ends or the person loses access to the app (signed out elsewhere, removed from sharing), the server closes the connection. The client clears cached query data, does not reconnect on its own, and sets `useAuth().error`. Show it with `retryAuth()` and `<SignInButton />`.

### Optimistic updates

A mutation normally shows its effect once the server confirms it, usually within tens of milliseconds. For interactions that must feel instant (checking a box, reordering, sending a chat message), predict the result on the client:

```tsx
const setTodoDone = client
  .useMutation("setTodoDone")
  .withOptimisticUpdate((store, id: string, done: boolean) => {
    store.setQuery("todos", [], (todos) => todos?.map((t) => (t.id === id ? { ...t, done } : t)));
  });

// Call it exactly like the plain mutation.
await setTodoDone(todo.id, true);
```

`withOptimisticUpdate(update)` returns the same callable with a prediction attached. `update(store, ...args)` receives the mutation's arguments and edits the client's cached query results:

| `store` method | Does |
| --- | --- |
| `getQuery(name, args?)` | The current result of a subscribed query, including earlier predictions, or `undefined`. `args` is the query's arguments as an array, `[]` for none. |
| `setQuery(name, args, valueOrUpdater)` | Replaces that result for display. Pass a function to derive it from the current value. Queries this page has not subscribed to are ignored. |
| `getAllQueries(name)` | Every subscribed instance of a query as `{ args, value }`, for example each page of a paginated list. |

How predictions behave:

- The prediction shows as soon as you call the mutation, even if it is still queued behind other writes. Nothing reaches the server except the mutation itself.
- The server stays authoritative. When the mutation commits, the real results replace the prediction. If it fails, the prediction is removed and the promise rejects as usual. Keep your normal error handling.
- Several pending predictions stack in call order. While a sent mutation is pending, newer server results for the queries its prediction touched are held back and shown once it settles, so the prediction never flickers or applies twice.
- A `useMutation` result takes one `withOptimisticUpdate`. Calling it twice throws.
- Write `update` as a pure function of the store and the arguments. Do not mutate the value you read: return a new array or object. Results are frozen in `fuast dev`, so in-place edits throw there.
- An `update` that throws is skipped with a console warning. The mutation still runs.
- New rows have no server `id` yet. Use a temporary id such as `"optimistic:" + Date.now()` and do not let the user act on that row until the real one arrives.

Only add a prediction where the wait is visible. Most forms do not need one.

### Loading, empty, and loaded

```tsx
function Todos() {
  const todos = client.useQuery("todos");
  if (todos === undefined) return null;
  if (todos.length === 0) return <EmptyState title="No todos yet" hint="Add one above." />;
  return (
    <ul className="transition-opacity duration-200 starting:opacity-0">
      {todos.map((t) => <li key={t.id}>{t.text}</li>)}
    </ul>
  );
}
```

Render nothing while the result is `undefined`, fade the loaded branch in, and give the empty state its own branch. No spinners or skeletons for query loads.

### Forms

```tsx
const [draft, setDraft] = useState("");
const [error, setError] = useState("");

async function onSubmit(event: Event) {
  event.preventDefault();
  const submitted = draft;
  try {
    await addTodo(submitted);
    setError("");
    setDraft((current) => (current === submitted ? "" : current));
  } catch (err) {
    setError(err instanceof Error ? err.message : String(err));
  }
}
```

On failure, keep the input and show the error beside the form. On success, clear only if the input still holds what was submitted.

### Auth on the client

`useAuth()` returns `{ isLoading, error, requireSignIn, userId, isGuest, isSignedIn, displayName, email?, picture?, workspace? }`. The page ships with its session, so `isLoading` is normally `false` on the first render. Render nothing while it is `true`.

- `canAccessApp()`: `true` when the session is ready, there is a guest or account, and the app's sign-in policy is met. Gate data components on it.
- `<SignInButton />` and `signIn()`: send the user to fuast sign-in and back. `SignInWithGoogle` is an alias of `SignInButton`.
- `signOut()`: ends the account session. Public apps fall back to a fresh guest.
- `retryAuth()`: retries a failed session setup or guest upgrade shown in `auth.error`.
- `getIdentity()`: the current auth state plus `token` (and `expired`) for custom `fetch` calls to your own endpoints. The client refreshes the token before it expires, so call `getIdentity()` right before each request instead of caching the token. `decodeIdentityClaims(token)` reads its unverified claims (`userId`, `appId`, `exp`). See [endpoint credentials](https://fuast.com/docs/auth.md#endpoint-credentials).

### Calling your endpoints

Build endpoint URLs with `appUrl(path)`, never with a literal path:

```tsx
import { appUrl, getIdentity } from "fuast/client";

const token = getIdentity().token;
const res = await fetch(appUrl("/api/status"), { headers: token ? { "X-Fuast-Token": token } : {} });
```

`appUrl("/api/status")` is `/api/status` on the app's own origin (`https://<slug>.fuast.com/`, `fuast dev`) and `/a/<slug>/api/status` when the app is served under a path. A page-relative `fetch("api/status")` breaks on nested routes (`/items/42` would request `/items/api/status`), and a root path `fetch("/api/status")` breaks under `/a/<slug>/`. `appUrl` keeps query strings and fragments, treats `"api/status"` like `"/api/status"`, and returns absolute URLs (`https://…`) unchanged. `basePath()` returns the base itself: `""` or `/a/<slug>`, never ending in `/`.

### Routes

```tsx
function ItemPage() {
  const { id } = useParams<{ id: string }>();
  return <main>Item {id}</main>;
}

export function App() {
  return (
    <Router>
      <nav><Link to="/">Home</Link> <Link to="/items/42">Item 42</Link></nav>
      <Routes>
        <Route path="/" element={<main>Home</main>} />
        <Route path="/items/:id" element={<ItemPage />} />
        <Route path="*" element={<main>Not found</main>} />
      </Routes>
    </Router>
  );
}
```

Paths are app-relative, so they work in dev, on `https://<slug>.fuast.com/`, and under `/a/<slug>/`. `useNavigate()` returns a `navigate(to)` function. `navigate(to)` also works outside components. `useLocation()` returns `{ pathname, search, hash }`.

### Error boundaries

fuast wraps `App` in an `ErrorBoundary`. Add your own for a narrower fallback:

```tsx
<ErrorBoundary fallback={(error, retry) => (
  <div><p>{error.message}</p><button type="button" onClick={retry}>Retry</button></div>
)}>
  <TodoList />
</ErrorBoundary>
```

### Deploy reloads

When a new deploy goes live, connected tabs receive a `refresh` message and reload at the next safe moment, so nobody runs an old client against a new server for long.

## fuast/ui

An optional component kit. Nothing requires it and the starter templates do not use it: an app's design is its own, written in Tailwind classes. It stays available for apps that already import it. It renders in light and dark mode and follows `prefers-color-scheme`.

| Component | Props worth knowing |
| --- | --- |
| `Page` | Centered page container. |
| `AppHeader` | The app's top bar. `title`, `subtitle?`, `icon?`, `actions?`, `nav?` (links shown underneath), `user?: { name, picture?, email? } \| null`, `onSignOut?`, `signOutLabel?` (default "Sign out"). Pass `user` and `onSignOut` for the standard signed-in block. |
| `PageHeader` | `title`, `subtitle?`, `actions?` |
| `Card` | `onClick?` |
| `ListRow` | One row of a list: `title`, `description?`, `leading?` (avatar or icon), `trailing?` (badges, buttons), `selected?`, `onClick?` (then the row is focusable and activates on Enter/Space). |
| `Stat` | A labelled number: `label`, `value`, `sub?` for the unit or context ("of 100 a day"). |
| `Button` | `variant?: "primary" \| "secondary" \| "ghost" \| "danger"`, `size?: "sm" \| "md"`, `type`, `disabled`, `onClick` |
| `Input`, `Textarea`, `Select`, `Checkbox` | Pass-through of the native element's props. |
| `FileInput` | A drop zone around a native file input: `label?`, `hint?`, `accept?`, `multiple?`, `disabled?`, `name?`, `id?`, `buttonLabel?`, `onFiles?(files: File[])` (also called after a drop), `onChange?`. Upload the files with `client.storage.upload`. |
| `Label`, `Field` | `Field`: `label`, `hint?`, children. |
| `Badge` | `tone?: "neutral" \| "green" \| "blue" \| "amber" \| "red"` |
| `Avatar` | `name`, `size?: "sm" \| "md"` |
| `EmptyState` | `title`, `hint?`, children for an action. |
| `ErrorText` | Renders nothing when empty. |
| `Tr` | A table row. `onClick?` makes it focusable and activates it on Enter/Space; `label?` names it for screen readers (give clickable rows one); `selected?`. |
| `Table`, `Td`, `Tabs`, `Dialog`, `Toast`, `Spinner` | Layout and feedback. Use `Spinner` for a button that is waiting, never for query loads. |

Everything else: complete Tailwind class names in JSX. Classes compile automatically during `dev`, `check`, and `deploy`. There are no CSS files, CSS modules, or Tailwind config.

## Types

You rarely need these. They help when a helper takes `ctx` or a request.

```ts
import { mutation, type ServerContext } from "fuast/server";

async function markDone(ctx: ServerContext, id: string) {
  await ctx.db.todos.update(id, { done: true });
}
```

`fuast/server` exports:

| Type | Describes |
| --- | --- |
| `QueryServerContext` | `ctx` in queries and read-only endpoints. |
| `ServerContext` | `ctx` in mutations, writable endpoints, schedules, and `onGuestUpgrade`. Its `db` can write. |
| `ActionServerContext` | `ctx` in actions. |
| `AuthContext` | `ctx.auth`. |
| `CapsuleAuth`, `GuestUpgrade` | The `auth` key and the `{ guestUserId, userId }` hook argument. |
| `EndpointRequest`, `EndpointResponse`, `EndpointResponseOptions`, `EndpointDefinition` | Endpoint pieces. |
| `ScheduleDefinition` | What `schedule()` returns. |
| `Scheduler<TApp>`, `JobContext`, `JobId`, `JobOptions` | `ctx.scheduler` (`ctx.scheduler.typed<typeof app>()` checks names and arguments), `ctx.job`. See [jobs](https://fuast.com/docs/jobs.md). |
| `Field`, `TableDefinition`, `Schema`, `Id` | Schema pieces. |
| `PaginationOptions`, `PaginationResult` | `{ cursor, numItems }` and `{ page, isDone, continueCursor }`. |
| `QueryName`, `QueryArgs`, `QueryResult`, and the same for mutations and actions | Handler signatures pulled from a capsule. |

`fuast/client` exports `FuastClient`, `PaginatedQueryResult`, `ErrorBoundaryProps`, `StorageClient`, `UploadedObject`, `AuthValue`, `IdentitySnapshot`, `IdentityClaims`, `OptimisticLocalStore<typeof app>` (the typed `store` of an optimistic update, for helpers), `UntypedOptimisticLocalStore`, and `OptimisticMutationFunction`.

`fuast-env.d.ts` in the capsule root declares all three modules, so editors and `fuast check` work with no `node_modules`. `fuast new` writes it. After upgrading the CLI, run `fuast types` to refresh it. See [migration](https://fuast.com/docs/migration-v2.md#regenerate-fuast-env-d-ts).
