lathe docs

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. 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, REST API or 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.
  • 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.

  1. 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.
  2. Copy the Auth values. On the Auth tab, open Keys. Copy the Auth URL, which ends in /auth/v1, and the anon key.
  3. 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.
  4. 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.
  5. 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.
  6. 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.

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.

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

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. Return the console to read-only or off afterward.

Verify the deployment

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

  1. Open /healthz and check that it returns HTTP 200 and ok.
  2. Open /login, create an account, sign in and submit a guestbook message.
  3. Redeploy the app. Check that the message and the /data visit counter remain.
  4. 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.