# Deploy a guestbook

Build one Next.js app with Postgres and Auth through the portal, the REST API or MCP. Each track reaches the same result: a public guestbook with sign-in and persistent data.

## What you will deploy

One app, three ways to operate Lathe.

The example is [lathe-dogfood-guestbook ](https://github.com/EvgeniLeonti/lathe-dogfood-guestbook). Visitors create an account, sign in and leave messages in Postgres. A counter under /data makes it easy to check that app storage survives a redeploy.

Its container listens on PORT , answers /healthz without calling a dependency, and keeps local persistent state under /data .

Choose your path: [portal ](https://docs.lathe.live/#portal), [REST API ](https://docs.lathe.live/#rest-api)or [MCP ](https://docs.lathe.live/#mcp). Plan for about 8 minutes while a new machine is provisioned, then several minutes for the first build.

## Before you start

- Create a Lathe account and connect GitHub on the portal's [Integrations page ](https://app.lathe.live/account/integrations#github).
- Install the Lathe GitHub integration for the repository you want to import.
- Use Postgres, Apps and Auth on the new machine. Auth needs both Postgres and Apps.
- Treat API keys, database URLs and the Auth service key as secrets. The Auth anon key is the browser-safe key used by this app.

## Portal track

Use the browser from creation through deployment.

- **Create the machine. **Open Instances, choose New instance, select a plan and region, and enable Postgres, Apps and Auth. Create it and wait until its status is running . A fresh machine can take about 8 minutes.
- **Copy the Auth values. **On the Auth tab, open Keys. Copy the Auth URL, which ends in /auth/v1 , and the anon key.
- **Create the table. **On the Postgres tab, enable read-write access for the SQL console and run the table statement below. Return the console to read-only or off when you finish.
- **Import the repository. **On the Apps tab, choose Import a repository and select EvgeniLeonti/lathe-dogfood-guestbook on main . Read the repository, then set the app name to guestbook , port to 3000 , health path to /healthz and memory to at least the Apps minimum shown by Lathe. Keep the app public, allow variables during the build, and deploy automatically.
- **Add the build variables. **Set NEXT_PUBLIC_AUTH_URL to the Auth URL and NEXT_PUBLIC_AUTH_ANON_KEY to the anon key. Import the app and wait for the first build to turn green and deploy.
- **Open and test it. **Use the public URL shown on the app page. Create an account, sign in and post a message.

To deploy a later commit, choose Deploy the latest build from the app page. Engine maintenance actions and app redeploys keep Postgres and /data intact.

## Create the guestbook table

Use this once in any of the three tracks. Connection strings, TLS and pooling: [Connect to PostgreSQL ](https://docs.lathe.live/postgres).

```
CREATE TABLE IF NOT EXISTS guestbook_entries (
  id BIGSERIAL PRIMARY KEY,
  user_id UUID NOT NULL,
  email TEXT NOT NULL,
  message TEXT NOT NULL CHECK (char_length(message) BETWEEN 1 AND 500),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

## REST API track

Run the whole flow with curl. Every call is in the [REST API reference ](https://docs.lathe.live/api).

Create an API key on the portal's Access page. It is shown once, so store it somewhere safe.

```
export LATHE_KEY=lathe_...
export API=https://app.lathe.live/api/v1
```

### 1. Create and wait for the machine

Get the current plan and region values from GET /plans and GET /locations , then create the machine:

```
curl -s -X POST "$API/instances" \
  -H "Authorization: Bearer $LATHE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-guestbook",
    "plan": "mini",
    "region": "<region from GET /locations>",
    "engines": {"postgres": true, "apps": true, "auth": true}
  }'
```

Keep the returned instance id as INSTANCE . If the response includes a checkout URL, open it to finish creation. Poll the instance until its status changes from creating to running . Plan for about 8 minutes.

```
export INSTANCE=itli...
curl -s -H "Authorization: Bearer $LATHE_KEY" \
  "$API/instances/$INSTANCE" | jq .status
```

### 2. Read Auth credentials and create the table

```
curl -s -H "Authorization: Bearer $LATHE_KEY" \
  "$API/instances/$INSTANCE/connection" > conn.json
jq -r '.engines.auth.url, .engines.auth.anon_key' conn.json

curl -s -X PUT "$API/instances/$INSTANCE/engines/postgres/console" \
  -H "Authorization: Bearer $LATHE_KEY" \
  -H "Content-Type: application/json" -d '{"mode":"rw"}'
```

Run the [table statement ](https://docs.lathe.live/#table)with POST /instances/$INSTANCE/engines/postgres/query , then set the console mode to off .

### 3. Import, then link the app

```
curl -s -X POST "$API/instances/$INSTANCE/apps/import" \
  -H "Authorization: Bearer $LATHE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "repo": "EvgeniLeonti/lathe-dogfood-guestbook",
    "branch": "main",
    "name": "guestbook",
    "port": 3000,
    "health_path": "/healthz",
    "public": true,
    "env": {
      "NEXT_PUBLIC_AUTH_URL": "<Auth URL>",
      "NEXT_PUBLIC_AUTH_ANON_KEY": "<anon key>"
    }
  }'

curl -s -H "Authorization: Bearer $LATHE_KEY" \
  "$API/instances/$INSTANCE/apps/guestbook/builds" | jq '.builds[0].status'
```

Wait for the first build to finish. Set links after import because the import request does not apply them yet:

```
curl -s -X PATCH "$API/instances/$INSTANCE/apps/guestbook" \
  -H "Authorization: Bearer $LATHE_KEY" \
  -H "Content-Type: application/json" \
  -d '{"links":["auth"]}'
```

After the app has a public URL, set that URL on the Auth engine so redirects return to the app:

```
curl -s -X PATCH "$API/instances/$INSTANCE/engines/auth/settings" \
  -H "Authorization: Bearer $LATHE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "site_url": "<public app URL>",
    "redirect_urls": ["<public app URL>/**"]
  }'
```

Each mutation returns a job. Poll GET /jobs/{id} until it is done before starting another mutation on the same machine.

## MCP track

Give the same work to an MCP client. Every tool is in the [MCP reference ](https://docs.lathe.live/mcp).

Connect a Streamable HTTP MCP client to https://mcp.lathe.live . Use OAuth, or create an API key on the portal's Access page and send it as a bearer token.

### 1. Inspect the live contract

Call list_plans for current plans, regions and engine memory minimums, app_guide for the container and deployment contract, and list_repos to confirm the repository is visible.

### 2. Create the machine

```
create_instance(
  tier="mini",
  name="my-guestbook",
  location="<location from list_plans>",
  engines=["postgres", "apps", "auth"]
)
```

If the result includes a checkout URL, open it to finish creation. Poll get_job and get_instance until the job is done and the instance is running. Plan for about 8 minutes rather than relying on a shorter estimate in an early response.

### 3. Import the repository

Call get_connection_urls for Auth. Use its URL as NEXT_PUBLIC_AUTH_URL and its anon key as NEXT_PUBLIC_AUTH_ANON_KEY . Do not print returned credentials into logs.

```
inspect_repo(
  repo="EvgeniLeonti/lathe-dogfood-guestbook",
  branch="main",
  instance_id="<instance id>"
)

import_repo(
  instance_id="<instance id>",
  repo="EvgeniLeonti/lathe-dogfood-guestbook",
  name="guestbook",
  branch="main",
  source="lathe",
  target="runner",
  port=3000,
  health_path="/healthz",
  public=true,
  memory_mb=<Apps minimum from list_plans>,
  env={
    "NEXT_PUBLIC_AUTH_URL": "<Auth URL>",
    "NEXT_PUBLIC_AUTH_ANON_KEY": "<anon key>"
  }
)
```

Poll list_builds or app_repo until the build is successful and deployed. Then set links after import:

```
update_app(
  instance_id="<instance id>",
  name="guestbook",
  links=["auth"]
)
```

Use set_sql_console in read-write mode and postgres_query to run the [table statement ](https://docs.lathe.live/#table). Return the console to read-only or off afterward.

## Verify the deployment

Use the public URL Lathe returns. Do not guess it.

- Open /healthz and check that it returns HTTP 200 and ok .
- Open /login , create an account, sign in and submit a guestbook message.
- Redeploy the app. Check that the message and the /data visit counter remain.
- Restart the app, Postgres and Auth separately. Check the message, account and counter again. These are engine and app restarts, not a full machine reboot.

## Clean up

Delete the machine from Settings in the portal, with DELETE /instances/{id}?confirm={id} in the REST API, or with delete_instance in MCP. Confirm that the delete job finishes and Billing marks the subscription cancelled. Revoke any temporary API key on the Access page. [Previous Lathe documentation ](https://docs.lathe.live/)[Next App deployment tutorials ](https://docs.lathe.live/tutorials)

## Related

- [Host a container beside its Postgres, flat price ](https://docs.lathe.live/guides/host-your-app)

Put the image of your app on the dedicated machine that runs its database: deploy from a push, your domain, TLS, nothing metered. From $15 a month.
- [Sign-in for your app with no price per user ](https://docs.lathe.live/guides/auth-flat-price)

Managed sign-in with the users in your own Postgres: email links, OAuth, MFA, sessions, in the flat plan price. Users are never a line on the bill.
- [Deploy from Claude Code, Cursor or ChatGPT ](https://docs.lathe.live/guides/deploy-from-ai-agent)

Copy-paste MCP setup for Claude Code, Cursor and ChatGPT, then create a machine, deploy an app and retrieve short-lived connection credentials.
