Skip to main content
Blocks are how features travel between Mercur projects: not as npm packages you depend on, but as source code copied into the target project. Anything you’ve built once — a module, workflows, routes, panel pages — can be packaged as a block, published through a registry, and installed with mercurjs add. This tutorial builds a minimal “announcements” block and ships it through your own registry.
Blocks are source, not dependencies. When someone installs your block, they get the files — editable, diffable, theirs. Updates are opt-in via mercurjs diff and add --overwrite, never forced through a lockfile. That’s the trade: you give up automatic upgrades, users gain full ownership. Design blocks so they’re readable after install.

What you’ll build

An announcements block containing a module (data model + service), a vendor API route, and a vendor portal page — built into registry JSON and installed into a Mercur project.

File types and where they land

Each file in a block carries a type that maps to an alias in the consumer’s blocks.json:
TypeLands at (default aliases)
registry:modulethe API package’s modules directory
registry:workflowthe API package’s workflows directory
registry:apipackages/api/src
registry:linkthe API package’s links directory
registry:vendorapps/vendor/src
registry:adminapps/admin/src
registry:libshared lib directory

Author and ship the block

1

Lay out the block source

A registry is a project with a registry.json and block sources under src/. Each block follows the standard directory convention, one folder per concern:
my-registry/
├── registry.json
└── src/
    └── announcements/
        ├── modules/announcements/     # data model, service, index
        ├── api/vendor/announcements/  # route.ts, validators.ts
        └── vendor/routes/announcements/
            └── page.tsx               # vendor portal page
Write the files exactly as they should land in a consumer’s project — real imports, real Medusa module definitions. The build step resolves imports and rewrites them to the consumer’s path aliases at install time. For the panel page, use the same conventions as any custom panel page: a default export plus a config for the sidebar entry.
2

Declare it in registry.json

registry.json
{
  "$schema": "https://registry.mercurjs.com/registry.json",
  "name": "@my-org",
  "homepage": "https://my-org.com",
  "items": [
    {
      "name": "announcements",
      "description": "Marketplace announcements with a vendor portal feed.",
      "dependencies": [],
      "registryDependencies": [],
      "docs": "## Setup\n\nRegister the module in `medusa-config.ts`, then run `bunx medusa db:generate announcements && bunx medusa db:migrate`, and finally `bunx @mercurjs/cli@rc codegen`.",
      "categories": ["module", "api", "vendor"],
      "files": [
        { "path": "announcements/modules/announcements/index.ts", "type": "registry:module" },
        { "path": "announcements/api/vendor/announcements/route.ts", "type": "registry:api" },
        { "path": "announcements/vendor/routes/announcements/page.tsx", "type": "registry:vendor" }
      ]
    }
  ]
}
Two fields do the heavy lifting: type on each file decides where it lands, and docs is the markdown shown after install. Put every manual step in docs — module registration, migrations, middleware, codegen. It’s the only instruction the installer sees.
3

Build the registry

bunx @mercurjs/cli@rc build
This reads registry.json, resolves each block’s imports, embeds file contents, and writes one JSON per block into r/r/announcements.json, plus an index r/registry.json.
4

Host it

Serve the r/ directory from any static host (GitHub Pages, Vercel, S3 — anything that makes {name}.json publicly reachable).
5

Install it into a project

In a consumer project, register your registry in blocks.json and install:
blocks.json
{
  "registries": {
    "@my-org": "https://my-registry.example.com/r/{name}.json"
  }
}
bunx @mercurjs/cli@rc add @my-org/announcements
The CLI fetches the JSON, maps each file’s type to the consumer’s aliases, rewrites imports, and prints your docs instructions.

Verify

  1. r/announcements.json exists after the build and embeds every file’s content.
  2. In the consumer project, the files landed under the alias-mapped paths and imports resolve.
  3. After following your own docs steps (module registration, migrations, codegen), bun run build passes and the vendor portal shows the Announcements page.
  4. bunx @mercurjs/cli@rc diff @my-org/announcements reports no changes — the installed copy matches the registry.

FAQ

Other blocks go in registryDependencies (e.g. @my-org/reviews) — the CLI installs them in order automatically. NPM packages go in dependencies; the build also auto-detects them from your imports, so you rarely list transitive ones by hand.
Yes — use the object form with headers in the consumer’s blocks.json: { "url": "…/{name}.json", "headers": { "Authorization": "Bearer ${REGISTRY_TOKEN}" } }. The env var is resolved from the installer’s environment. See Registry.
They run mercurjs diff <block> to compare their local copy against your registry, then add --overwrite to take the new version. Because blocks are source, consumers with local edits merge deliberately rather than being force-upgraded.

Next steps

Registry

The full registry.json schema, auth, and block dependencies.

Blocks

What blocks can contain and how consumers manage them.