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