Troubleshooting

When a deploy, a rehearsal or a room does not do what you expect.

On this page

Find what you are seeing, then read what causes it and what to do. The words in quotation marks are the ones pitch, the app and the room print, so you can search this page for the message in front of you.

pitch stops at the first thing it cannot do, prints FAIL and why, and leaves nothing half-published: a deploy uploads nothing until every check has passed, and only with --commit. Errors lists every refusal by where it comes from.

Signing in to the app

Google refuses the sign-in

Continue with Google is open to invited testers only for now, so for anyone else Google's own page refuses it before Roomi is reached. Use Continue with GitHub. There is no sign-in by email.

Signing in with pitch

The code is refused in the browser

pitch login prints a code and the page to enter it on. That page answers:

It says Because Do
"That code is not one pitch issued." A mistyped code Check it against the terminal, or run pitch login again
"That code has expired." Codes are short-lived Run pitch login again for a new one
"That code has already been used." It was approved or denied once already Run pitch login again
"That code belongs to someone else’s sign-in." The code was started by a different person Approve only a code your own pitch login has printed a moment ago
"Your session has ended. Sign in again." You are signed out of the app Sign in, and you come straight back to the code

Sign-in was denied or expired

  • "Sign-in was denied in the browser." Someone chose No, deny it on the code page. Run pitch login again.
  • "The code expired before it was confirmed. Run pitch login again." Nobody confirmed the code in time.
  • "Sign-in could not start: … answered …" pitch could not reach the platform it is pointed at. pitch whoami says which one, and where that came from; unless PP_API or pitch config set api says otherwise, it is https://api.getroomi.com.

The stored token is no longer valid

pitch whoami says "The stored token is no longer valid. pitch login again." The session behind it has ended, for example after pitch logout on another machine. Run pitch login.

Not on a Mac

pitch login keeps its token in the macOS Keychain. Elsewhere it stops with "the keychain could not be read", because there is no Keychain to write to. pitch reads a token from PP_TOKEN instead, but the app cannot issue one yet: API tokens are shown in Settings as not built. For now, sign pitch in on a Mac.

If PP_TOKEN is set, pitch login and pitch logout refuse with "PP_TOKEN is set, so the Keychain is not in use. Unset it to sign in or out."

pitch init

pitch.json already exists

"pitch.json already exists. pitch init --force replaces it; pitch init --print shows what it would write." pitch init never overwrites your manifest unless you ask. Use --print to compare, then edit the one you have or replace it with --force.

Not deployable yet: not single-origin

"Not deployable yet: this repo is not single-origin." pitch init found several apps, each on its own dev port. A room loads your demo from one origin, so it has to become one server that holds every app under its own path, as Bullring's does. pitch init prints the change it proposes and does not make it: one list of apps and their paths, each app built with that path as its base, one server that serves each app's built files under its path and the API ahead of them, and a Dockerfile that runs it on PORT with a health route. Then run pitch init again. Container demos has the contract.

Could not tell, and nobody to ask

"Could not tell, and there is nobody to ask:" followed by questions and "Nothing was written." pitch init could not work something out and is not running in a terminal it can ask in. Each question is printed with the flag that answers it:

pitch init --kind container --health /healthz --device app=laptop --device coach=tablet
pitch init --kind static --output dist

--yes takes the default for any question that has one. Where the export of a Next app lands has none when pitch init cannot read its distDir: a note above the questions says so, and --output answers it.

pitch validate

pitch validate checks pitch.json and what it names without building anything. A problem looks like this, and each one names the page, and usually the field, that explains it:

  pitch.json does not validate: 2 problems
    stroy: pitch does not know this field (did you mean "story"?); the platform would ignore it
      https://getroomi.com/docs/pitch-json
    pages: docs/why-corner.md: is not there
      https://getroomi.com/docs/pages

Common ones:

  • "pitch.json: is not here. pitch init writes one": run it in the folder that holds pitch.json, or run pitch init.
  • An unknown field: a misspelt field is refused rather than ignored, since a misspelt story would be a tour that silently never appears.
  • "Dockerfile: is not here": a container build is built from a Dockerfile at the project root.
  • A page "is not there" or "is outside the project": pages are paths relative to pitch.json, inside the project.

A note is not a problem and does not stop a deploy, but read it: notes about the Dockerfile are the rehearsal failures below, caught early. pitch.json has every field.

The rehearsal

pitch deploy without --commit builds, scans and runs your demo on this machine. Each check prints ok or FAIL.

Docker is not running

"FAIL build: Docker is not running (docker info failed). Start it and run again." A container demo is built and rehearsed in Docker. Start Docker Desktop, or your Docker daemon, and run pitch deploy again. A static demo needs no Docker.

No Dockerfile

"FAIL build: a container build needs a Dockerfile at the project root." Add one, or if the demo is a built folder with no server, set "kind": "static".

The image does not build in /app

"FAIL the image's WORKDIR is /usr/src/app. The runner unpacks a build into /app, so the Dockerfile's last stage needs WORKDIR /app." Only /app is taken from your image, so the last stage must build there. Set WORKDIR /app in it.

No CMD

"FAIL the image has no CMD, so nothing says how the build starts." Add a CMD in exec form, such as CMD ["node", "server/index.js"]. pitch validate warns earlier: a Node base image's own CMD starts a Node prompt, not your server.

The health route does not answer

"FAIL the runner could not start the build: /healthz did not answer 200 within 90000 ms. Its last words:" and the last lines your server printed. The runner waits for your health route before anything is shown to a viewer. Check that:

  • the server listens on the port in PORT, and on the host in HOST, which the runner sets; a port of your own choosing is never reached;
  • the health route in pitch.json (/healthz unless health says otherwise) exists and answers 200 once the server is up;
  • it is up within 90 seconds.

"FAIL the runner never opened its control port" is the runner itself not starting in time, usually a slow machine building for another architecture. Run again.

The build stops as soon as it starts

If your server exits while it starts (a missing file, an uncaught error), the container stops with it, and the rehearsal can end with a bare fetch failed instead of a FAIL line. To see what your server says, run the image pitch built yourself:

docker run --rm --platform linux/amd64 -e PORT=8080 -e HOST=0.0.0.0 -p 127.0.0.1:8080:8080 pp-build/<project>:latest

Installing at start

pitch validate notes "the CMD installs or fetches packages. A demo’s container has no internet: install in the build, not at start". A CMD that runs npm install, pnpm install, yarn add or npx cannot work for a viewer, whose copy has no network. Install in the Dockerfile, and start the built server directly.

Native modules

pitch validate notes "the last stage is Node 22; the platform runs every build on Node 24, and a native module built for 22 may not load". The runner is Node 24 on linux/amd64, whatever your base image is. Build on node:24-slim (or another Node 24 image), and install inside the Dockerfile, so native modules are compiled for Node 24 on linux/amd64. Never copy a node_modules folder in from your own machine: on a Mac it holds binaries for macOS. pitch deploy always builds for linux/amd64, whatever your machine is.

A system package installed with apt-get is outside /app, so it is not there when your demo runs.

A secret was found

"FAIL secrets: gitleaks found secret(s) in the project:" followed by each finding's rule, file and line, and "Values redacted. Remove it, then rotate it: it has been on disk in plain text." The deploy stops before anything is uploaded.

Remove the secret from the project, and replace whatever used it with a mock: a demo runs on mock data and has no internet, so a real key could do nothing there anyway. Then rotate the key where it came from. The scan covers the project, then the built output or bundle and its ENV; your own .gitleaksignore is not read.

The project scan leaves out the folders build tools generate, such as .next, where Next writes keys of its own each time it runs; a finding there no longer stops a deploy, and there is nothing to delete first. A finding in the build output or bundle is in what would be uploaded, generated or not, so it always stops the deploy. Static demos lists the folders.

gitleaks could not run

"FAIL secrets: gitleaks could not be started (…). That is not a clean scan, so the deploy stops here." and "Install it (brew install gitleaks), or set PP_GITLEAKS to its path." A scan that could not run is never treated as clean. Install gitleaks, or point PP_GITLEAKS at it.

A static build fails

  • "FAIL build: npm run build exited 1" and the last lines it printed: your own build script failed. Run it yourself and fix it.
  • "FAIL build: dist/index.html is missing after the build.": the build wrote somewhere other than output. Set output in pitch.json to the folder it writes.
  • "FAIL build: output ../site is outside the project.": output must be inside the project.

A story step fails

  FAIL story 2/4 "Flag Tom": POST /api/demo/flag answered 404
  FAIL story: 1 step failed on the rehearsal's copy (flag). A viewer's tour would stop there too.

The rehearsal runs every step's calls, in order, on its own copy, as the room will on a viewer's. A FAIL is a step a viewer would see fail. The usual cause is a step that depends on an earlier one: a viewer can start at any step from the outline, so each step's run must set up what it needs itself. A step that names a page is also asked for it (its page, GET /coach/today, answered 404): the path is usually mistyped, or missing the trailing slash the demo's routes use. Fix the story, not the demo, and rehearse again. Writing the tour has the shape.

Publishing

Not signed in

"Not signed in. pitch login first." pitch deploy --commit checks this before it builds, so you do not wait for a build it cannot send.

Refused at a limit

A request your plan does not allow is refused with what the plan allows, and pitch prints it after the request and its status:

  POST /api/projects/:projectId/uploads answered 402: Free allows 2 deploys in any 24 hours. Pro deploys without a limit — paid plans are coming soon.

The deploy count is a rolling 24 hours, so the next one is possible 24 hours after the oldest of the two. Plans and limits has every limit and its exact refusal.

The project is paused

"… answered 423: this project is paused: the workspace has more projects than its plan allows. It opens again when the plan allows it: ask Roomi to change the plan while paid plans are coming soon". A paused project takes no deploys and no links, and its room admits nobody. See Plans and limits. "this workspace is suspended by Pitch Product; contact support" and "this project was taken down by Roomi; contact support" are paused by Roomi, not by your plan.

The name is refused

pitch deploy --commit creates the project under an address made from name in pitch.json, or the one --project gives. The address is refused when:

  • it is not 3 to 40 lower-case letters, digits and single hyphens;
  • it is a name the platform uses itself, such as app, api or docs;
  • it contains a word that borrows someone else's trust, such as paypal, bank or login, or a name Roomi has banned;
  • another project has it, or had it in the last 30 days.

"The project name "…" makes no usable slug; pass --project <slug>." means the name gave fewer than three usable characters. Pass --project with the address you want.

In the room

Not found

A room answers a plain "Not found" for every refusal, so a refusal says nothing about what exists. The same page means any of:

  • the link was revoked, or its expiry has passed;
  • the token in it is wrong: a character lost when it was copied;
  • the project is paused by Roomi (over the plan, or the workspace suspended), deleted or taken down;
  • the workspace is on Free, which has no private links: links made on a paid plan are closed ("Closed on Free" in the app) until it is on one again;
  • at a public address: the project is not listed, or its card is waiting for review again after an edit.

If you are the viewer, ask whoever sent the link for a new one. If you sent it, check the link in the project's Links tab, and the project's state.

This room is paused

"This room is paused" and "… has paused this room for now. Your link still works: the room opens here again when they resume it." Someone in the workspace paused the room, perhaps with a note beneath. The link is fine. To open the room again, Resume the room in the project's Settings; Room history on its Activity tab says who paused it and when. See Pausing a room.

This demo is full right now

"This demo is full right now" and "…'s demo has as many people in it as it can take at once. Try again in a few minutes." Either a listed project's public address already has five people in it, or a container demo has as many copies running as the project's plan allows. Seats come free as people leave. See Plans and limits.

This room is not taking visits right now

"This room is not taking visits right now" and "…'s room has reached its viewing allowance for this month." The workspace has used its viewer-hours for the month. Nobody already in a room is cut off, and the room opens again when the month turns (UTC). See Plans and limits.

The demo does not start

The stage says what the viewer's copy is doing:

The room says Because
"Starting the demo…" The copy is starting. It began as the room opened, so it usually finishes while the viewer reads.
"The demo is starting — high demand, one moment." The platform is running as many demos as it allows at once. The room asks again every five seconds for a minute.
"The demo is still in high demand. Try again in a moment." A minute passed without a place. Try again asks again.
"The demo could not start." The copy did not become ready in time, or stopped while starting. Rehearse again: whatever stops it there will stop it here.
"The live demo is paused right now. The notes are still here." Roomi has paused new demo containers. The pages still work.
"The live demo is not running on this machine." A local development room with no Docker running.

The demo paused

"The demo paused while you were away." with Resume the demo. A copy runs only while its room is in use: hidden or untouched for ten minutes, the room stops keeping it awake, and it stops three minutes later. No copy runs longer than two hours. Resume the demo starts a fresh copy, seeded from the start.

A blank app

The frame shows your app's page but it is empty, or unstyled. Almost always its scripts and styles are being asked for at the wrong path:

  • Each app is served under its path. An app at /coach/ must ask for its files under /coach/, not /. With Vite, set base to the app's path.
  • A static build is served from the root of output. An app at / needs output/index.html; an app at /coach/ needs output/coach/index.html, and its files beside it. A request under an app's path that matches no file gets that app's index.html, so client-side routing works.
  • A container demo must serve each app's built files under its path itself.

Open the app in its own tab from the room, and look in the browser's developer tools for the requests that answer 404. In a container demo, anything your server fetches from the internet as it answers fails too: its container has no network.

The tour does not ring anything

A step's point is a CSS selector the room asks your page to find and ring. It is best effort: if nothing on the page matches within a few seconds, the step shows without a ring, and nothing else breaks. It is looked for on the page the app is showing: the step's path when it has one, and otherwise wherever the viewer left the app, so a step that points at one page's element should open that page. The rehearsal does not check selectors, so look at the room after publishing. Prefer an id, a data- attribute or a class your own code names, never one a build tool generates.

A tour step says Didn't work

"Didn't work: …" with what the demo said, or "the demo answered 500", or "the room could not reach the demo". The step's calls ran on this viewer's copy and one was refused. The rehearsal runs the same calls, so rehearse again and read its FAIL line.

Pages not showing

  • A page listed in pitch.json is uploaded only by pitch deploy --commit or pitch pages push. A rehearsal uploads nothing.
  • An HTML page shows blank, or without its images. Pages are sandboxed: they run no script and load nothing from the network. Inline the styles, and embed images as data: URIs.
  • "… is longer than a page can be (2000000 characters)." Split it.
  • A page you removed came back. It is still in pitch.json's pages, and the next deploy uploaded it again.

Pages covers them in full.

The connector

  • The app says "This address is on this computer." Claude's connectors reach only public https addresses, and a local platform is not one. Use Claude Code with the pitch CLI instead.
  • "This connection may only read. Reconnect and allow changes." The connection was allowed to read but not to change anything. Disconnect Pitch Product in Claude, connect again, and allow both.
  • "The platform refused this connection. Disconnect Roomi in Claude and connect it again." The connection's approval no longer holds, for example because you left the workspace it was for. Connect again.
  • "That request has expired or was already answered. Start again from Claude." The consent page was left open too long, or answered twice.
  • It shows the wrong projects. A connection acts in the workspace you were in when you allowed it. Switch workspace in the app's Settings, then connect again.
  • deploy_static refuses the files. It says every problem at once, each with its fix, in the words check_bundle uses: most often an app or a tour step whose page is not among the files, such as no index.html at the root. From chat, a demo is at most 200 files and 3 MB, with relative paths and no hidden files ("a .env has no place in a demo"). Anything bigger, or with a server, is deployed with pitch deploy, and the refusal says how to install it: npm install -g @pitch-product/cli, then pitch login. Every check, and its fix lists them.