Skip to main content
primo dev runs the full Primo CMS on your machine and keeps it in sync with your local files. You write blocks and content in your editor; the CMS picks up the changes. With --author cms, edits you make in the CMS are written back to disk instead (see Author Modes).

Starting the Dev Server

From a workspace root (the directory containing server.yaml):
primo dev runs until you stop it (Ctrl+C). It prints the URLs for each site:
  • http://127.0.0.1:3000/admin/dashboard — the dashboard
  • http://my-site.localhost:3000/admin/site — the CMS editor for a site
  • http://my-site.localhost:3000/ — the site’s preview
Run from the folder that contains a workspace, primo dev tells you to cd into it rather than creating a new one.
From scripts or AI agents, run primo dev in the background, log to a file, and stop it by its PID when you’re done:

Hosted server changes

If your workspace has a hosted server configured, primo dev checks its locally tracked sites and shared library against your last pull or push before loading the local CMS. If server data changed or was deleted, you see a warning naming the affected site or library. The check uses your saved login and waits up to five seconds across all requests. Missing sync history, an unreachable server, an expired login, or an older server produces a notice that freshness could not be verified. Local development still starts. Workspaces without a configured server skip the check. This check does not pull data or change your saved sync history. To update an outdated workspace, save your local work, stop primo dev, and pull from the hosted server before editing. A pull replaces local files, so preserve your changes first. primo push checks the server again when you upload.

Ports

primo dev uses two ports: the CMS port (3000 by default, or port: in server.yaml) and the next one up for live reload. If either is taken, it picks the next free pair and tells you:
Local site hosts like my-site.localhost:3000 are updated to the port in use, so you can run several workspaces side by side. If you chose the port yourself (with -p or port: in server.yaml), primo dev asks before switching to another one. Without a terminal to ask in (scripts, CI, agents), it stops with an error instead, so pass a free port with -p.

Leftover CMS processes

If a previous primo dev was killed without shutting down its CMS, the next start tells you which process is in the way:
primo dev --force stops any process on the workspace’s port, including a primo dev running for a different workspace. Prefer kill <pid> when you have more than one workspace running.

Options

Author Modes

--author controls which side wins when files and the CMS disagree. This is the most important concept in local dev — pick a mode that matches how you’re working.
Files are source of truth.
  • Edits to local files are imported into the CMS on save.
  • The CMS UI is read-only for content that’s tracked in files — you’ll see a banner.
  • Use this mode when you’re building blocks or page types in your editor.

Site Layout

Each site folder follows this layout:
See Architecture for the underlying data model.

Uploads

Files dropped into sites/my-site/uploads/ are automatically imported into the CMS as site_uploads records:
  • On import, new files become uploads with stable IDs.
  • Hash comparison skips re-upload when content is unchanged.
  • A file removed from uploads/ becomes an orphan upload warning — clean it up in the CMS or remove the record from the file system to delete it.
This means you can drop an image into uploads/ and reference it from an image field without ever opening the CMS.

site.yaml

site.yaml holds the site’s name, its site_id, and its group:
site_id ties the folder to its record in the CMS. Primo writes it for you, so don’t edit or copy it by hand. The site’s hostname is not stored in files. It’s per-environment (a *.localhost host in dev, your domain in production) and is set in the dashboard, so primo push can’t point your production site at localhost.
Keep backup copies of a site outside sites/ (for example in a _backups/ folder at the workspace root). A copy inside sites/ has the same site_id: primo dev skips it, and primo push refuses to push both.

Adding Sites

primo new <name> creates a site folder under sites/, and primo dev imports it into the CMS. In an interactive terminal, primo new starts the CMS for you; with --skip-dev, or from a script, it only creates the files:
A folder you create by hand (or copy) under sites/ isn’t in the CMS until you register it with primo add:
primo add needs the workspace’s port, so stop primo dev first, then start it again afterward. If primo dev is running, primo add stops and tells you so rather than racing it.
To start a new site from a copy of an existing one, delete the site_id line from the copy’s site.yaml before running primo add. A folder that still has a site_id counts as already registered, so primo add imports it into the original site and overwrites it. Once site_id is gone, primo add also removes the other record ids copied from the original and assigns new ones.
primo status lists any folders on disk that aren’t registered yet:

Shared Library

The workspace’s library/ folder holds blocks shared across sites. library/groups.yaml lists the library’s groups and their folders:
Each block lives in library/<group-folder>/<block>/, with the same files as a site block. primo dev loads the library into the CMS, where editors add library blocks to a site from the block picker. To use a library block on a site in files, copy its folder into sites/<name>/blocks/ (leaving out its _id). The copy is independent afterwards.

Working with AI Agents

primo init, primo new, and primo pull write an AGENTS.md at the workspace root: a guide to the workspace layout, commands, content-design defaults, and valid field types. Most coding agents read it automatically. They also write a CLAUDE.md that imports it (@AGENTS.md) for Claude Code. For validation and scaffolding tools, connect the Primo MCP server. primo dev also registers it in the workspace’s .mcp.json.

Site Groups

Groups organize sites in the dashboard (Home Services, Food & Hospitality, and so on). Declare them in the workspace’s server.yaml, giving each a local id and a display name:
Reference a group from a site’s site.yaml by that local id:
Use the local id in site.yaml.group; the name is only the display label. The CLI resolves the id against server.yaml, then sends the matching name to the server, which assigns its own group id on import and matches groups by name. So the local id is a reference the server replaces, and a site.yaml that names a group by its display name may not select the one you meant — a typo quietly creates a new group instead.Expect the server-assigned id to differ from your local id after the first import. primo dev and primo add print a notice when a configured id looks locally-chosen.

Avatar Uploads

iPhone HEIC photos are automatically converted to JPEG when uploaded as avatars — no manual conversion required.

Validating Your Work

Run primo validate from the workspace root to check every site, or from a site folder to check just that site:
It catches structural mistakes such as a missing homepage (pages/index.yaml), or fields: under a repeater or group field, where nested fields must be listed under subfields:. To rebuild a site’s preview while primo dev is running, pass the site folder:
For AI-assisted development, the MCP server exposes per-file validators (validate_block, validate_page, validate_site) that agents call automatically.

Next Steps

Sync with a Hosted Server

Pull and push changes against a deployed Primo

Deploy

Ship your workspace to production