# Lakebed Docs

Lakebed is an agent-native CLI and runtime for building small full-stack TypeScript apps called capsules.

If you are an agent building with Lakebed, treat the capsule directory as the whole app. Write the server contract, write the Preact client, run the Lakebed CLI, inspect the runtime state, and deploy without leaving code.

## Start Here

Create and run a capsule:

```sh
npx lakebed new my-app --template todo
cd my-app
npx lakebed dev
```

`npx lakebed create` is an alias for `npx lakebed new`. New capsules get a git repository and initial commit unless they are created inside an existing git repository or `--no-git` is passed.

A Lakebed v0 capsule has this shape:

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

- `server/index.ts`: schema, queries, mutations, and external endpoints. Import only from `lakebed/server` and pure relative files.
- `client/index.tsx`: the Preact UI entrypoint. Import hooks and auth UI from `lakebed/client`.
- `shared/`: pure TypeScript shared by server and client. Do not import Lakebed runtimes, DOM APIs, Node built-ins, secrets, or env values here.
- `.env.lakebed.server`: optional server-only environment variables exposed to server handlers through `ctx.env`.

## Server Contract

Every capsule exports a default `capsule()` definition from `server/index.ts`.

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

export default capsule({
  schema: {
    messages: table({
      body: string(),
      authorId: string()
    })
  },

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

  mutations: {
    sendMessage: mutation((ctx, body: string) =>
      ctx.db.messages.insert({
        body,
        authorId: ctx.auth.userId
      })
    )
  },

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

      const payload = await req.json<{ body: string }>();
      ctx.db.messages.insert({
        body: payload.body,
        authorId: "webhook"
      });

      return json({ ok: true });
    })
  }
});
```

Server handlers receive:

- `ctx.auth`: the current guest or Google identity.
- `ctx.db`: typed table access with `where`, `orderBy`, `limit`, `all`, `get`, `insert`, `update`, and `delete`.
- `ctx.env`: server-only values from `.env.lakebed.server`.
- `ctx.log`: structured logs captured by the runtime.

Make queries and mutations server-authoritative. Filter rows by `ctx.auth.userId` when data belongs to a user, and re-check ownership inside mutations before updates or deletes. Anonymous deploys run the bundled server JavaScript in a restricted source runtime by default, so ordinary JavaScript authorization checks stay authoritative.

Use `endpoints` for webhooks and external services. Endpoint handlers receive the same `ctx` as queries and mutations plus a request object with `headers`, `query`, `text()`, `json()`, and `bytes()`.

## Client Contract

The client exports `App` from `client/index.tsx`.

```tsx
import { SignInWithGoogle, signOut, useAuth, useMutation, useQuery } from "lakebed/client";

type Message = {
  id: string;
  body: string;
  authorId: string;
  createdAt: string;
  updatedAt: string;
};

export function App() {
  const auth = useAuth();
  const messages = useQuery<Message[]>("messages");
  const sendMessage = useMutation<[body: string], void>("sendMessage");

  return (
    <main className="min-h-screen bg-black p-6 text-white">
      {auth.isLoading ? (
        <span>Checking session</span>
      ) : auth.isGuest ? (
        <SignInWithGoogle />
      ) : (
        <button className="inline-flex items-center gap-2" type="button" onClick={() => signOut()}>
          {auth.picture ? <img alt="" className="h-6 w-6 rounded-full" referrerPolicy="no-referrer" src={auth.picture} /> : null}
          Sign out {auth.displayName}
        </button>
      )}

      <button type="button" onClick={() => void sendMessage("hello")}>
        Send
      </button>

      <pre>{JSON.stringify(messages, null, 2)}</pre>
    </main>
  );
}
```

- `useQuery<T>("name")` subscribes to a server query.
- `useMutation<TArgs, TResult>("name")` calls a server mutation and returns a promise.
- `useAuth()` reads the current client identity. Use `auth.isLoading` to avoid showing signed-out UI while Lakebed confirms a stored session.
- `<SignInWithGoogle />`, `signInWithGoogle()`, and `signOut()` provide the built-in Google auth path.
- Use `<Router>`, `<Routes>`, `<Route>`, `<Link>`, `useParams()`, `useLocation()`, and `useNavigate()` for client-side pages.
- Use Tailwind classes directly in JSX. V0 does not have CSS files, CSS modules, PostCSS, or a Tailwind build step.

Client routes are app-relative and work in dev and hosted deploys:

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

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

export function App() {
  return (
    <Router>
      <Link to="/items/123">Open item</Link>
      <Routes>
        <Route path="/" element={<main>Home</main>} />
        <Route path="/items/:id" element={<ItemPage />} />
        <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, the endpoint handles direct HTTP requests first.

## Auth And Env

Every app starts with guest auth. In dev, set the current global guest identity with:

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

For per-tab identities, add `?lakebed_guest=alice` or `?lakebed_guest=bob` to the app URL.

Server-only env belongs in `.env.lakebed.server`:

```txt
OPENAI_API_KEY=sk-...
STRIPE_WEBHOOK_SECRET=whsec_...
```

Read it only from server handlers:

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

`npx lakebed dev` loads server env locally. Hosted server env syncs only after a deploy is claimed, and deploy sync replaces the hosted env with the file contents. Env values are not exposed to client code or embedded in anonymous artifacts.

## Inspect The Runtime

While `npx lakebed dev` is running:

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

For hosted or locally deployed apps, pass a deploy id or URL:

```sh
npx lakebed inspect <deploy-id-or-url>
npx lakebed db dump <deploy-id-or-url>
npx lakebed logs <deploy-id-or-url>
```

Use these before guessing. Local `npx lakebed dev` inspection is open on localhost. Hosted deploys keep app manifests, state, table names, logs, and usage private by default; the CLI reads `.lakebed/deploy.json` and sends the saved claim token automatically when you run these commands from the capsule directory. Non-private hosted manifests expose only the app name, deploy id, client bundle hash, and runtime version.

## Deploy

From inside a capsule:

```sh
npx lakebed deploy
```

Anonymous deploys work first. Claim the deploy when the app needs hosted server env or outbound server-side `fetch`, then run `npx lakebed deploy` again. Anonymous deploys do not rewrite guarded JavaScript mutations into weaker IR.

Inspection for hosted deploys is private by default. For demos where public data and logs are intentional, deploy with:

```sh
npx lakebed deploy --public-inspect
```

For a local deploy runner:

```sh
npx lakebed anonymous-server --port 8787
npx lakebed deploy --api http://localhost:8787
```

After a hosted deploy is claimed, reserve a Lakebed-owned app subdomain from the capsule directory:

```sh
npx lakebed domains add my-app.lakebed.app
```

## Current Limits

- One server entry: `server/index.ts`.
- One client entry: `client/index.tsx`.
- App code can use relative imports, `lakebed/server`, `lakebed/client`, and Lakebed-provided Preact modules.
- Arbitrary npm imports inside capsule code are not supported yet.
- Node built-ins are not available inside capsule modules.
- Local state resets when `npx lakebed dev` restarts.
- File storage is not part of v0.
- Anonymous deploys disable outbound server-side `fetch`.
- Non-empty `.env.lakebed.server` files require a claimed deploy before hosted env can sync.

## Read Next

- [`capsule-api.md`](./capsule-api.md): detailed app-author API.
- [`reference.md`](./reference.md): capsule runtime, API, CLI, and deploy reference.
- [`examples`](../examples): working capsules that show the intended app shape.
- [`docs.json`](/docs.json), [`llms.txt`](/llms.txt), and [`llms-full.txt`](/llms-full.txt): machine-readable docs entrypoints.
