# Capsule API

This page shows the API shape an agent should use when authoring a Lakebed app.

## File Layout

```txt
server/index.ts
client/index.tsx
shared/
```

Use `server/index.ts` for schema, queries, mutations, and external endpoints. Use `client/index.tsx` for the Preact UI. Put validation helpers, types, and constants in `shared/` when both sides need them.

## Define The Server

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

export default capsule({
  schema: {
    todos: table({
      text: string(),
      done: boolean().default(false),
      ownerId: string()
    })
  },

  queries: {
    todos: query((ctx) =>
      ctx.db.todos
        .where("ownerId", ctx.auth.userId)
        .orderBy("createdAt", "desc")
        .all()
    )
  },

  mutations: {
    addTodo: mutation((ctx, text: string) => {
      const cleanText = cleanTodoText(text);
      if (!cleanText) {
        return;
      }

      ctx.db.todos.insert({
        text: cleanText,
        done: false,
        ownerId: ctx.auth.userId
      });
    }),

    setTodoDone: mutation((ctx, id: string, done: boolean) => {
      const todo = ctx.db.todos.get(id);
      if (!todo || todo.ownerId !== ctx.auth.userId) {
        return;
      }

      ctx.db.todos.update(id, { done });
    })
  }
});
```

The important pattern is server authority:

- Queries decide which rows the client can read.
- Mutations validate input before writing.
- Mutations re-check ownership before changing existing rows.
- Client code never writes directly to tables.

Anonymous deploys preserve this model by running the bundled server JavaScript in a restricted source runtime. IR should be treated as a future optimization only when it can preserve the full handler semantics.

## Use Shared Code Carefully

Good shared code:

```ts
export type Todo = {
  id: string;
  text: string;
  done: boolean;
  ownerId: string;
  createdAt: string;
  updatedAt: string;
};

export function cleanTodoText(value: string): string {
  return value.trim().slice(0, 160);
}
```

Keep `shared/` pure. Do not import `lakebed/server`, `lakebed/client`, Preact, DOM APIs, Node built-ins, env values, or secrets from shared files.

## Build The Client

```tsx
import { SignInWithGoogle, signOut, useAuth, useMutation, useQuery } from "lakebed/client";
import { cleanTodoText, type Todo } from "../shared/todo";

export function App() {
  const auth = useAuth();
  const todos = useQuery<Todo[]>("todos");
  const addTodo = useMutation<[text: string], void>("addTodo");
  const setTodoDone = useMutation<[id: string, done: boolean], void>("setTodoDone");
  const authLabel = auth.displayName;
  const authStatus = auth.isLoading && auth.isGuest ? "checking session" : `signed in as ${authLabel}`;

  async function onSubmit(event: SubmitEvent) {
    event.preventDefault();
    const form = event.currentTarget as HTMLFormElement;
    const data = new FormData(form);
    const text = cleanTodoText(String(data.get("text") ?? ""));
    if (!text) {
      return;
    }

    await addTodo(text);
    form.reset();
  }

  return (
    <main className="min-h-screen bg-black px-6 py-10 text-white">
      <section className="mx-auto max-w-2xl">
        <div className="mb-3 flex items-center justify-between gap-3">
          <div className="flex min-w-0 items-center gap-2">
            {!auth.isLoading && auth.picture ? (
              <img alt="" className="h-7 w-7 rounded-full" referrerPolicy="no-referrer" src={auth.picture} />
            ) : null}
            <p className="min-w-0 truncate font-mono text-sm text-neutral-500">{authStatus}</p>
          </div>
          {!auth.isLoading && auth.isGuest ? (
            <SignInWithGoogle className="border border-neutral-700 px-3 py-1.5 text-sm text-neutral-200" />
          ) : !auth.isLoading ? (
            <button type="button" onClick={() => signOut()}>
              Sign out
            </button>
          ) : null}
        </div>

        <form className="mb-8 flex gap-3" onSubmit={(event) => void onSubmit(event)}>
          <input name="text" className="min-w-0 flex-1 border border-neutral-700 bg-black px-3 py-2" />
          <button type="submit" className="border border-white px-4 py-2">
            Add
          </button>
        </form>

        <ul>
          {todos.map((todo) => (
            <li key={todo.id}>
              <label>
                <input
                  checked={todo.done}
                  type="checkbox"
                  onChange={(event) => void setTodoDone(todo.id, event.currentTarget.checked)}
                />
                {todo.text}
              </label>
            </li>
          ))}
        </ul>
      </section>
    </main>
  );
}
```

Client rules:

- Export `App`.
- Call queries by the names defined in `server/index.ts`.
- Call mutations by the names defined in `server/index.ts`.
- Await mutations when the UI should wait for the server write.
- Use the built-in client router for multiple pages.
- Use Tailwind classes in JSX for styling.

Client routes use Preact components and app-relative paths:

```tsx
import { Link, Route, Router, Routes, useParams } from "lakebed/client";

function TodoPage() {
  const { id } = useParams<{ id: string }>();
  return <main>Todo {id}</main>;
}

export function App() {
  return (
    <Router>
      <Link to="/todos/123">Open todo</Link>
      <Routes>
        <Route path="/" element={<main>Home</main>} />
        <Route path="/todos/:id" element={<TodoPage />} />
        <Route path="*" element={<main>Not found</main>} />
      </Routes>
    </Router>
  );
}
```

Use server endpoints for HTTP APIs and webhooks. If a `GET` endpoint and a client route use the same path, direct HTTP requests hit the endpoint first.

## Auth

Use auth through Lakebed APIs only.

Server:

```ts
ctx.auth.userId;
ctx.auth.displayName;
ctx.auth.picture;
ctx.auth.email;
```

Client:

```tsx
const auth = useAuth();
```

Guest auth is available immediately. To test multiple local users, use separate URLs:

```txt
http://localhost:3000/?lakebed_guest=alice
http://localhost:3000/?lakebed_guest=bob
```

To add Google sign-in, render `<SignInWithGoogle />` or call `signInWithGoogle()` from a custom button. After sign-in, server handlers receive the verified identity on `ctx.auth`.

## Server Env

Add server-only values at the capsule root:

```txt
# .env.lakebed.server
OPENAI_API_KEY=sk-...
```

Read them only from server handlers:

```ts
queries: {
  hasOpenAiKey: query((ctx) => Boolean(ctx.env.OPENAI_API_KEY))
}
```

External endpoints can use the same env binding for webhook secrets:

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

endpoints: {
  webhook: endpoint({ method: "POST", path: "/webhooks/incoming" }, async (ctx, req) => {
    if (req.headers.get("x-webhook-secret") !== ctx.env.WEBHOOK_SECRET) {
      return text("unauthorized", { status: 401 });
    }
    return json({ ok: true });
  })
}
```

Do not put secrets in `client/` or `shared/`.

## Run And Inspect

```sh
npx lakebed dev
npx lakebed db list --port 3000
npx lakebed db dump --port 3000
npx lakebed logs --port 3000
```

The database is local and in-memory during `npx lakebed dev`. Restarting dev resets it.

Hosted inspection is private by default. Run hosted inspection commands from the capsule directory so Lakebed can send the saved claim token from `.lakebed/deploy.json`; non-private hosted manifests expose only non-sensitive deploy metadata.

## Deploy

```sh
npx lakebed deploy
```

If the app uses `.env.lakebed.server` or outbound server-side `fetch`, claim the deploy and run `npx lakebed deploy` again so Lakebed can publish the source-backed server path.

Use `npx lakebed deploy --public-inspect` only for demos where making hosted data and logs public is intentional.
