Plugin SDK Reference
Build TypeScript plugins for VeloCMS. Earn 80% of every sale. Plugins run in a V8 isolate sandbox — no Node.js access, no filesystem, no process.env. All host interactions go through the typed SDK.
Getting Started#
Install the SDK as a dev dependency. It ships pure TypeScript types — no runtime bundle is needed because plugin code is executed inside a VeloCMS V8 sandbox.
npm install --save-dev @velocms/plugin-sdkA minimal plugin exports a manifest and an init() function:
import type { PluginManifest, HookContext, AfterPostPublishPayload } from "@velocms/plugin-sdk";
export const manifest: PluginManifest = {
$schema: "https://velocms.org/schemas/plugin-v2.json",
name: "@myorg/word-counter",
displayName: "Word Counter Pro",
version: "1.0.0",
description: "Adds word count to every published post.",
author: { name: "My Org", email: "[email protected]" },
type: "integration",
category: "productivity",
engines: { velocms: ">=1.0.0" },
capabilities: {
content: { read: true },
hooks: ["post:afterPublish"],
},
pricing: { model: "free" },
};
export async function init(ctx: HookContext) {
ctx.hooks.on("post:afterPublish", async (payload: AfterPostPublishPayload) => {
const words = payload.post.content_html
?.replace(/<[^>]+>/g, " ")
.split(/\s+/)
.filter(Boolean).length ?? 0;
ctx.logger.info(`Post published: ${payload.post.title} (${words} words)`);
});
}Capability Manifest#
Every capability your plugin requires must be declared in the manifest. VeloCMS shows these permissions to the tenant before install, and enforces them at runtime in the sandbox. Undeclared capabilities throw a CapabilityError.
| Capability | Description | Risk |
|---|---|---|
| posts:read | Read posts and drafts | Low |
| posts:write | Create and update posts | Medium |
| pages:read | Read page builder pages | Low |
| pages:write | Create and update pages | Medium |
| media:read | List and get media files | Low |
| media:write | Upload media files | Medium |
| members:read | Read subscriber emails + tiers | High |
| orders:read | Read commerce order history | High |
| products:read | Read product catalog | Low |
| products:write | Create and update products | Medium |
| network | Outbound HTTP (allowlist required) | Medium |
| settings | Encrypted key-value storage | Low |
// Manifest with multiple capabilities
capabilities: {
content: { read: true, write: true }, // posts:read + posts:write
network: {
allowlist: ["api.sendgrid.com"], // only calls to this domain
methods: ["POST"], // only POST allowed
},
settings: true, // encrypted key-value store
hooks: ["post:afterPublish", "member:afterSignup"],
},Hook Registry#
Hooks are the primary extension point. A hook fires at a specific point in the VeloCMS lifecycle. Your handler receives a typed payload and the full Context API.
| Hook name | When it fires |
|---|---|
| post:afterCreate | After a new post is created (draft) |
| post:afterPublish | After a post goes public |
| post:beforeDelete | Before a post is permanently deleted |
| member:afterSignup | After a reader subscribes |
| member:afterTierChange | After subscriber upgrades to paid |
| member:afterUnsubscribe | After reader cancels subscription |
| order:afterPaid | After a product purchase succeeds |
| order:afterRefund | After a refund is issued |
| page:afterPublish | After a page builder page goes public |
| editor:seoScorer | Augments the SEO score with custom rules |
ctx.hooks.on("member:afterSignup", async (payload) => {
const { member } = payload;
// payload is fully typed — member.email, member.tier, etc.
await ctx.network.fetch("https://api.sendgrid.com/v3/contacts", {
method: "POST",
body: JSON.stringify({ email: member.email }),
});
ctx.logger.info("Subscriber synced", { email: member.email });
});Context API#
The ctx object passed to init(ctx)and hook handlers exposes everything your plugin can do. All APIs are scoped to the current tenant — your plugin cannot access another tenant's data.
ctx.contentRead/write posts, pages, media. Each method is gated by capability.
ctx.content.posts.list({ status: 'published' })ctx.networkOutbound HTTP. Every URL is validated against your manifest allowlist.
ctx.network.fetch('https://api.example.com/data')ctx.settingsEncrypted key-value store scoped to your plugin + tenant.
await ctx.settings.set('webhook_url', url)ctx.loggerStructured logging. Output visible in Admin → Plugins → Logs.
ctx.logger.info('Done', { count: 3 })Testing#
Import mockVelocms from @velocms/plugin-sdk/testing to unit-test your plugin with in-memory stores and captured log calls — no PocketBase or Stripe required.
import { describe, it, expect } from "vitest";
import { mockVelocms } from "@velocms/plugin-sdk/testing";
import { init } from "./index";
describe("word-counter", () => {
it("logs word count on post:afterPublish", async () => {
const vm = mockVelocms({
content: {
posts: [{ id: "p1", title: "Hello", content_html: "<p>Hello world</p>" }],
},
});
await init(vm);
await vm.hooks.fire("post:afterPublish", { post: vm.content.posts._store[0] });
expect(vm.logger.calls[0][0]).toBe("info");
expect(vm.logger.calls[0][1]).toContain("2 words");
});
});Publishing to the Marketplace#
VeloCMS collects a 20% platform fee. You keep 80%. Earnings are paid out weekly via Stripe Connect.
- 1
Connect Stripe
Go to Developer Portal → Stripe Connect and complete the Express onboarding. This sets up your payout account.
Connect Stripe → - 2
Build and bundle
Your plugin must export a single ESM bundle. Run: npx tsup src/index.ts --format esm
- 3
Submit for review
Go to Developer Portal → Submit Plugin. Upload the bundle. The review pipeline runs 4 automated stages then routes to a VeloCMS maintainer.
Submit Plugin → - 4
Set your price
Free plugins are available instantly post-approval. Paid plugins require a Stripe Price ID — create one in your Stripe Connect dashboard.
- 5
Live!
Approved plugins appear in the VeloCMS Plugin Marketplace within 24h of final review.