# Lakebed

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

This package is an early public prototype. It includes the `lakebed` CLI, a local dev runtime, an anonymous deploy artifact compiler, and an anonymous deploy runner.

## Install

Run Lakebed through `npx` for now:

```sh
npx lakebed new
npx lakebed dev my-app
```

Enter `my-app` when `npx lakebed new` asks for a capsule name.

`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.

## Capsule Shape

Lakebed v0 expects this directory layout:

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

Server code imports from `lakebed/server`. Client code imports from `lakebed/client`.

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

export default capsule({
  schema: {
    messages: table({
      body: string(),
      authorId: string()
    })
  },
  queries: {
    messages: query((ctx) => ctx.db.messages.orderBy("createdAt", "desc").all())
  },
  mutations: {
    sendMessage: mutation((ctx, body: string) =>
      ctx.db.messages.insert({
        body,
        authorId: ctx.auth.userId
      })
    )
  }
});
```

## Auth

Every app starts with local guest auth. To let users sign in with Google, render the built-in button. Lakebed asks Shoo for profile fields and exposes the user's name and avatar through `auth.displayName` and `auth.picture`.
Check `auth.isLoading` before showing signed-out UI, because Lakebed may still be confirming a stored session.

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

export function App() {
  const auth = useAuth();
  const authLabel = auth.displayName;

  if (auth.isLoading) {
    return <p>Checking session</p>;
  }

  return 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 {authLabel}
    </button>
  );
}
```

After sign-in, server handlers receive the verified Google identity through `ctx.auth`:

```ts
mutations: {
  save: mutation((ctx) => {
    ctx.log.info("signed in user", { userId: ctx.auth.userId, displayName: ctx.auth.displayName });
  });
}
```

## Server Env

Server handlers can read capsule-specific environment variables through `ctx.env`.

```txt
# .env.lakebed.server
OPENAI_API_KEY=sk-...
STRIPE_WEBHOOK_SECRET=whsec_...
```

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

`npx lakebed dev` loads `.env.lakebed.server` locally. For hosted apps, claim the deploy, then run `npx lakebed deploy` to replace the deploy's server env with the file contents. Env values are not included in anonymous artifacts or source manifests.

## External Endpoints

Define app-relative HTTP endpoints for webhooks and external services:

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

endpoints: {
  incoming: 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 });
  })
}
```

Endpoint handlers receive the same server context as queries and mutations. The request exposes headers, query params, and repeatable `text()`, `json()`, and `bytes()` body readers.

## Commands

```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 domains add <subdomain.lakebed.app> [--api <url>] [--json]
npx lakebed anonymous-server [--port 8787] [--public-root-url <url>] [--dashboard-root-url <url>] [--app-base-domain <domain>] [--role all|api-dashboard|runner]
npx lakebed inspect <deploy-id-or-url> [--api <url>] [--inspect-token <token>] [--json]
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>]
```

## Current Constraints

- App code can use relative imports, `lakebed/server`, `lakebed/client`, and Lakebed-provided Preact modules.
- Arbitrary npm imports inside capsules are not supported yet.
- Node built-ins are not available inside capsule modules.
- Tailwind classes in JSX are the only styling path in v0.
- Guest auth works by default. Google sign-in is built in through Shoo and exposes verified identity on `ctx.auth`.
- Anonymous deploys disable outbound `fetch`.
- Non-empty `.env.lakebed.server` files require a claimed deploy before they can sync to hosted server code.

## Hosted Deploys

Once a Lakebed deploy runner is available, deploy a capsule with:

```sh
cd my-app
npx lakebed deploy
```

For local deploy-runner testing:

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

In production, set `PUBLIC_ROOT_URL` to the deploy API origin and `LAKEBED_APP_BASE_DOMAIN` to the app domain without the wildcard prefix:

```sh
PUBLIC_ROOT_URL=https://api.lakebed.dev
LAKEBED_DASHBOARD_ROOT_URL=https://dashboard.lakebed.dev
LAKEBED_APP_BASE_DOMAIN=lakebed.app
```

The staged Railway split uses dedicated service entrypoints:

```sh
node packages/lakebed/src/services/all-in-one.js
node packages/lakebed/src/services/api-dashboard.js
node packages/lakebed/src/services/capsule-runner.js
```

With a verified `*.lakebed.app` custom domain on the runner, deploy responses use `https://<slug>.lakebed.app`.

Anonymous deploy creation is relaxed for local `localhost` runners. When `PUBLIC_ROOT_URL` points at a non-local origin, the runner defaults to 50 anonymous deploy creates per forwarded client per UTC day and 5,000 globally. Override those with:

```sh
LAKEBED_ANONYMOUS_DEPLOY_CREATE_PER_CLIENT_PER_DAY=50
LAKEBED_ANONYMOUS_DEPLOY_CREATE_GLOBAL_PER_DAY=5000
LAKEBED_ANONYMOUS_DEPLOY_CREATE_DISABLED=0
LAKEBED_ANONYMOUS_REQUESTS_PER_CLIENT_PER_DAY=100000
LAKEBED_ANONYMOUS_MUTATIONS_PER_CLIENT_PER_DAY=10000
```

Deploy creation, app requests, and app mutations also have per-client IP backstops. Forwarded client IP headers are trusted automatically on Railway only when the request comes through Railway's edge proxy. For other proxy setups, set `LAKEBED_TRUST_PROXY_HEADERS=1` only when your proxy strips untrusted inbound forwarding headers before adding its own.

Expired unclaimed deploys are cleaned up by the runner. By default, cleanup marks expired anonymous deploys terminated after a 1 hour grace window and deletes their state, logs, env, quota rows, slug mapping, and unreferenced artifacts after 7 days:

```sh
LAKEBED_ANONYMOUS_CLEANUP_GRACE=1h
LAKEBED_ANONYMOUS_CLEANUP_RETENTION=7d
LAKEBED_ANONYMOUS_CLEANUP_INTERVAL=1h
```

Deploy responses include claim metadata. Configure GitHub OAuth on the API and dashboard service, then run `npx lakebed claim` to open the claim page and attach the anonymous deploy to a developer account:

```sh
LAKEBED_GITHUB_CLIENT_ID=...
LAKEBED_GITHUB_CLIENT_SECRET=...
LAKEBED_SESSION_SECRET=...
LAKEBED_SERVER_ENV_SECRET=...
```

Claimed deploys are listed at `/deploys` on the dashboard origin. They keep the same resource limits as anonymous deploys and do not expire. Anonymous deploys cannot use outbound `fetch` or hosted server env; after a deploy is claimed, `npx lakebed deploy` can update it with a source-backed server artifact that supports async handlers, server-side fetch, and `.env.lakebed.server` sync. If the first deploy already needs server-side `fetch` or server env, `npx lakebed deploy` creates a claim-required preview, saves its claim metadata, and prints the `npx lakebed claim` command. Run that command, then run `npx lakebed deploy` again to publish the real source-backed app. Set `LAKEBED_SERVER_ENV_SECRET` on Postgres-backed runners to encrypt stored server env values.

Hosted inspection is private by default. `npx lakebed inspect`, `npx lakebed db list`, `npx lakebed db dump`, and `npx lakebed logs` send the saved claim token automatically when run from the capsule directory.

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

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

Reserved product names such as `api`, `admin`, `docs`, and `www` are rejected.

## Admin Dashboard

Set `LAKEBED_ADMIN_PASSWORD` on the API and dashboard service, then open `/admin` on the dashboard origin. The password is exchanged for an HttpOnly cookie so the dashboard stays unlocked until the cookie expires or the password changes. The resource table can terminate active deploys while preserving their resource history, and the users table can set per-user request and mutation limit overrides for claimed deploys.
