CLI
The papervine CLI serves any MDX + docs.json repo with the Papervine renderer — on your machine while you write, or in production.
The papervine CLI serves any MDX + docs.json repo with the Papervine renderer — on your machine while you write, or in production.
papervine runs inside a folder of MDX + a docs.json and serves that folder with the
Papervine renderer — the same renderer that powers hosted Papervine sites, so what you see
locally is what you ship.
It is a production build, not a development server: NODE_ENV=production, no dev-mode
compile step, no hot reload. Two commands run it — papervine dev while you write, and
papervine serve on anything serving real traffic. They are the same server; serve just binds
every interface and refuses to scaffold. See
Serving a site in production.
npx papervine new my-docs # create a site from the starter template
npx papervine dev # preview ./ (must contain docs.json)
npx papervine dev ./docs # preview a subfolder
npx papervine dev -p 4000 # preview on a specific port
papervine serve ./docs # serve it for real (binds 0.0.0.0)
Requires Node 20.9 or newer.
papervine new [dir] writes a complete working site — docs.json, pages, a component
gallery, an OpenAPI example — which you then preview and trim. It refuses a directory that
isn’t empty unless you pass --force, because scaffolding writes files and a mistyped path
shouldn’t cost you your work. Directories holding only dotfiles (a fresh git init) count as
empty.
You don’t have to know about new to find it, either. Running papervine dev where there
are no docs yet offers to create them:
$ papervine dev
! no docs.json in /Users/you/my-docs
Create a starter docs site here? [y/N]
That prompt only appears in an interactive terminal. In CI or a pipe the command prints the
error and exits non-zero as it always has — a prompt nobody can answer is worse than a clear
failure. --yes scaffolds without asking.
The template ships inside the package, so new works offline and always matches the version
of the CLI that wrote it.
The CLI is a previewer, not a deployer — it has no deploy and no login. Publishing
happens through Git (push your repo) and the hosted control plane, not the CLI.
| Flag | Applies to | Description | Default |
|---|---|---|---|
-p, --port <port> | dev, serve | Port to serve on | 3000 |
--host <addr> | dev, serve | Bind address | 127.0.0.1 for dev, 0.0.0.0 for serve |
-y, --yes | dev | Create a starter site if there are no docs, without asking | — |
-f, --force | new | Scaffold into a directory that isn’t empty | — |
-h, --help | all | Show help | — |
-v, --version | — | Print the installed version | — |
Without --port, a busy port is skipped and the next free one is used. With an explicit
--port, a busy port is an error instead — an explicit request shouldn’t be quietly
redirected somewhere you weren’t told about.
--host sets the bind address, and its default depends on which command you ran: dev binds
loopback (127.0.0.1), so a command you run on your laptop isn’t on the network; serve binds
every interface (0.0.0.0), because being reachable is the point. Both print which address they
bound at startup. PAPERVINE_HOST is equivalent to --host, for when an environment variable is
easier to set than an argument.
The variable is PAPERVINE_HOST, not HOSTNAME. Docker sets HOSTNAME to the container id
and Kubernetes to the pod name, so reading it would let an unrelated part of the environment
decide what the site is reachable from.
Images are optimized on the fly — resized to what the page actually needs and served as WebP to browsers that accept it. A 220KB screenshot typically goes out at a couple of KB.
That work is done by sharp, which ships as an optional
dependency because it contains a compiled binary specific to your operating system and CPU.
Installing papervine normally installs the right one for your machine and there is nothing to
configure.
If it can’t be installed — an unusual platform, or --no-optional — the CLI still runs and
serves your images at their original size, and says so at startup:
! image optimization unavailable — serving images at original size.
Install the optional dependency with `npm i sharp` in this project.
Worth acting on if you’re serving a site to real readers rather than previewing locally. Optimization is the difference between a page sending a few KB of images and a few hundred.
Help and status output is colourised in the Papervine palette when the CLI is attached to a
terminal, and plain everywhere else — piping or redirecting gives you clean text instead of
escape codes, so papervine --help | grep port and anything parsing the output work as you’d
expect. Terminals that don’t advertise truecolor get the nearest 256-colour match rather than an
escape they can’t render.
| Variable | Effect |
|---|---|
NO_COLOR | Set to any non-empty value to disable colour entirely (no-color.org) |
FORCE_COLOR | Set to keep colour through a pipe or into a file; 0 disables colour |
NO_COLOR wins over FORCE_COLOR when both are set.
Pages render per request and read your files from disk each time, so saving a file and refreshing the browser shows the change. There is no hot reload — a refresh is the update.
That’s a consequence of how the CLI ships: the renderer is prebuilt into the package. Nothing is compiled on your machine, so there’s no build toolchain to install and no first-run compile — the preview starts serving in about a second. The tradeoff is the missing hot reload.
Because the CLI runs a production build of the same renderer behind hosted Papervine sites, serving your own docs from it is supported rather than a workaround. Four things change when you do.
Use serve. It binds every interface, so nothing else is needed to be reachable:
papervine serve ./docs --port 3000
dev stays on loopback, which is right for a command on your laptop and wrong for a server.
Either can be pinned with --host (or PAPERVINE_HOST) — and pinning serve back to
127.0.0.1 is the right move behind a reverse proxy, since only the proxy can then reach it.
Terminate TLS in front of it. The CLI serves plain HTTP and does no Host routing, so put
nginx, Caddy, or a cloud load balancer in front and proxy to that port.
Publish by replacing files. Pages read from disk per request, so writing new MDX into the served directory updates the site — no rebuild and no restart. The costly half of rendering (MDX compilation and syntax highlighting) is cached, so repeat requests don’t recompile, and the search index is fingerprinted by content and rebuilt only when the files change.
Know what isn’t included. One site per process, no caching layer or CDN, no reader
authentication, no analytics. The assistant and the MCP server are
included — a self-hosted site serves /mcp with the same tools a hosted one does. Those are the control plane, and none of it is in this package —
see How it relates to the hosted product. A process
behind a reverse proxy is a complete deployment when you don’t need them.
The full guide — Docker, systemd, container platforms, nginx and Caddy configs, publishing updates, and what you give up versus hosted Papervine.
The CLI is just the renderer — MDX compilation, docs.json parsing, navigation, the
component set, OpenAPI endpoint pages — compiled into the package as one self-contained
application.
It deliberately carries none of the hosted control plane — no authentication, database, object storage, or realtime. Those are services of the hosted product, not things a local previewer needs, so they’re absent from the package entirely rather than disabled at runtime.
Search and the AI assistant are included: neither needs the control plane. Search is an in-memory index over the folder you’re previewing, and the assistant answers by searching and reading those same pages. What the assistant needs from you is a model — see AI assistant below.
Because the stylesheet is compiled when the package is built, arbitrary Tailwind utility
classes written in your own MDX aren’t in it. Use the built-in components and docs.json
theming instead.
The assistant that runs on hosted Papervine runs here too. It answers questions about your docs
with citations, retrieving by searching and reading your pages — there’s no index to build and no
vector database, because retrieval is the same full-text search the ⌘K palette uses.
It appears in the navbar once a model is configured, and is absent when one isn’t. Nothing to enable; the SDKs ship with the CLI, and you bring a model. The model id and the routing always travel together:
Put them in your docs project’s .env.local — the CLI loads it — or export them before running.
A provider key on its own does nothing. AI_ROUTING defaults to gateway, so exporting
ANTHROPIC_API_KEY and starting the server leaves the assistant hidden while it looks for a
gateway key. Always set the model and the routing.
Copy-paste recipes for Claude, ChatGPT, Gemini, the AI Gateway, Ollama, LM Studio and any OpenAI-compatible server — plus every variable, and what each 503 message means.
Usage is billed by whoever provides the model — your key, your account, at their prices. The CLI meters nothing and reports nothing anywhere; it has no way to, since it carries no database and no telemetry. That is the one substantive difference from the hosted assistant, which is metered against your plan’s credits and records what readers asked so you can see which questions went unanswered.
papervine dev compiles the repo’s MDX on your machine, but it does not execute the
repo’s code there. Anything a page computes — an expression, a component you defined, a hook —
runs in your browser, the same as any web page. Content that only the server renders is
Markdown, the built-in components, and literal values, none of which is author logic.
That is deliberate: a docs page can’t read your environment variables, touch your filesystem, or
run commands, because there is no server-side step where its code runs. A page that tries is
either evaluated in the browser (where there is nothing to reach) or refused outright — imports
outside /snippets/ and dynamic import() don’t render at all.
It is still someone else’s JavaScript running in your browser on the preview’s origin, which is the same trust you extend to any site you visit. Use normal judgement with an unfamiliar repo — but previewing one no longer gives it your machine.
The surface is narrow by construction too: it binds loopback by default, serves only asset file types out of your content directory, and refuses remote image URLs. The one thing that talks to the network is the assistant, and only when you have configured a model and asked it something — your question and the retrieved page text go to the provider you chose, and nowhere else.
no docs.json in <dir> — the CLI is a docs-repo previewer, so it needs a docs.json at
the root of the folder you point it at. Docs usually live in a subdirectory, so pass it:
papervine dev ./docs.
port 3000 is already in use — you passed an explicit --port that’s taken. Choose
another, or drop the flag and let the CLI pick the next free port.
A component renders as plain text — unknown components degrade to their children rather than failing the page, which is deliberate. Check the name against the component library.
Papervine is one repository that produces two products from a shared core:
Compiles MDX, parses docs.json, builds navigation, renders the component set and
OpenAPI pages. An internal library, shared by both products.
A thin app that mounts the render engine over a local folder for papervine dev,
published prebuilt. The one package behind npx papervine.
The hosted control plane consumes the same @papervine/renderer, then adds multi-tenancy,
Git sync, auth, analytics, and the assistant on top. Because the renderer is one shared
engine, a docs repo renders identically whether you preview it locally with the CLI or
deploy it to a hosted site.
new, dev and serve are the command surface today. Planned next: broken-links (dead
internal links), validate (a strict config/frontmatter gate for CI), openapi-check, and
build (static export).