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:
server/index.ts
client/index.tsx
shared/
.env.lakebed.server
server/index.tsexports the capsule definition.client/index.tsxexports the PreactAppcomponent.shared/contains pure TypeScript used by both sides..env.lakebed.serveris 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
import { boolean, capsule, mutation, query, string, table } from "lakebed/server";
Export one default capsule() call:
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) =>
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().default(value)on a field
Every stored row includes:
idcreatedAtupdatedAt
Table methods:
where(field, value): filter rows.orderBy(field, "asc" | "desc"): sort rows.limit(count): cap results.all(): return rows.get(id): return one row ornull.insert(value): create a row.update(id, patch): patch a row.delete(id): delete a row.
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.
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
import {
Link,
Route,
Router,
Routes,
SignInWithGoogle,
navigate,
signInWithGoogle,
signOut,
useAuth,
useLocation,
useMutation,
useNavigate,
useParams,
useQuery
} from "lakebed/client";
useQuery<T>("name"): subscribe to a server query.useMutation<TArgs, TResult>("name"): call a server mutation.useAuth(): read the current client identity. Useauth.isLoadingto 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, including hosted/d/<slug>deploys.useParams<T>(),useLocation(),useNavigate(), andnavigate(): read and change the current client route.
Mutation calls return promises:
const addTodo = useMutation<[text: string], void>("addTodo");
await addTodo("Ship the app");
Router example:
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:
npx lakebed auth as alice
Set identity per browser tab:
http://localhost:3000/?lakebed_guest=alice
Auth shape on client and server:
type Auth = {
userId: string;
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:
OPENAI_API_KEY=sk-...
Read them from server handlers:
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:
npx lakebed db list --port 3000
npx lakebed db dump --port 3000
npx lakebed logs --port 3000
For a deployed app:
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 logs <deploy-id-or-url>
Local state is in-memory and resets when npx lakebed dev restarts.
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 read .lakebed/deploy.json and attach the saved claim token. Direct HTTP callers can use Authorization: Bearer <claim-token>. Non-private hosted manifests expose only app name, deploy id, client bundle hash, and runtime version.
CLI
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 domains add <subdomain.lakebed.app> [--api <url>] [--json]
npx lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--app-base-domain <domain>]
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 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.
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 per client on non-local PUBLIC_ROOT_URL origins. Forwarded client IP headers are trusted automatically on Railway only when the request comes through Railway's edge proxy; other proxy setups can opt in with LAKEBED_TRUST_PROXY_HEADERS=1. Expired unclaimed deploys are marked terminated after LAKEBED_ANONYMOUS_CLEANUP_GRACE (default 1h) and deleted after LAKEBED_ANONYMOUS_CLEANUP_RETENTION (default 7d), including state rows, logs, server env, quota rows, slug mappings, and unreferenced artifacts.