# Server SDK

`cloudbed/server` is how a capsule declares its data and behavior. The whole server is one default export:

```ts
import { boolean, capsule, mutation, query, string, table } from "cloudbed/server";

const schema = {
  todos: table({
    text: string(),
    done: boolean().default(false),
    ownerId: string(),
  }),
};

export default capsule({
  schema,
  queries: {
    todos: query((ctx) =>
      ctx.db.todos.where("ownerId", ctx.auth.userId).orderBy("createdAt", "desc").all()),
  },
  mutations: {
    addTodo: mutation((ctx, text: string) => {
      if (!text) throw new Error("Text required");
      return ctx.db.todos.insert({ text, ownerId: ctx.auth.userId });
    }),
  },
});
```

The server is authoritative. Queries decide what each user may read; mutations validate their arguments and re-check ownership before writing. The platform independently re-validates every write against the declared schema on commit, but per-row authorization (which user may touch which row) is your capsule's job — as the `ownerId` checks above show.

## Schema

- `table(fields)` — declare a table.
- `string()`, `number()`, `boolean()` — field types, each chainable with `.default(value)`.

Every row automatically carries three server-managed fields you never write yourself:

| Field | Type | |
|---|---|---|
| `id` | `string` | unique row id |
| `createdAt` | `string` | ISO timestamp, set on insert |
| `updatedAt` | `string` | ISO timestamp, maintained on update |

## Handlers

- `query(fn)` — a read. Runs on subscription and automatically re-runs (and pushes to subscribed clients) after every mutation.
- `mutation(fn)` — a write. Receives `ctx` plus whatever arguments the client passed. Throw an `Error` to reject; the client's mutation promise rejects with `{ code, message }`.
- `endpoint({ method, path }, fn)` — a plain HTTP handler for webhooks and non-browser clients, registered under `endpoints:`. Return a response descriptor built with:
  - `json(value, { status?, headers? })` — JSON, default 200
  - `text(value, options?)` — plain text
  - `empty(options?)` — default 204
  - `redirect(url, options?)` — default 302

Each handler invocation is bounded at **10 seconds** of wall-clock time.

## The context

Every handler receives `ctx`:

| Property | What it is |
|---|---|
| `ctx.db` | Typed database handle, one property per schema table (below) |
| `ctx.auth` | The calling user: `userId`, `displayName`, `provider` (`"guest"` \| `"google"`), `isGuest`, `isAuthenticated`, and `email` / `emailVerified` / `picture` when signed in |
| `ctx.env` | Server-only env vars from `.env.cloudbed.server` ([claimed deploys](/deploys) only) |
| `ctx.log` | `info` / `warn` / `error` loggers; entries are kept by the runtime (see [log retention](/deploys)) and exposed on the deploy's inspect API |

Before sign-in, users are guests: `ctx.auth.userId` is `guest:local` by default, and opening the app with `?guest=<name>` acts as the named guest `guest:<name>` (persisted per tab — useful for testing multi-user behavior). Ownership checks work the same whether or not the user has signed in.

## Reading and writing

`ctx.db.<table>` exposes a small chainable query builder:

```ts
ctx.db.todos.where("ownerId", ctx.auth.userId).orderBy("createdAt", "desc").limit(50).all();
ctx.db.todos.get(id);                    // one row by id, or null
ctx.db.todos.insert({ text, ownerId }); // returns the full row (with id, timestamps)
ctx.db.todos.update(id, { done: true }); // partial patch, returns the updated row
ctx.db.todos.delete(id);
```

`where` filters by equality, `orderBy` defaults to `"asc"`, and a query may return at most 1,000 rows — see [limits](/deploys).
