Reference
Troubleshooting
Symptoms you will actually see from the Editor and the Viewer, what causes each one, and what to do about it.
Every symptom below is a real message from the code, not a paraphrase. If you are looking at an error, search this page for a distinctive phrase from it.
Two things are worth knowing before you start. The Editor refuses early and loudly rather than starting halfway: most of its failures happen in the first second and print one line. The Viewer validates its configuration at boot for the same reason, so a Viewer that started is a Viewer whose configuration parsed.
Editor will not start#
Repo not supported by Editor:#
The Editor checks your repository's shape before doing anything else, prints one line saying what is wrong, and exits with code 3. A distinct code exists so a wrapping script can tell "you pointed me at a webpack project" apart from "something broke."
| Message contains | Cause | Fix |
|---|---|---|
'vue' is at major 2; Editor supports only Vue 3 |
Vue 2. | There is no fix. Vue 2 is refused and is not on the roadmap. |
has neither a 'vue' nor a 'react' dependency |
Neither framework is declared in dependencies, devDependencies or peerDependencies. |
Declare the one you use. If this is a monorepo, point the Editor at the package that has the app, not at the workspace root. |
No package.json at … |
Wrong directory, or the project root is one level down. | Pass the directory containing package.json. |
Failed to parse …/package.json |
Invalid JSON. | Fix the JSON. |
There is no refusal for missing Vite. A vite dependency and a root vite.config used to be
required and are not any more: which dev server to start is a separate decision made after this
gate, and a project that fits none of the built-in dev servers is offered
attach mode rather than turned away. See what triggers exit
3 for the exact four reasons this gate refuses on today.
Full detail on the gate is in the repository gate.
Repo path not found: /some/path#
The path you passed does not exist. It is resolved against your current working directory, so a relative path behaves differently depending on where you ran the command from. The message prints the absolute path it tried. Check that first. Exit code 1.
Could not read .git directory:#
The directory exists but is not inside a git repository. The full message includes git's own words:
Failed to start editor: Could not read .git directory: Command failed:
git -C /path/to/my-prototype rev-parse --git-common-dir
fatal: not a git repository (or any of the parent directories): .gitThe Editor edits your working tree in place and relies on git for branches, publishing and undo
(see branch mode). Run git init and make at least one commit, or
point it at a real checkout. Exit code 1.
Canonical has in-progress git operations:#
Your repository is mid-merge, mid-rebase, mid-cherry-pick, mid-revert or mid-bisect. Editing over
that would write on top of unresolved conflict markers. Finish or abort the operation
(git merge --abort, git rebase --abort) and start again.
A dirty working tree and a detached HEAD are both fine: those are your call, not data loss.
Editor UI bundle not found at …#
The Editor serves its own interface from a built bundle at editor-cli/ui-src/dist, and it has
not been built. Build it once from the repository root:
npm --prefix editor-cli run build:uiThe message itself says npm run build:ui: that is the script name as seen from inside
editor-cli, which is why the command above uses --prefix.
Bridge bundle not found at …#
The bridge (the script Desde injects into your running prototype so the page can talk to the surrounding interface) has not been built. Build it once from the repository root:
npm run build:bridgeIt is written to dist/bridge-bundle.js at the repository root. Older documents in this
repository name functions/serve/dist/bridge-bundle.js; that path no longer exists.
Cannot find module '@vitejs/plugin-vue'#
failed to load config from /path/to/my-prototype/vite.config.ts
Failed to start editor: Cannot find module '@vitejs/plugin-vue'Your prototype's dependencies are not installed. The Editor executes your Vite config in order to merge its own plugins into it, so anything that config imports has to be present. Run your project's own install step inside the prototype repository and try again. Exit code 1.
Editor starts but does not work#
Every action fails, and the response says Invalid Origin#
{ "ok": false, "reason": "Invalid Origin (expected http://127.0.0.1:4321)" }The page loaded, but every request to /api/… comes back 403.
Cause. The Editor binds to 127.0.0.1 and requires every API request to carry
Origin: http://127.0.0.1:<port> exactly. localhost and 127.0.0.1 are the same machine but
they are different origins to a browser. If you typed http://localhost:4321, the page
served fine and every call after it was refused.
Fix. Use the URL the CLI printed:
▸ Editor UI ready at http://127.0.0.1:4321Open exactly that. If you bookmarked the localhost form, replace the bookmark: there is no
setting that widens the check, and there should not be, because it is what stops another page in
your browser from driving your editor.
Your change to an Editor panel does not appear#
You edited something under src/components/editor/ or editor-cli/ui-src/ and the interface
still shows the old version.
Cause. The Editor serves a built bundle, not your sources. The build pulls the shared
src/ tree in through an alias, so editing a React component in src/ changes nothing until you
rebuild.
Fix. Rebuild and reload the browser tab:
npm --prefix editor-cli run build:uiYou do not need to restart the CLI. Files are read from disk per request and index.html is sent
with Cache-Control: no-store, so a plain reload picks up the new bundle.
Smoke check warning: Bridge <script> tag not found in served HTML#
The Editor injected the bridge tag and then could not find it in what your dev server actually
served. The usual cause is a transformIndexHtml hook in your own Vite config that rewrites or
replaces the HTML after ours ran.
The Editor keeps running (the interface is up), but selection and inspection will not work
inside the prototype. Look for plugins in your vite.config that touch index.html.
A sibling warning, data-desde-src not found in any compiled source module, means the source-position
stamps that link a clicked element back to a line of code are missing. Either the source-tag plugin
was skipped, or the repository has no components of its own yet. Edits will fail without it.
Missing ANTHROPIC_API_KEY#
Missing ANTHROPIC_API_KEY. Set it to use Editor's AI features, or set
EDITOR_USE_CLAUDE_SUBSCRIPTION=1 to use the Claude subscription of the bundled
`claude` CLI (only appropriate when you are running Editor for yourself. See the README).Selection, inspection and deterministic editing need no credentials at all. Four things go through
the Editor's provider registry and refuse with this message rather than falling back to anything:
the LLM save lane that commits text, attribute and style edits when no deterministic applicator
took them; the structural repair lane behind /api/editor/llm-fallback; the agent's goal
verification; and opt-in generation of design-system rendering hints.
The two edit lanes are the ones that surprise people. A deterministic edit is instant and free, so inspector editing looks credential-free right up until one mutation the applicators refuse sends the whole save through the LLM lane.
Fix, in the app. Open the settings gear in the editor's top bar, choose Anthropic API key, and paste one. The key is checked with Anthropic before it is saved and takes effect immediately, with no CLI restart. This is the only route available in the desktop app: a macOS app launched from Finder inherits launchd's environment, not your shell's, so an export in a dotfile never reaches it.
Fix, in the shell. If you launch the Editor from a terminal, exporting a key still works and takes precedence over a stored one:
export ANTHROPIC_API_KEY=sk-ant-...The alternative, EDITOR_USE_CLAUDE_SUBSCRIPTION=1, routes those calls through the claude CLI's
own login. It is explicitly opt-in because it spends whoever's subscription is logged in on that
machine (appropriate when you are running the Editor for yourself, not when you hand it to
someone else).
Chat fails with an authentication error#
Chat is a different path from the message above: it runs on the Claude Agent SDK and signs in
through your local claude CLI. When those credentials expire you get:
Authentication failed (401). Editor signs in through your local `claude` CLI, and its
credentials look expired or invalid. Run `claude` then `/login` to re-authenticate
(or set a valid `ANTHROPIC_API_KEY`), then start a new chat turn.Do exactly that, then start a new turn.
A rate-limited turn is a different thing, and it is easy to mistake for this one: the session ends
in the same failed state and carries the same Failed badge in the session list. The only
place the two are distinguished is the toast: a warning reading Chat session rate-limited,
with try again in … appended when the provider sent a retry hint, instead of the error toast
Chat session failed. A rate limit is recoverable by waiting; re-authenticating will not help it.
Viewer will not start#
[viewer] failed to start: with ENOENT and dist/bridge-bundle.js#
The Viewer reads the bridge bundle at boot and there is nothing to read. Same fix as the Editor's version of this:
npm run build:bridgeMissing VIEWER_GITHUB_CLIENT_SECRET. Required when either of VIEWER_GITHUB_CLIENT_ID, VIEWER_GITHUB_CLIENT_SECRET is set#
Three groups of variables are all-or-nothing: sign-in, repositories, and SMTP. Setting one member makes the rest of that group required, and a missing one stops boot. That is deliberate: a Viewer running with half-configured sign-in is worse than one that refuses to start, because the failure would otherwise surface as a broken button several minutes later.
The message names the exact variable and the group it belongs to. See Viewer configuration.
Unknown VIEWER_PROFILE "…"#
selfhost is the only value. Leave the variable unset.
An error naming node:sqlite#
The Viewer stores its data through Node's built-in SQLite module, which needs Node 24 or newer.
The repository README's general "Node 20+" line does not apply here. Check with node -v.
The dashboard 404s on pages you know exist#
You started the Viewer in production mode against a stale or missing build. In production the
Viewer serves a prebuilt .next directory; whatever is in it is what you get, however new your
source is.
For development, use the mode that compiles on demand:
npm --prefix viewer run dev:localFor production, build first:
npm --prefix viewer run buildViewer permissions#
Only editors and admins may connect a repository#
{ "error": "Only editors and admins may connect a repository" }You are signed in, you can see the project, and every attempt to connect a repository, trigger a build, or change its settings is a 403.
Cause. Managing a project needs instance-level authority, not a role on the project itself. There are no per-project roles any more. You need the Editor or Admin instance role. A Viewer can read and comment on any project they can access, but cannot manage one, even a project they created a long time ago under the old model.
Fix. Ask an admin to change your role to Editor or Admin from /settings → Members. See
members and tokens for what each role can do.
This action requires a write-scoped token#
{ "error": "This action requires a write-scoped token" }Reads work, the first write is a 403.
Cause. The token is read-only. The mint form at /settings has read checked and write
unchecked by default, so this is the default path, not an edge case. It is the most common
reason an Editor connected to a Viewer can list comments but cannot post one.
Fix. Mint a new token with write checked and swap it in. Scopes cannot be edited after
creation. You can check what a token carries without trying a write:
curl -s http://localhost:3100/api/v1/me -H "Authorization: Bearer $VIEWER_TOKEN"scopes in the response is null for a browser session and an array for a dsv_ token.
A 404 on a project you are sure exists#
Two different causes:
- You may not read it. A project you have no access to answers with exactly the same 404 as a project that does not exist. A 403 would confirm it exists. Ask an admin or editor to add you to the project's access list, or check whether you are signed in as the account you think.
- You used the slug as an id. There is no
GET /api/v1/projects/:slug. Lookup is by id only. FetchGET /api/v1/projectsand find the slug in the list. See the API reference.
Prototype pages may not call the viewer API#
JavaScript inside a hosted prototype tried to call the Viewer's API. Under path serving the
prototype runs on the same origin as the Viewer, so its fetch would otherwise carry the reviewer's
session cookie. Requests whose Referer is a page under /p/ are refused.
This is expected behaviour. If your prototype needs to call something, point it at a mock inside its own build.
Serving a prototype#
The prototype loads, then 404s as soon as you navigate#
The first page renders. Clicking a link inside the app gives you the Viewer's own 404 page, and
the URL bar shows something like http://localhost:3100/checkout (with no /p/{slug}/ in it).
Cause. Under path serving, your prototype lives at /p/{slug}/, but a client-side router that
does not know that pushes to the origin root. /checkout is not under /p/, so it never reaches
your prototype at all. The Viewer's fallback for unknown client-side routes only applies inside
the prefix.
Fix: pick one.
- Tell your router its base. The Viewer injects
<base href="/p/{slug}/">into the served HTML. Vue Router with no explicit base reads that<base>tag and adopts it, which is why many Vue apps work unchanged. React Router does not. Setbasenameto/p/{slug}/. - Build relative.
base: './'in your Vite config produces output with no root-absolute references, which also removes the related class of missing-asset problems. - Serve on a subdomain. Set
VIEWER_SERVE_DOMAINand the prototype sits at the root of its own origin, where none of this compensation is needed. It requires wildcard DNS and a wildcard certificate. See serving.
Prototype has no deployment yet#
The project exists but nothing has been built or uploaded to it yet. Trigger a build, or upload a bundle. See builds.
Prototype not found#
No project with that slug, or one you may not read: the two are deliberately identical. Check the
slug against GET /api/v1/projects.
Bundle has no index.html at its root#
Your upload's archive has index.html one level down, usually because it was made with
tar czf bundle.tar.gz dist rather than from inside the output directory. Archive the contents,
not the folder:
tar czf bundle.tar.gz -C dist .Images or fonts are missing, and the build log has a Note:#
Note: 12 root-absolute reference(s) to bundle paths found in JS/CSS (e.g. "/assets/).Your build baked absolute paths like /assets/logo-a1b2c3.png into its JavaScript or CSS. The
Viewer rewrites HTML attributes to the /p/{slug}/ prefix, but it cannot reach inside a bundled
string. Those requests are resolved by a fallback that leans on the Referer header, which works
for content-hashed filenames and is unreliable for anything else.
Build with base: './' for output that carries no root-absolute references at all.
Builds#
Builds are not enabled on this deployment#
A 503. There is no GitHub App configured, so there is no build queue: the runner clones with the App's installation token. Configure the repositories group in Viewer configuration, or upload a bundle directly instead.
Connect a GitHub repository before building#
The project has no repository attached. Connect one from the project's page.
A build is already running for this project#
A 409, and the response includes the in-flight deploymentId (usually the build you actually
wanted to watch). Stream its log rather than starting another.
Webhooks are not configured#
A 503 on POST /api/v1/webhooks/github. VIEWER_GITHUB_APP_WEBHOOK_SECRET is unset, so the
Viewer refuses rather than processing an unverified payload. Set it to the same value as your
GitHub App's webhook secret.
Pushes do not trigger a build, and GitHub shows the delivery as successful#
A push only builds a project whose connected repository and branch match, and which has
autoDeploy turned on. When nothing matches, the Viewer still answers 200: a non-2xx would make
GitHub mark the delivery failed and retry forever for something that is not an error.
Check the project's repository settings: owner, name, branch, and the auto-deploy toggle. A push to a branch other than the connected one is ignored by design.
Cannot find package 'vite' in a build log#
Your build toolchain is a devDependency and something omitted it during install. The Viewer sets
NODE_ENV=production on the build step only, never on install, precisely because npm reads
that as --omit=dev. If you see this, check whether your own installCommand sets it.
Your build cannot see an environment variable#
The Viewer does not hand its process environment to your build. The child gets an allowlist
(PATH, HOME, TMPDIR, CI=true, and a couple of git variables) plus NODE_ENV=production on
the build step. A variable you exported for the Viewer will not reach your prototype's build.
Commit the values your build needs, or use your framework's own .env files inside the repository.
Sign-in#
The GitHub redirect comes back with an error, or never comes back#
The callback URL registered on your GitHub App must match
<VIEWER_PUBLIC_URL>/api/v1/auth/github/callback exactly, including scheme, host and port. A
Viewer on http://localhost:3100 and an App registered for port 3000 will start the flow and
never finish it.
Signed in, then signed out again on the next request#
The session cookie's Secure flag is set if and only if VIEWER_PUBLIC_URL starts with https://.
If you terminate TLS at a proxy and left VIEWER_PUBLIC_URL on http://, the cookie is issued
without Secure, which is a real exposure, and depending on the browser and any upstream policy,
may also mean the cookie is not stored at all.
Whenever a browser reaches the Viewer over HTTPS, VIEWER_PUBLIC_URL must say https://.
They authenticated with GitHub, but land on "This viewer is invite-only"#
Authenticating with GitHub, or clicking a magic link, is not the same thing as getting an account.
The Viewer is invite-only. A separate check, admitSignIn, decides after every successful
authentication whether an account gets created at all. It admits, in order: an existing account,
a valid invite for that exact email, the local operator handing off to GitHub, a brand-new
instance with zero users, or a matching domain rule. Anyone who matches none of those sees the
plain "invite-only" page and nothing is created.
Fix. Invite the address from /settings → Members, or add a domain rule if everyone at that
company should get in automatically. See members and tokens for
both.
Setting VIEWER_ALLOWED_EMAIL_DOMAINS doesn't let new people in#
This variable only does something on the first boot, and only if the stored domain-rule table
is still empty at that moment. It copies each domain into a stored rule once, then is ignored on
every later boot, even if you change it. If you need to open up a new domain, add it as a domain
rule from /settings instead of editing the environment variable: the environment variable will
not be read again.
A value that parses to nothing (VIEWER_ALLOWED_EMAIL_DOMAINS=) is treated as unset, so it
seeds no rules at all.
Still stuck#
- Confirm the shape of your repository against the repository gate.
- Confirm which program you should be running at all (Editor or Viewer) in what is Desde?.
- Check what a route actually requires in the API reference.