Your First Plugin
This walkthrough builds a real Vencord plugin from scratch — Quick Notes, a per-channel note-taking popover. By the end, you'll have a working plugin published to a venpm index that anyone can install.
Prerequisites
Before starting, make sure you have:
- venpm installed globally (
npm install -g @kamaras/venpm) - Vencord source checkout (we'll use
~/src/Vencord) - Discord running with Vencord injected
- Node.js 18+ and pnpm installed
Run venpm doctor to verify everything is set up.
Step 1: Scaffold a Plugin Repo
Create a new plugin repository:
venpm create ~/src/my-pluginsThis creates:
my-plugins/
plugins.json # venpm plugin index (empty)
plugins/ # your plugins go here
README.md
.github/workflows/ # CI validationEnter the directory:
cd ~/src/my-pluginsStep 2: Scaffold the Plugin
venpm create plugins/quickNotes --tsx --cssThe --tsx flag creates an .tsx entry point (for React UI) and --css adds a style.css. You now have:
plugins/quickNotes/
index.tsx # Plugin entry point
style.css # Plugin stylesStep 3: The Basics
Open plugins/quickNotes/index.tsx. The scaffold gives you a minimal plugin. Replace its contents with:
import definePlugin from "@utils/types";
export default definePlugin({
name: "QuickNotes",
description: "Per-channel note-taking popover",
authors: [{ name: "YourName", id: 0n }],
start() {
console.log("QuickNotes started!");
},
stop() {
console.log("QuickNotes stopped!");
},
});Author ID
Replace 0n with your Discord user ID as a BigInt literal (e.g., 123456789012345678n). You can find your ID by enabling Developer Mode in Discord settings, then right-clicking your profile.
Install and Test
Register the plugin locally:
venpm install quickNotes --local plugins/quickNotes --no-buildBuild and deploy:
venpm rebuildAfter Discord restarts, open Vencord settings. You should see QuickNotes in the plugin list. Enable it and check the DevTools console (Ctrl+Shift+I) — you should see "QuickNotes started!".
Step 4: Settings
Add user-configurable settings. Update index.tsx:
import { definePluginSettings } from "@api/Settings";
import definePlugin, { OptionType } from "@utils/types";
const settings = definePluginSettings({
defaultText: {
type: OptionType.STRING,
description: "Default text for new notes",
default: "",
},
showTimestamp: {
type: OptionType.BOOLEAN,
description: "Show last-edited timestamp on notes",
default: true,
},
});
export default definePlugin({
name: "QuickNotes",
description: "Per-channel note-taking popover",
authors: [{ name: "YourName", id: 0n }],
settings,
start() {},
stop() {},
});Rebuild with venpm rebuild. In Vencord settings, click the gear icon next to QuickNotes — you'll see your two settings.
Step 5: React UI
Now build the note-taking popover. Add a NotesPopover component to index.tsx:
import { definePluginSettings } from "@api/Settings";
import definePlugin, { OptionType } from "@utils/types";
import { React, useState, useEffect } from "@webpack/common";
const settings = definePluginSettings({
defaultText: {
type: OptionType.STRING,
description: "Default text for new notes",
default: "",
},
showTimestamp: {
type: OptionType.BOOLEAN,
description: "Show last-edited timestamp on notes",
default: true,
},
});
// Store notes per channel in localStorage
function getNote(channelId: string): string {
return localStorage.getItem(`quickNotes-${channelId}`) ?? settings.store.defaultText;
}
function setNote(channelId: string, text: string) {
localStorage.setItem(`quickNotes-${channelId}`, text);
localStorage.setItem(`quickNotes-${channelId}-ts`, new Date().toISOString());
}
function getNoteTimestamp(channelId: string): string | null {
return localStorage.getItem(`quickNotes-${channelId}-ts`);
}
function NotesPopover({ channelId }: { channelId: string; }) {
const [text, setText] = useState(() => getNote(channelId));
const [timestamp, setTimestamp] = useState(() => getNoteTimestamp(channelId));
useEffect(() => {
const timer = setTimeout(() => {
setNote(channelId, text);
setTimestamp(new Date().toISOString());
}, 500); // auto-save after 500ms of inactivity
return () => clearTimeout(timer);
}, [text, channelId]);
return (
<div className="vc-quickNotes-popover">
<textarea
className="vc-quickNotes-textarea"
value={text}
onChange={e => setText(e.target.value)}
placeholder="Write a note for this channel..."
rows={6}
/>
{settings.store.showTimestamp && timestamp && (
<div className="vc-quickNotes-timestamp">
Last edited: {new Date(timestamp).toLocaleString()}
</div>
)}
</div>
);
}
export default definePlugin({
name: "QuickNotes",
description: "Per-channel note-taking popover",
authors: [{ name: "YourName", id: 0n }],
settings,
start() {},
stop() {},
});The component isn't injected into Discord yet — that comes in the next step with patches.
Step 6: Patches
Patches let you modify Discord's bundled code before it loads. We'll inject a "Notes" button into the channel header toolbar.
Patches are fragile
Discord's internal module code changes frequently. The find string and match pattern below target a specific build. If a Discord update changes the module, the patch will silently fail. This is normal — you'll need to update the patch when it breaks.
Add the patches array and a button component to your plugin:
// Add to imports at the top:
import { findByPropsLazy } from "@webpack";
import { Tooltip } from "@webpack/common";
// Webpack module for channel header classes
const headerClasses = findByPropsLazy("title", "iconWrapper");
// Notes button component for the toolbar
function NotesButton({ channelId }: { channelId: string; }) {
const [open, setOpen] = useState(false);
return (
<Tooltip text="Quick Notes">
{(tooltipProps: any) => (
<div style={{ position: "relative" }}>
<button
{...tooltipProps}
className={`vc-quickNotes-button ${headerClasses.iconWrapper}`}
onClick={() => setOpen(!open)}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10H7v-2h10v2zm0-4H7V7h10v2z"/>
</svg>
</button>
{open && <NotesPopover channelId={channelId} />}
</div>
)}
</Tooltip>
);
}Then add the patch to your definePlugin:
export default definePlugin({
name: "QuickNotes",
description: "Per-channel note-taking popover",
authors: [{ name: "YourName", id: 0n }],
settings,
patches: [
{
find: ".toolbar,children:",
replacement: {
match: /(?<=\.toolbar,children:)(\[)/,
replace: "$1,$self.NotesButton(arguments[0]),",
},
},
],
NotesButton(props: { channel: { id: string; }; }) {
return <NotesButton channelId={props.channel.id} />;
},
start() {},
stop() {},
});Rebuild with venpm rebuild. You should see a notes icon in the channel header toolbar. Click it to open the popover.
Step 7: Styling
Open style.css and add styles using Discord's CSS variables:
.vc-quickNotes-button {
cursor: pointer;
background: none;
border: none;
color: var(--interactive-normal);
display: flex;
align-items: center;
padding: 4px;
border-radius: 4px;
}
.vc-quickNotes-button:hover {
color: var(--interactive-hover);
background: var(--background-modifier-hover);
}
.vc-quickNotes-popover {
position: absolute;
top: 100%;
right: 0;
z-index: 1000;
width: 300px;
padding: 12px;
margin-top: 8px;
background: var(--background-floating, #2b2d31);
border-radius: 8px;
box-shadow: var(--elevation-high);
}
.vc-quickNotes-textarea {
width: 100%;
min-height: 100px;
padding: 8px;
background: var(--input-background, #1e1f22);
border: none;
border-radius: 4px;
color: var(--text-normal);
font-family: inherit;
font-size: 14px;
resize: vertical;
}
.vc-quickNotes-textarea:focus {
outline: 2px solid var(--brand-500);
}
.vc-quickNotes-textarea::placeholder {
color: var(--text-muted);
}
.vc-quickNotes-timestamp {
margin-top: 8px;
font-size: 12px;
color: var(--text-muted);
}Import the stylesheet in your plugin by adding to index.tsx:
import "./style.css";CSS Variable Fallbacks
Always provide hardcoded fallbacks for CSS variables (e.g., var(--background-floating, #2b2d31)). Discord's CSS variables aren't available in every injection context.
Rebuild with venpm rebuild to see the styled popover.
Step 8: Dev Workflow
For faster iteration, use Vencord's watch mode instead of rebuilding each time:
cd ~/src/Vencord && pnpm build --watchChanges to your plugin source are picked up automatically. You may still need to restart Discord for patch changes.
Step 9: Publish
Write the Plugin Index
Open plugins.json in your repo root and add your plugin:
{
"$schema": "https://venpm.dev/schemas/v1/plugins.json",
"name": "my-plugins",
"description": "My custom Vencord plugins",
"plugins": {
"quickNotes": {
"version": "0.1.0",
"description": "Per-channel note-taking popover",
"authors": [{ "name": "YourName", "id": "123456789012345678" }],
"license": "MIT",
"source": {
"git": "https://github.com/you/my-plugins.git",
"path": "plugins/quickNotes"
}
}
}
}Author ID format
In definePlugin, the author ID is a BigInt (0n). In plugins.json, it's a string ("0"). These are different formats — the index schema requires strings.
Validate
venpm validate plugins.jsonFor thorough checks including dependency cross-references:
venpm validate plugins.json --strictHost It
The simplest approach is GitHub Releases. Push your repo to GitHub, and venpm's scaffolded CI workflow (.github/workflows/) validates your index on every push and publishes it as a release asset.
Users install from the stable release URL:
https://github.com/you/my-plugins/releases/latest/download/plugins.jsonStep 10: Install It
Another user can now install your plugin:
venpm repo add https://github.com/you/my-plugins/releases/latest/download/plugins.json --name your-plugins
venpm install quickNotesThat's it — the full cycle from scaffolding to published plugin.
What's Next
- Plugin Index Format — all fields, dependencies, version history
- Scaffolding —
venpm createoptions in detail - CI & Publishing — GitHub Actions workflow, validation, release automation
- CLI Reference — all 12 commands documented