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:

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:

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

Server Contract

Every capsule exports a default capsule() definition from server/index.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:

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.

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

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

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:

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:

OPENAI_API_KEY=sk-...
STRIPE_WEBHOOK_SECRET=whsec_...

Read it only from server handlers:

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:

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:

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:

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:

npx lakebed deploy --public-inspect

For a local deploy runner:

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:

npx lakebed domains add my-app.lakebed.app

Current Limits