TypeScript SDK for VeloCMS plugin development.
npm install @velocms/plugin-sdk --save-devimport type { PluginManifest, HookContext, AfterPostCreatePayload } from "@velocms/plugin-sdk";
// 1. Define your manifest
export const manifest: PluginManifest = {
$schema: "https://velocms.org/schemas/plugin-v2.json",
name: "@myorg/my-plugin",
displayName: "My Plugin",
version: "1.0.0",
description: "Sends a Slack notification on every new post.",
author: { name: "My Org", email: "plugins@myorg.com" },
type: "integration",
category: "social",
icon: "./icon.png",
engines: { velocms: ">=1.0.0" },
capabilities: {
content: { read: true },
network: true,
network_allowlist: ["hooks.slack.com"],
},
pricing: { model: "free" },
entry: { runtime: "./dist/runtime.js" },
permissions_displayed_to_user: [
"Read your posts",
"Make HTTP requests to Slack",
],
};
// 2. Export hook handlers
export async function afterPostCreate(
payload: AfterPostCreatePayload,
ctx: HookContext
): Promise<void> {
await ctx.fetch("https://hooks.slack.com/services/YOUR/WEBHOOK/URL", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `New post published: ${payload.post.title}`,
}),
});
}SDK version 1.0.0-alpha.2 adds a real-time event bus. Plugins can subscribe to
VeloCMS system events and emit custom namespaced events.
export const manifest: PluginManifest = {
// ...
capabilities: {
events: {
subscribe: ["post.published", "member.subscribed"],
emit_custom: true, // only if you call ctx.events.emit()
},
},
};Register subscriptions in onAppStart — VeloCMS fires this hook once when
your plugin's runtime loads, which is where ctx.events.on() registrations
belong (registering inside a request-scoped hook like afterPostCreate
would re-subscribe the same handler on every invocation).
import type { OnAppStartPayload, HookContext } from "@velocms/plugin-sdk";
export async function onAppStart(
_payload: OnAppStartPayload,
ctx: HookContext
): Promise<void> {
ctx.events.on("post.published", async ({ post }) => {
await ctx.fetch("https://hooks.slack.com/services/xxx/yyy/zzz", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: `New post: ${post.title}` }),
});
});
ctx.events.on("member.subscribed", async ({ member, source }) => {
await ctx.kv.set("last_signup_source", source);
});
}Custom events must be namespaced to your plugin's org prefix (@org/).
Other plugins can subscribe to your events by name. Emitting requires
capabilities.events.emit_custom: true in your manifest.
// Emitting (from within any hook handler that receives ctx)
await ctx.events.emit("@myorg/slack-notifier:webhook-sent", {
webhookUrl: "https://hooks.slack.com/...",
postTitle: post.title,
});
// Subscribing (from another plugin's onAppStart)
ctx.events.on("@myorg/slack-notifier:webhook-sent", async (payload) => {
await ctx.kv.set("last_slack_event", JSON.stringify(payload));
});A plugin is just a module exporting a manifest plus one function per hook
name it wants to handle — there is no definePlugin() wrapper. VeloCMS
looks up handlers by matching the exported function name to the HookName
union.
import type {
PluginManifest,
OnAppStartPayload,
AfterPostPublishPayload,
HookContext,
} from "@velocms/plugin-sdk";
export const manifest: PluginManifest = {
$schema: "https://velocms.org/schemas/plugin-v2.json",
name: "@myorg/slack-notifier",
displayName: "Slack Notifier",
version: "1.0.0",
description: "Posts to Slack whenever a new post is published.",
author: { name: "My Org", email: "plugins@myorg.com" },
type: "integration",
category: "social",
icon: "./icon.png",
engines: { velocms: ">=1.0.0" },
capabilities: { network: true, network_allowlist: ["hooks.slack.com"] },
pricing: { model: "free" },
entry: { runtime: "./dist/runtime.js" },
permissions_displayed_to_user: ["Make HTTP requests to Slack"],
};
export async function onAppStart(
_payload: OnAppStartPayload,
ctx: HookContext
): Promise<void> {
ctx.log.info("Slack Notifier activated");
}
export async function afterPostPublish(
payload: AfterPostPublishPayload,
ctx: HookContext
): Promise<void> {
const webhookUrl = await ctx.kv.get("slack_webhook_url");
if (!webhookUrl) return;
await ctx.fetch(webhookUrl, {
method: "POST",
body: JSON.stringify({ text: `New post: ${payload.post.title}` }),
});
}| Event | Payload |
|---|---|
post.created |
{ post: HookPost } |
post.updated |
{ post: HookPost; changedFields: string[] } |
post.published |
{ post: HookPost } |
post.unpublished |
{ post: HookPost } |
post.deleted |
{ postId: string; slug: string } |
member.subscribed |
{ member: HookMember; source: string } |
member.unsubscribed |
{ memberId: string } |
member.tier_changed |
{ member: HookMember; previousTier: string } |
comment.posted |
{ commentId: string; postId: string; authorEmail?: string } |
comment.approved |
{ commentId: string; postId: string } |
comment.deleted |
{ commentId: string; postId: string } |
page.published |
{ page: PageHookData } |
media.uploaded |
{ mediaId: string; filename: string; mimeType: string } |
- Best-effort, not guaranteed. If the Railway container restarts mid-flight, events in transit may be missed. Plugin handlers must be idempotent.
- 5-second timeout per handler. A hung handler is killed; the next handler still runs.
- Circuit-breaker. 3 consecutive handler errors → circuit opens for 5 minutes. Events are not dispatched to a circuit-open handler. Reactivating the plugin resets the circuit.
- 30-day audit log. All events are persisted in
plugin_eventsfor 30 days. Use this for debugging via the PocketBase admin panel.
Full SDK reference: velocms.org/developers/sdk
- Build your plugin:
npm run build - Upload to velocms.org/developers/submit
- The automated review pipeline will scan your bundle
- Manual review by the VeloCMS team (2-5 business days)
- Published to the marketplace
MIT