# Lakebed Reference

Use this as the quick contract when building a Lakebed capsule.

## Capsule

A capsule is one complete Lakebed app: source, server API, client UI, state, auth, logs, and deploy URL.

V0 expects this directory shape:

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

- `server/index.ts` exports the capsule definition.
- `client/index.tsx` exports the Preact `App` component.
- `shared/` contains pure TypeScript used by both sides.
- `.env.lakebed.server` is optional server-only configuration.

There is no `lakebed.config.ts` in v0.

## Module Boundaries

- Server code imports from `lakebed/server`.
- Client code imports from `lakebed/client`.
- Shared code imports only pure relative TypeScript.
- App code can import relative files and Lakebed-provided Preact modules.
- App code cannot import arbitrary npm packages yet.
- Capsule modules cannot use Node built-ins.
- Shared code must not read env, secrets, DOM APIs, Node APIs, or Lakebed runtime APIs.

## Server API

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

Export one default `capsule()` call:

```ts
export default capsule({
  schema: {
    todos: table({
      text: string(),
      done: boolean().default(false),
      ownerId: string()
    }).index("by_owner", ["ownerId"])
  },
  queries: {
    todos: query(async (ctx) =>
      ctx.db.todos
        .withIndex("by_owner", (q) => q.eq("ownerId", ctx.auth.userId))
        .order("desc")
        .collect()
    )
  },
  mutations: {
    addTodo: mutation(async (ctx, text: string) =>
      ctx.db.todos.insert({
        text,
        done: false,
        ownerId: ctx.auth.userId
      })
    )
  }
});
```

Server handlers receive:

- `ctx.auth`: current identity.
- `ctx.db`: table access for the capsule database.
- `ctx.env`: server-only env values.
- `ctx.log`: structured logs captured by Lakebed.

## Data API

Tables are declared with `table({ ...fields })`. V0 field helpers are:

- `string()`
- `boolean()`
- `id("table")`
- `.default(value)` on a field
- `.index(name, fields)` on a table

Every stored row includes:

- `id`
- `createdAt`
- `updatedAt`

Table methods are async. Use `withIndex(name, range)`, then `order("asc" | "desc")` and one terminal:
`collect()`, `take(count)`, `first()`, or `paginate(options)`. Direct `get`, `insert`, `update`, and `delete`
are also awaited. Legacy `where`, `orderBy`, `limit`, and `all` calls are rejected for new artifacts.

See the [database guide](./database.md) for index and consistency details. Use the
[Database API v1 migration guide](./database-migration.md) for before-and-after syntax and a copy-paste agent prompt.

Treat queries and mutations as the source of truth. Filter user-owned data by `ctx.auth.userId`, and re-check ownership inside every mutation that changes or deletes an existing row. Anonymous deploys run bundled server JavaScript in a restricted source runtime by default, so ordinary control flow such as `get(id)` plus `if (!row || row.ownerId !== ctx.auth.userId) return` is preserved instead of approximated by IR.

## External Endpoints

Use `endpoint({ method, path }, handler)` for webhooks and other services that call your app over HTTP.

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

endpoints: {
  stripeWebhook: 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 body = await req.text();
    ctx.log.info("stripe webhook received", { bytes: body.length });
    return json({ ok: true });
  })
}
```

Endpoint handlers receive `ctx.auth`, `ctx.db`, `ctx.env`, and `ctx.log`. The request exposes `method`, `path`, `url`, `headers.get(name)`, `query`, `text()`, `json()`, and `bytes()`. Successful endpoint calls can write to the database and publish subscribed queries. Use `.env.lakebed.server` secrets for webhook checks.

## Client API

```tsx
import {
  ErrorBoundary,
  Link,
  Route,
  Router,
  Routes,
  SignInWithGoogle,
  navigate,
  signInWithGoogle,
  signOut,
  useAuth,
  useLocation,
  useMutation,
  useNavigate,
  useParams,
  usePaginatedQuery,
  useQuery
} from "lakebed/client";
```

- `useQuery<T>("name")`: subscribe to a server query.
- `usePaginatedQuery<T>("name", args, { initialNumItems })`: subscribe to cursor pages, accumulate them, and expose `loadMore()`/`reset()`.
- `useMutation<TArgs, TResult>("name")`: call a server mutation.
- `<ErrorBoundary>`: catch query failures and show a retryable fallback. Lakebed wraps the generated app root with this boundary automatically.
- `useAuth()`: read the current client identity. Use `auth.isLoading` to avoid showing signed-out UI while Lakebed confirms a stored session.
- `<SignInWithGoogle />`: render the built-in Google sign-in button.
- `signInWithGoogle()`: start Google sign-in from custom UI.
- `signOut()`: return to guest auth.
- `<Router>`, `<Routes>`, and `<Route>`: render client-side pages.
- `<Link to="/path">`: navigate without a page reload. Paths are app-relative locally and on hosted app subdomains.
- `useParams<T>()`, `useLocation()`, `useNavigate()`, and `navigate()`: read and change the current client route.

Mutation calls return promises:

```tsx
const addTodo = useMutation<[text: string], void>("addTodo");
await addTodo("Ship the app");
```

Paginated query handlers accept a trailing argument whose `pagination` property is passed to `.paginate(...)`:

```ts
messages: query(async (ctx, args: { roomId: string; pagination: { cursor: string | null; numItems: number } }) =>
  ctx.db.messages
    .withIndex("by_room", (q) => q.eq("roomId", args.roomId))
    .order("desc")
    .paginate(args.pagination)
)
```

```tsx
const messages = usePaginatedQuery<Message>("messages", { roomId }, { initialNumItems: 25 });

return (
  <>
    {messages.page.map((message) => <p key={message.id}>{message.body}</p>)}
    {!messages.isDone ? <button onClick={messages.loadMore}>Load more</button> : null}
  </>
);
```

Router example:

```tsx
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>
  );
}
```

Declared server endpoints take precedence over client routes for direct HTTP requests.

## Auth

Every capsule starts with guest auth.

Set the local guest identity globally:

```sh
npx lakebed auth as alice
```

Set identity per browser tab:

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

Auth shape on client and server:

```ts
type Auth = {
  userId: string;
  subject?: string; // same immutable key as userId for authenticated identities
  identityAliases?: string[]; // migration-only audience-scoped aliases
  displayName: string;
  provider: "guest" | "google";
  isGuest: boolean;
  isAuthenticated: boolean;
  isLoading?: boolean; // client-only
  email?: string;
  emailVerified?: boolean;
  picture?: string;
};
```

## Server Env

Put server-only values in `.env.lakebed.server`:

```txt
OPENAI_API_KEY=sk-...
```

Read them from server handlers:

```ts
query((ctx) => Boolean(ctx.env.OPENAI_API_KEY));
```

`npx lakebed dev` loads this file locally. Hosted env syncs only after the deploy is claimed. Sync is replace-based: keys removed from `.env.lakebed.server` are removed from the hosted deploy.

Env values are not exposed to client code and are not embedded in anonymous artifacts.

## Styling

Use Tailwind classes directly in JSX.

V0 does not support CSS files, CSS modules, PostCSS, Tailwind config, or a CSS build pipeline.

## Runtime Inspection

While `npx lakebed dev` is running:

```sh
npx lakebed db list --port 3000
npx lakebed db dump --port 3000
npx lakebed db export --port 3000 --out backup.json
npx lakebed logs --port 3000
```

For a deployed app:

```sh
npx lakebed inspect <deploy-id-or-url>
npx lakebed db list <deploy-id-or-url>
npx lakebed db dump <deploy-id-or-url>
npx lakebed db export <deploy-id-or-url> --out backup.json
npx lakebed logs <deploy-id-or-url>
```

Local state is in-memory and resets when `npx lakebed dev` restarts.

`db dump` is a bounded inspection view. `db export` scans the built-in `by_creation` index in bounded pages and writes a versioned JSON backup through a same-directory temporary file before atomically replacing the destination. Tables are ordered by name; rows are ordered by immutable `(createdAt, id)` ascending; row object keys are serialized canonically. Each page is a separate store snapshot, so the complete backup is not a point-in-time transaction: concurrent inserts after the cursor may appear, deletes not yet visited may disappear, and updates reflect the page that reads them. A schema change, authorization failure, network/write error, or handled interruption fails the export, preserves an existing destination, and removes the temporary file.

Local inspection is open on localhost. Hosted inspection is private by default for manifests, table names, row dumps, logs, and usage. Run hosted inspection commands from the capsule directory so the CLI can find `lakebed.json` or `.lakebed/deploy.json` in the working directory tree and send developer auth from the binding or saved anonymous claim token. Direct HTTP callers can use `Authorization: Bearer <token>`. Non-private hosted manifests expose only app name, deploy id, client bundle hash, and runtime version.

## CLI

```sh
npx lakebed new [name] [--template todo] [--no-git]
npx lakebed create [name] [--template todo] [--no-git]
npx lakebed dev [capsule-dir] [--port 3000]
npx lakebed build [capsule-dir] --target anonymous [--out .lakebed/artifacts/app.json] [--json]
npx lakebed deploy [capsule-dir] [--api <url>] [--public-inspect] [--json]
npx lakebed claim [capsule-dir] [--api <url>] [--json]
npx lakebed auth login [--api <url>] [--json]
npx lakebed auth status [--api <url>] [--json]
npx lakebed auth logout [--api <url>]
npx lakebed token create --name <name> [--personal] [--api <url>] [--json]
npx lakebed token list [--api <url>] [--json]
npx lakebed token revoke <token-id> [--api <url>]
npx lakebed domains add <subdomain.lakebed.app> [--api <url>] [--json]
npx lakebed inspect <deploy-id-or-url> [--api <url>] [--inspect-token <token>] [--json]
npx lakebed run-many [capsule-dir] [--count 20] [--base-port 4000]
npx lakebed auth as <name>
npx lakebed auth reset
npx lakebed db list [deploy-id-or-url] [--port 3000] [--inspect-token <token>]
npx lakebed db dump [deploy-id-or-url] [--port 3000] [--inspect-token <token>]
npx lakebed db export [deploy-id-or-url] [--port 3000] [--inspect-token <token>] [--out <file>]
npx lakebed logs [deploy-id-or-url] [--port 3000] [--inspect-token <token>]
```

## Deploy Behavior

`npx lakebed deploy` can publish an anonymous deploy first.

Claim the deploy before relying on hosted server env or outbound server-side `fetch`, then run `npx lakebed deploy` again. Anonymous deploys intentionally disable those capabilities while preserving server handler control flow in the source runtime.

Run `npx lakebed auth login` before the first deploy to create an owned app. The CLI writes `lakebed.json` at the capsule root with only `deployId`; commit it for fresh-checkout and CI deploys. `npx lakebed token create --name github-actions` returns a deploy-scoped CI credential once. Use `npx lakebed token create --personal --name local-automation` when automation needs an owner-wide credential instead. Supply the returned value as `LAKEBED_TOKEN`. For a custom API origin, `LAKEBED_TOKEN_API` must match the canonical `--api` origin exactly. The canonical origin is the scheme and host, plus a non-standard port when present, without a path or query.

Hosted deploy inspection is `private` by default. Use `npx lakebed deploy --public-inspect` only for demos where making data and logs public is intentional.

Claimed deploys can reserve Lakebed-owned app subdomains with `npx lakebed domains add my-app.lakebed.app`. Reserved product names such as `api`, `admin`, `docs`, and `www` cannot be registered.

Hosted anonymous deploys enforce the advertised state byte limit during mutation commit, cap logs by entry count and bytes, and rate-limit deploy creation, app requests, and app mutations. Unclaimed deploys expire: shortly after expiry they stop serving, and about a week later they are permanently deleted along with their data, logs, and server env. Claim a deploy to keep it.
