@velocms/plugin-sdk v1.0.0

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-sdk
bash

A 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)`);
  });
}
typescript

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.

CapabilityDescriptionRisk
posts:readRead posts and draftsLow
posts:writeCreate and update postsMedium
pages:readRead page builder pagesLow
pages:writeCreate and update pagesMedium
media:readList and get media filesLow
media:writeUpload media filesMedium
members:readRead subscriber emails + tiersHigh
orders:readRead commerce order historyHigh
products:readRead product catalogLow
products:writeCreate and update productsMedium
networkOutbound HTTP (allowlist required)Medium
settingsEncrypted key-value storageLow
// 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"],
},
typescript

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 nameWhen it fires
post:afterCreateAfter a new post is created (draft)
post:afterPublishAfter a post goes public
post:beforeDeleteBefore a post is permanently deleted
member:afterSignupAfter a reader subscribes
member:afterTierChangeAfter subscriber upgrades to paid
member:afterUnsubscribeAfter reader cancels subscription
order:afterPaidAfter a product purchase succeeds
order:afterRefundAfter a refund is issued
page:afterPublishAfter a page builder page goes public
editor:seoScorerAugments 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 });
});
typescript

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.content

Read/write posts, pages, media. Each method is gated by capability.

ctx.content.posts.list({ status: 'published' })
ctx.network

Outbound HTTP. Every URL is validated against your manifest allowlist.

ctx.network.fetch('https://api.example.com/data')
ctx.settings

Encrypted key-value store scoped to your plugin + tenant.

await ctx.settings.set('webhook_url', url)
ctx.logger

Structured 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");
  });
});
typescript

Publishing to the Marketplace#

VeloCMS collects a 20% platform fee. You keep 80%. Earnings are paid out weekly via Stripe Connect.

  1. 1

    Connect Stripe

    Go to Developer Portal → Stripe Connect and complete the Express onboarding. This sets up your payout account.

    Connect Stripe →
  2. 2

    Build and bundle

    Your plugin must export a single ESM bundle. Run: npx tsup src/index.ts --format esm

  3. 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. 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. 5

    Live!

    Approved plugins appear in the VeloCMS Plugin Marketplace within 24h of final review.