Developer documentation
Build a Portfoleez module
Modules extend the builder. A module can register new blocks, inspector panels and settings sections. Users install them from the marketplace, and installed blocks appear in the editor's block library instantly.
Overview
A module is a plain JavaScript/TypeScript object with a manifest and a setup function. On install, Portfoleez calls setup(host) and hands you a host API. Anything you register is removed again automatically when the user uninstalls the module.
- No build step is required — ship a single ES module.
- React 19 and the Portfoleez block types are provided by the host.
- Modules never receive user credentials; data access happens through the host app.
Module anatomy
The smallest valid module looks like this:
import type { PortfoleezModule } from "@portfoleez/sdk";
export default {
manifest: {
slug: "viewer-counter", // unique, lowercase, url-safe
name: "Viewer counter",
version: "1.0.0", // semver
author: "your-username",
},
setup: (host) => {
host.registerBlock(viewerCounterBlock);
// Optional: return a teardown function
return () => console.log("module removed");
},
} satisfies PortfoleezModule;The host API exposes registerBlock, registerEditorPanel and registerSettingsSection.
Starter template
Copy this template into a new folder and you have a publishable module. It contains everything the uploader validates: a manifest, a block, inspector fields and a teardown.
// index.tsx — the module entry point
import type { PortfoleezModule, BlockDefinition } from "@portfoleez/sdk";
const helloBlock: BlockDefinition = {
type: "hello-banner",
label: "Hello banner",
description: "A friendly greeting banner.",
category: "content",
icon: "Sparkles",
defaultProps: { title: "Hello", subtitle: "Built with a Portfoleez module" },
fields: [
{ key: "title", label: "Title", type: "text" },
{ key: "subtitle", label: "Subtitle", type: "textarea" },
{ key: "image", label: "Background", type: "image" },
],
render: (props, ctx) => (
<section style={{ padding: 32, textAlign: "center" }}>
<h2>{String(props.title ?? "")}</h2>
<p>{String(props.subtitle ?? "")}</p>
{ctx.editing ? <small>editor preview</small> : null}
</section>
),
};
const module: PortfoleezModule = {
manifest: {
slug: "hello-banner",
name: "Hello banner",
version: "1.0.0",
author: "your-username",
},
setup: (host) => {
host.registerBlock(helloBlock);
return () => {
/* cleanup timers, listeners, subscriptions */
};
},
};
export default module;Add a module.json next to it so the uploader can read your metadata without executing any code:
{
"slug": "hello-banner",
"name": "Hello banner",
"version": "1.0.0",
"author": "your-username",
"description": "Adds a friendly greeting banner block.",
"categories": ["Blocks", "Content"],
"tags": ["banner", "hero"],
"entry": "dist/index.js",
"minAppVersion": "1.0.0",
"screenshots": ["screenshots/banner.png"]
}Folder & file requirements
The uploader expects this layout. Anything else is optional.
hello-banner/
├── module.json # required — manifest metadata (slug must match code)
├── src/
│ └── index.tsx # required — default-exports the module object
├── dist/
│ └── index.js # required — bundled ES module referenced by "entry"
├── screenshots/ # optional — PNG/JPG, max 2 MB each
│ └── banner.png
├── README.md # optional — shown on the module details page
└── CHANGELOG.md # optional — per-version notes| File | Rule |
|---|---|
| module.json | Required. slug is lowercase, url-safe and must equal manifest.slug in code. |
| dist/index.js | Required. A single bundled ES module with a default export; no bare imports at runtime. |
| version | Required semver. Each upload must use a version higher than the last published one. |
| entry | Path to the bundled file, relative to the module root. |
| screenshots/* | Optional images, 16:10 recommended, max 2 MB each. |
| README.md | Optional markdown shown as the long description in the marketplace. |
Zip the folder (or upload dist/index.js plus module.json) in the developer area. Uploads that miss a required file, or whose slug/version conflict with an existing release, are rejected before review.
Defining blocks
A block definition describes how it appears in the library, its default content, the fields shown in the inspector, and how it renders.
const viewerCounterBlock = {
type: "viewer-counter", // unique across the install
label: "Viewer counter",
description: "Shows how often this page was viewed.",
category: "advanced", // layout | navigation | content | media | social | advanced
icon: "Eye", // any lucide-react icon name
styleOverrides: { paddingY: 16 },
defaultProps: { label: "views", showIcon: true },
fields: [
{ key: "label", label: "Label", type: "text" },
{ key: "showIcon", label: "Show icon", type: "switch" },
],
render: (props, ctx) => (
<span>{props.label} · {ctx.editing ? "preview" : "live"}</span>
),
};render receives the block props and a context object with device (desktop, tablet, mobile) and editing. Use editing to show sample data inside the editor instead of performing network writes — the built-in Viewer counter module does exactly that.
Inspector fields
Field types available to your blocks:
| Type | Renders |
|---|---|
| text / textarea / richtext | Single-line, multi-line and formatted text inputs |
| url | Link input |
| image | Upload button plus the user's media library picker |
| number | Numeric input |
| select | Dropdown, driven by an options array |
| switch | Boolean toggle |
| color | Color picker bound to theme tokens |
| code | Monospaced editor |
| list | Repeatable group defined by itemFields |
Image fields automatically get the shared media library, so uploads made anywhere in the app are reusable in your block.
Panels & settings sections
host.registerEditorPanel({
id: "seo-helper",
label: "SEO helper",
component: ({ blockId }) => <MyPanel blockId={blockId} />,
});
host.registerSettingsSection({
id: "my-integration",
label: "My integration",
component: MySettings,
});Panels appear next to the block inspector; settings sections appear in portfolio settings.
Publishing & uploading a module
- Apply for the developer role in the developer area. A moderator reviews every application; you can author and submit modules once it is approved.
- Choose New module and fill in name, slug, description, categories and tags.
- Upload your module package: either a
.zipof the module folder (containingmodule.jsonanddist/index.js) or the single bundled.jsfile. Packages are limited to 25 MB and are required for every release. Add screenshots through the same media library used everywhere else in the app; a public source URL is optional but recommended. - Set a semver version and a short changelog, then submit. Each upload creates a new immutable module version; the latest published version is what users install.
- Test locally first: install your module from the marketplace listing preview and confirm the block appears in the editor library, renders in preview, and still renders on a published page.
Manifest slug and the module record's slug must match, otherwise the upload is rejected.
Review, versioning & safety
- Follow semver: breaking prop changes require a major bump.
- Never remove a block
typein a minor release — existing pages reference it. - Modules run in the user's browser. Do not request credentials, inject third-party trackers, or fetch remote code at runtime; such modules are removed from the marketplace.
- Users are warned that third-party modules are unreviewed code, so keep your source auditable.
Ready to ship?
Head to the developer area to create your first module, or browse the marketplace for reference implementations like the Viewer counter.