# Fold API

How to define a fold's server: `fold()`, the schema, queries, mutations, actions, endpoints, and the `ctx` every handler receives.

All server code imports from `@aozumi/fold/server`. The default export of `server/index.ts` must be a `fold({...})` call.

## A complete server

```ts
import { action, boolean, endpoint, fold, json, mutation, query, string, table, text, userId } from "@aozumi/fold/server";
import { cleanBody } from "../shared/notes";

// shared/notes.ts: export function cleanBody(value: string): string { return value.trim().slice(0, 500); }

export default fold({
  name: "notes",

  auth: { requireSignIn: false },

  schema: {
    notes: table({
      body: string(),
      pinned: boolean().default(false),
      color: string().optional(),
      ownerId: userId()
    }).index("by_owner", ["ownerId"])
  },

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

  mutations: {
    addNote: mutation(async (ctx, body: string) => {
      const { userId } = ctx.auth.requireIdentity();
      const clean = cleanBody(body);
      if (!clean) {
        throw new Error("Write something first.");
      }
      return ctx.db.notes.insert({ body: clean, ownerId: userId });
    }),
    removeNote: mutation(async (ctx, id: string) => {
      const { userId } = ctx.auth.requireIdentity();
      const note = await ctx.db.notes.get(id);
      if (!note || note.ownerId !== userId) {
        throw new Error("Note not found.");
      }
      await ctx.db.notes.delete(id);
    })
  },

  actions: {
    noteCount: action(async (ctx) => {
      const { userId } = ctx.auth.requireIdentity();
      const total = await ctx.db.notes.withIndex("by_owner", (q) => q.eq("ownerId", userId)).count();
      return { total };
    })
  },

  endpoints: {
    health: endpoint({ method: "GET", path: "/api/health" }, () => text("ok")),
    createNote: endpoint({ method: "POST", path: "/api/notes" }, async (ctx, req) => {
      const auth = ctx.auth.requireSignedIn();
      const payload = await req.json<{ body: string }>();
      const note = await ctx.db.notes.insert({ body: cleanBody(payload.body), ownerId: auth.userId });
      return json({ id: note.id }, { status: 201 });
    })
  }
});
```

## fold(definition)

`fold()` accepts only these keys. Any other key throws `Unknown fold() option`.

| Key | Type | Meaning |
|---|---|---|
| `name` | `string` | App title. The first deploy uses it to pick the subdomain. Defaults to `"AOZUMI Fold"`. |
| `favicon` | `string` | Path to an `.svg` or `.ico` file in the fold. Without it, `favicon.svg` or `favicon.ico` at the fold root is used. Max 64 KiB. |
| `auth` | `{ requireSignIn?: boolean; onGuestUpgrade?: (ctx, upgrade) => void \| Promise<void> }` | See [Auth](/fold/auth/). |
| `schema` | `Record<string, TableDefinition>` | Tables. See below. |
| `queries` | `Record<string, handler>` | Read-only, live handlers. |
| `mutations` | `Record<string, handler>` | Transactional writes. |
| `actions` | `Record<string, handler>` | One-shot, read-only calls with size and time caps. |
| `endpoints` | `Record<string, EndpointDefinition>` | HTTP routes for webhooks and other clients. |

## Schema

```ts
schema: {
  lists: table({ title: string(), ownerId: userId() }).index("by_owner", ["ownerId"]),
  items: table({
    listId: id("lists"),
    text: string(),
    done: boolean().default(false),
    dueAt: number().optional()
  })
    .index("by_list", ["listId"])
    .index("by_list_due", ["listId", "dueAt"])
}
```

Field builders:

| Builder | Stores |
|---|---|
| `string()` | A string. No NUL characters or unpaired surrogates. |
| `number()` | A finite number. `NaN` and `Infinity` are rejected. |
| `boolean()` | `true` or `false`. |
| `id("table")` | The `id` string of a row in another table. The referenced table must exist in the schema. |
| `userId()` | A Fold user ID. Use it for every owner field. When a guest signs in, these values move to the account. |

Modifiers, chainable on any field:

- `.optional()`: the field may be absent. Inserts may omit it or pass `null`.
- `.default(value)`: used when an insert omits the field, and returned for older rows that lack it.

Rules:

- Table, field, and index names start with a letter and use only letters, digits, and `_`.
- `id`, `createdAt`, and `updatedAt` are reserved. Fold sets them on every row (`createdAt` and `updatedAt` are ISO strings).
- `.index(name, fields)` declares an index. Fields may include `id`, `createdAt`, and `updatedAt`. The name `by_creation` is reserved: every table already has it.
- Every index sorts by its fields, then `createdAt`, then `id`.

Only `string`, `number`, `boolean`, `id`, and `userId` exist. Store a timestamp as `number()` (epoch milliseconds). Store a list as rows in another table. See [Database](/fold/database/) for queries.

## Queries

```ts
query(async (ctx, args: { listId: string }) => {
  return ctx.db.items.withIndex("by_list", (q) => q.eq("listId", args.listId)).collect();
})
```

- Read-only database. Any write throws `Query handlers have a read-only database capability.`
- Live: a client subscribed with `useQuery` gets a new result whenever a table the query read changes.
- Give a query at most one argument and make it an object. The client sends it as JSON.
- The result must be JSON-compatible.

## Mutations

```ts
mutation(async (ctx, listId: string, text: string) => {
  const { userId } = ctx.auth.requireIdentity();
  const list = await ctx.db.lists.get(listId);
  if (!list || list.ownerId !== userId) {
    throw new Error("List not found.");
  }
  return ctx.db.items.insert({ listId, text });
})
```

- Read and write. All writes in one call commit together or not at all. A thrown error rolls back every write.
- A mutation may take several positional arguments. Each must be JSON-compatible.
- Await or return every database call.
- Each call counts toward the daily mutation limit when hosted.

## Actions

```ts
action(async (ctx, search: string) => {
  const matches = await ctx.db.items.withIndex("by_creation").order("desc").take(200);
  return matches.filter((item) => String(item.text).includes(search)).length;
})
```

- Read-only database, like a query, but not live: the client calls it once with `useAction`.
- Arguments up to 16 KiB, result up to 48 KiB, both JSON. Runs for at most 5 seconds.
- Each call counts toward the daily request and mutation limits when hosted.

## Endpoints

```ts
endpoint({ method: "POST", path: "/api/items" }, async (ctx, req) => {
  const body = await req.json<{ listId: string; text: string }>();
  const item = await ctx.db.items.insert({ listId: body.listId, text: body.text });
  return json(item, { status: 201 });
})
```

`endpoint(route, handler)`:

- `route.method`: an uppercase HTTP method, such as `"GET"` or `"POST"`.
- `route.path`: an absolute path such as `/api/items`. No query string, fragment, or backslash. Paths are matched exactly. Reserved: `/`, `/index.html`, `/client.js`, `/auth/...`, `/__fold/...`, `/__span/...`. When hosted, `/storage` and `/storage/...` are taken by file storage, so do not use them.
- `route.readOnly`: defaults to `true` for `GET` and `HEAD`, `false` for other methods. A read-only endpoint gets a read-only `ctx.db`. A writable one runs in a transaction like a mutation and counts toward the daily mutation limit when hosted.

The request (`EndpointRequest`):

| Member | Type |
|---|---|
| `method`, `path`, `url` | `string` |
| `headers` | `get(name)`, `has(name)`, `entries()` |
| `query` | `URLSearchParams` |
| `text()` | `Promise<string>` |
| `json<T>()` | `Promise<T>` |
| `bytes()` | `Promise<Uint8Array>` |

Body readers can be called more than once. The request body may be up to 2 MiB.

Return a response with a helper:

| Helper | Response |
|---|---|
| `json(value, { status?, headers? })` | JSON body |
| `text(value, { status?, headers? })` | Plain text body |
| `empty({ status?, headers? })` | No body |
| `redirect(url, { status?, headers? })` | Redirect to `url` |

Endpoint callers authenticate with the same-origin guest cookie, or with a Google identity token in the `X-Fold-Token` header. `Authorization` is left for your app. See [Auth](/fold/auth/).

## ctx

Every handler gets `ctx` as its first argument:

| Field | Type | Notes |
|---|---|---|
| `ctx.auth` | `AuthContext` | The caller. `userId`, `isGuest`, `isSignedIn`, `displayName`, and the guards `requireIdentity()` and `requireSignedIn()`. See [Auth](/fold/auth/). |
| `ctx.db` | `ReadDatabase` or `WriteDatabase` | One table API per schema table. Writable only in mutations and writable endpoints. |
| `ctx.env` | `Record<string, string \| undefined>` | Values from `.env.fold.server` in local dev. Empty in anonymous deploys. |
| `ctx.log` | `{ info, warn, error }` | Each takes `(message: string, data?: unknown)`. Readable with `logs --port` in local dev. Hosted logs are coming soon. |

## Handler capabilities

| | Query | Mutation | Action | Endpoint |
|---|---|---|---|---|
| Called from | `useQuery`, `usePaginatedQuery` | `useMutation` | `useAction` | Any HTTP client |
| Database | Read | Read and write, one transaction | Read | Read (`GET`, `HEAD`), or read and write |
| Live updates | Yes | No | No | No |
| Time limit, hosted | 10 s | 10 s | 5 s | 10 s |
| Counts as, hosted | Request | Request and mutation | Request and mutation | Request, plus mutation if writable |
| Outbound `fetch` | Local dev only | Local dev only | Local dev only | Local dev only |
| `ctx.env` values | Local dev only | Local dev only | Local dev only | Local dev only |

Outbound `fetch` and server env are coming soon for hosted folds, claimed ones included, so today they work only in `npx @aozumi/fold dev`. An anonymous deploy rejects any server or shared file that mentions `fetch`.

Fold has no scheduler. To ingest data on a schedule, have an outside service call a writable endpoint. See [Dashboard counts](/fold/database/#dashboard-counts).

## Errors

Throw an `Error` to fail a call. The message reaches the caller: `useMutation` and `useAction` promises reject with it, `useQuery` throws it during render (catch it with `ErrorBoundary`), and endpoints answer `500` with `{ "error": "<message>" }`. Do not put secrets in error messages.
