paperclipai/paperclip · warning
[paperclip] UI dist not found; running in API-only mode
Error message
[paperclip] UI dist not found; running in API-only mode
What it means
When the API server starts in static-serving mode (production), it scans candidate directories for a built UI dist containing index.html. If none is found, it warns and serves only /api routes — the board UI is unreachable in the browser while the REST API remains fully functional.
Solutions
- Build the UI first: run the repo build (`pnpm build`) so ui/dist with index.html exists next to the server.
- Verify the directory the server scans actually contains index.html (candidates are resolved relative to the server build output).
- For local development use vite-dev mode (`pnpm dev`) instead of static mode.
- If API-only operation is intended, ignore the warning and interact via REST/CLI.
Example fix
# before pnpm --filter server start # after pnpm build && pnpm start
Defensive patterns
Strategy: validation
Validate before calling
import fs from 'node:fs';
import path from 'node:path';
// Fail fast at deploy time instead of silently serving API-only.
const uiDist = path.resolve(__dirname, '../../ui/dist');
if (!fs.existsSync(path.join(uiDist, 'index.html'))) {
throw new Error(`UI dist missing at ${uiDist}; run pnpm build before starting in static mode`);
} Prevention
- Build UI and server in the same CI artifact; assert index.html exists in the package.
- Add a post-deploy smoke check that GET / returns HTML (not just /api/health).
- Use vite-dev mode only for local dev; static mode requires a prior build.
- Alert on this warn in production logs — it means the board UI is down.
When it happens
Trigger: Starting the server in static mode without a prior UI build (ui/dist absent); a packaged deployment that shipped only the server; the dist directory moved or renamed so no candidate contains index.html.
Common situations: Running the production entrypoint where only `pnpm dev` was ever used; Docker image built from server-only context; CI artifact missing the UI build; deploying to a host where the relative path to ui/dist changed.
Related errors
- [adapter-ui-loader] Failed to load UI parser for
- Add an objective after /goal to start a goal.
- Agent not found
- `}
- database-contract-unmet
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-08-18).
Data as JSON: /api/errors/b7dd9680fd9342a1.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/app.ts:977
// never change once built, so they can be cached aggressively.
app.use(
"/assets",
express.static(path.join(uiDist, "assets"), {
maxAge: "1y",
immutable: true,
}),
);
// Serve root/index through the same runtime HTML transform as SPA routes.
app.get(["/", "/index.html"], (_req, res) => {
res.type("html").set("Cache-Control", "no-cache").send(readBrandedStaticIndexHtml(uiDist));
});
// Non-hashed static files (favicon.ico, manifest, robots.txt, etc.):
// short cache so operators who swap them out see the new version
// reasonably fast, with must-revalidate overrides for index.html and
// sw.js (see staticUiCacheControl for why those two).
app.use(
express.static(uiDist, {
maxAge: "1h",
setHeaders(res, filePath) {
const override = staticUiCacheControl(filePath);
if (override) {
res.set("Cache-Control", override);
}
},
}),
);
// SPA fallback. Only for non-asset routes — if the browser asks for
// /assets/something.js that doesn't exist, we must NOT serve the HTML
// shell: the browser would try to load it as a JavaScript module, fail
// with a MIME-type error, and cache that broken response. Return 404
// instead. The index.html response itself is no-cache so a subsequent
// deploy's updated asset hashes are picked up on next load.
app.get(/.*/, (req, res) => {
if (req.path.startsWith("/assets/")) {
res.status(404).end();
return;View on GitHub (pinned to 3f1d897a7c)