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/index.ts: schema, queries, mutations, and external endpoints. Import only fromlakebed/serverand pure relative files.client/index.tsx: the Preact UI entrypoint. Import hooks and auth UI fromlakebed/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 throughctx.env.
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:
ctx.auth: the current guest or Google identity.ctx.db: typed table access withwhere,orderBy,limit,all,get,insert,update, anddelete.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.
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. Useauth.isLoadingto avoid showing signed-out UI while Lakebed confirms a stored session.<SignInWithGoogle />,signInWithGoogle(), andsignOut()provide the built-in Google auth path.- Use
<Router>,<Routes>,<Route>,<Link>,useParams(),useLocation(), anduseNavigate()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:
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
- 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 devrestarts. - File storage is not part of v0.
- Anonymous deploys disable outbound server-side
fetch. - Non-empty
.env.lakebed.serverfiles require a claimed deploy before hosted env can sync.
Read Next
capsule-api.md: detailed app-author API.reference.md: capsule runtime, API, CLI, and deploy reference.examples: working capsules that show the intended app shape.docs.json,llms.txt, andllms-full.txt: machine-readable docs entrypoints.