An editable, JSON-backed docs engine. Pages are stored as plain JSON, rendered and edited live in a React (Vite) app, and written back through a small zero-dependency Node API. You vendor the engine into a project and run it against that project's own content — the engine ships the app and the system pages; your pages, backlog, uploads, and progress stay yours.
polaris init # scaffold a fresh instance into ./polaris/
polaris dev # API server + Vite dev server, together
polaris dev finds the instance from where you're standing — run it from inside
polaris/ or from the parent beside it (no cd needed).
init with no argument scaffolds into a new polaris/ folder beside you (pass
an explicit path — polaris init my-docs — to choose another). If that folder
isn't already inside a git repo, init git-inits it so the engine can be
vendored as a submodule there.
init also runs npm install in the vendored engine's web/ for you, so the
instance is runnable right away — no separate dependency step (pass
--no-install to skip it). Then open the Vite URL dev prints (typically
http://localhost:5173/).
polaris dev is instance-aware from your current directory: run it from
inside an instance folder — or from the parent beside it, where it finds the one
instance init scaffolded — and it starts THAT instance via the instance's own
vendored (pinned) engine, even when polaris is a global command living in a
different checkout. The same holds for migrate and status. (init is the
exception: it targets a chosen dir, not the ambient instance.)
The polaris command is bin/polaris.mjs in this repo. To put it on your PATH:
make install # symlinks bin/polaris.mjs into ~/.local/bin (tracks this checkout)
make uninstall removes the link; make install PREFIX=/usr/local installs
elsewhere. Alternatively npm link this repo, or invoke it directly
(node path/to/polaris/bin/polaris.mjs …).
The server itself has no dependencies; only the React app (web/) does, and
init installs those. If you ever need to install them by hand (e.g. after
--no-install), polaris dev refuses to start without them and tells you to run
cd web && npm install.
One entrypoint (bin/polaris.mjs) unifies what used to be several manual steps:
| Command | What it does |
|---|---|
polaris init [dir] |
Scaffold a fresh instance: polaris.config.json, db/ content dirs, a seeded .polaris-version, and a starter home.json; vendor the engine at <dir>/engine; and npm install the app's deps so it's runnable right away. [dir] defaults to ./polaris; if it isn't already inside a git repo, init git-inits it so the engine can be vendored there. --name <name> sets the instance name (default: the dir's basename, or the current folder's when [dir] is omitted). --no-engine scaffolds content only; --no-install skips the dependency install. Refuses to clobber an existing instance. |
polaris dev |
Start the API server and the Vite dev server together, with unified teardown (Ctrl-C or either process exiting tears both down). Resolves the instance from your current directory: run from inside an instance folder — or from its parent, where it finds the one instance beside you — and it starts THAT instance via its own vendored engine, even when invoked as a global polaris. Ambiguous parents (more than one instance) refuse and ask you to cd in. --port <n> sets the API port (default: 5501). |
polaris migrate |
Apply any pending content migrations, then exit. Thin wrapper over server/scripts/migrate.mjs. Also cwd-resolved — hands off to the ambient instance's vendored engine. |
polaris status |
Print a read-only report: instance name, engine vs. data schema version, the compatibility verdict (a behind state blocks the server), and whether a newer engine is available upstream. Also cwd-resolved. |
Polaris deliberately separates the code you pull from the content you write, so an engine update can never overwrite your docs.
- Engine root — this repo: the app (
web/), the write API and libs (server/), the versionedmigrations/, and the engine-owned system pages (db/system/). When vendored as a submodule, this is the submodule prefix. You update it by pulling; you never hand-edit it in an instance. - Content root — your
db/pages,db/backlog,db/uploads,db/progress, and the.polaris-versionstamp. This is your content; an update never touches it.
polaris.config.json binds the two via its contentDir field. It lives at the
instance root — an ancestor of the engine in the vendored layout — and the
engine finds it by walking up from its own directory (see
server/lib/instance-config.mjs). Content lives outside the engine's
submodule prefix precisely so git submodule update / subtree pull can
replace the engine wholesale without clobbering anything you wrote.
In a flat single-repo checkout (like this one during engine development) the
config sits at the repo root and contentDir is ./db, so both roots collapse
to the repo root — the same code path, no special case.
Polaris is vendored per instance as a nested submodule. Each instance lives in its own folder (e.g. one per team in a monorepo); the engine is mounted one level down, so the config and content sit inside the instance folder but outside the submodule:
monorepo/
└── team-a/
└── polaris/ ← instance folder (findInstanceRoot lands here)
├── engine/ ← submodule → the Polaris engine (bin, server, web, migrations, db/system)
├── db/ ← THIS instance's content (committed to the monorepo)
│ ├── pages/ backlog/ uploads/ progress/ + .polaris-version
│ └── .polaris-update-cache.json (gitignore this — regenerated per install)
└── polaris.config.json ← { name, contentDir: "./db" }
The submodule boundary at polaris/engine/ is what makes the split work: an
engine update (git submodule update --remote) can replace engine/ wholesale
without touching db/ or polaris.config.json, and the host monorepo can commit
those (git won't track files inside a submodule dir). contentDir stays
"./db" because it resolves relative to the config file's own location.
polaris init team-a/polaris scaffolds the instance and runs the submodule
add for you:
git submodule add -b main <this-repo-url> team-a/polaris/engine
After the add, init runs npm install in team-a/polaris/engine/web so the
instance is immediately runnable (pass --no-install to skip it). Pass
--engine-url <url> to override the URL (default: the running engine's origin),
or --no-engine to scaffold content only. When the target isn't inside
a git repo, init git-inits it first so the submodule has a host repo to record
the mapping in. If the add still fails (unknown URL, an add conflict), init
writes the content and prints the exact command to run yourself — it never
leaves you half-scaffolded.
Later, pull a newer engine and bring content forward:
git submodule update --remote team-a/polaris/engine
(cd team-a/polaris/engine && ../bin/polaris.mjs migrate) # or via a linked `polaris`
Polaris nudges you when you're behind: a best-effort check compares your running
commit against upstream main and, when you're behind, prints a one-line hint
at startup and shows a dismissible in-app banner with the exact command to run.
It's a soft courtesy — never blocks startup, degrades silently offline or from a
non-git copy, and is throttled to one network check a day. Turn it off or
customize the command it suggests via the updateCheck / updateCommand config
fields below.
Six fields (defaults from DEFAULT_CONFIG in server/lib/instance-config.mjs);
polaris init writes only the first two and lets the rest default:
| Field | Default | Purpose |
|---|---|---|
name |
"Polaris" |
Sidebar wordmark / browser tab title. |
contentDir |
"./db" |
Where this instance's content lives, relative to the config file. |
updateCheck |
true |
Master on/off for the "newer engine upstream" nudge. false disables the fetch, the startup log, and the in-app banner entirely. |
updateCommand |
"git submodule update --remote" |
The exact command the nudge tells you to run. |
allowedHosts |
[] |
Extra Host headers the Vite dev server accepts (server.allowedHosts). Vite blocks unrecognized hosts (anti-DNS-rebinding); localhost is always allowed. Add names reached via a proxy/tunnel — e.g. ["box"] to hit an instance on a GCP dev box over its SOCKS alias. Keeps the engine unedited per-instance. |
webPort |
5173 |
Port for the Vite dev server (web/). Distinct from the API port (polaris dev --port, default 5501). Set it to run two co-located instances without a port collision (e.g. 5179) from config rather than an engine edit. |
bin/— thepolarisCLI (polaris.mjs), the single entrypoint.server/— the zero-dependency Node write API (api-server.mjs) and itslib/(page store, config seam, versioning, update check, …) plus agent-facing scripts underscripts/(migrate,backlog,progress).web/— the React app (Vite, plain JS/JSX, no TypeScript). Itsnode_modulesis the only dependency install in the project.db/system/— engine-owned system pages (the component catalog, the changelog, guidelines). They ship with the engine and update when you pull; they are served read-only alongside your pages but never merged into your content.migrations/— versioned content transforms fordb/pages/**/*.json, applied bypolaris migrate. Seemigrations/README.mdfor the contract migration authors follow.
Content directories (db/pages, db/backlog, db/uploads, db/progress) are
per-instance and gitignored in the engine repo — each install generates its own.
Pulling a newer engine can bump the schema version the code understands. The
version stamp on your content (.polaris-version) must match, or the server
refuses to boot — a loud guard rather than silently serving mismatched data
(see server/lib/engine-version.mjs):
- Data behind the engine → pending migrations exist. Run
polaris migrateto bring your content forward, then start the server. - Data ahead of the engine → you're running an older engine against already-migrated content (e.g. after a downgrade). Update the engine.
polaris status reports exactly where you stand before you start the server.