Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@velocms/plugin-sdk

TypeScript SDK for VeloCMS plugin development.

Installation

npm install @velocms/plugin-sdk --save-dev

Quick Start

import 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}`,
    }),
  });
}

Phase 2.A: Event Bus

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.

Declare the capability

export const manifest: PluginManifest = {
  // ...
  capabilities: {
    events: {
      subscribe: ["post.published", "member.subscribed"],
      emit_custom: true,  // only if you call ctx.events.emit()
    },
  },
};

Subscribe to system events

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);
  });
}

Emit a custom event

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));
});

Full example: Slack Notifier

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

System event catalog

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 }

Delivery semantics

  • 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_events for 30 days. Use this for debugging via the PocketBase admin panel.

Documentation

Full SDK reference: velocms.org/developers/sdk

Publishing to the Marketplace

  1. Build your plugin: npm run build
  2. Upload to velocms.org/developers/submit
  3. The automated review pipeline will scan your bundle
  4. Manual review by the VeloCMS team (2-5 business days)
  5. Published to the marketplace

License

MIT

About

TypeScript SDK for building VeloCMS plugins — typed hook contexts, capability-gated APIs, and test helpers

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages