Troubleshooting
When a deploy, a rehearsal or a room does not do what you expect.
On this page
- Signing in to the app
- Google refuses the sign-in
- Signing in with pitch
- The code is refused in the browser
- Sign-in was denied or expired
- The stored token is no longer valid
- Not on a Mac
- pitch init
- pitch.json already exists
- Not deployable yet: not single-origin
- Could not tell, and nobody to ask
- pitch validate
- The rehearsal
- Docker is not running
- No Dockerfile
- The image does not build in /app
- No CMD
- The health route does not answer
- The build stops as soon as it starts
- Installing at start
- Native modules
- A secret was found
- gitleaks could not run
- A static build fails
- A story step fails
- Publishing
- Not signed in
- Refused at a limit
- The project is paused
- The name is refused
- In the room
- Not found
- This room is paused
- This demo is full right now
- This room is not taking visits right now
- The demo does not start
- The demo paused
- A blank app
- The tour does not ring anything
- A tour step says Didn't work
- Pages not showing
- The connector
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 loginagain. - "The code expired before it was confirmed. Run
pitch loginagain." Nobody confirmed the code in time. - "Sign-in could not start: … answered …"
pitchcould not reach the platform it is pointed at.pitch whoamisays which one, and where that came from; unlessPP_APIorpitch config set apisays otherwise, it ishttps://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/pagesCommon ones:
- "pitch.json: is not here.
pitch initwrites one": run it in the folder that holdspitch.json, or runpitch init. - An unknown field: a misspelt field is refused rather than ignored, since
a misspelt
storywould 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":
pagesare paths relative topitch.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 inHOST, which the runner sets; a port of your own choosing is never reached; - the health route in
pitch.json(/healthzunlesshealthsays 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>:latestInstalling 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 buildexited 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. Setoutputinpitch.jsonto the folder it writes. - "FAIL build: output ../site is outside the project.":
outputmust 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,apiordocs; - it contains a word that borrows someone else's trust, such as
paypal,bankorlogin, 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, setbaseto the app's path. - A static build is served from the root of
output. An app at/needsoutput/index.html; an app at/coach/needsoutput/coach/index.html, and its files beside it. A request under an app's path that matches no file gets that app'sindex.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.jsonis uploaded only bypitch deploy --commitorpitch 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'spages, 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
pitchCLI 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_staticrefuses the files. It says every problem at once, each with its fix, in the wordscheck_bundleuses: most often an app or a tour step whose page is not among the files, such as noindex.htmlat 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 withpitch deploy, and the refusal says how to install it:npm install -g @pitch-product/cli, thenpitch login. Every check, and its fix lists them.