# Vencord Vencord is a Discord client modification that provides a comprehensive plugin system for extending and customizing the Discord desktop and web applications. Built with TypeScript and React, it offers over 100 built-in plugins and a robust API for developers to create custom functionality. The mod works across all Discord branches (Stable, Canary, PTB) and supports browser extensions, making it one of the most versatile Discord modification frameworks available. The core architecture centers around a plugin system with webpack interception, allowing plugins to patch Discord's internal modules, register custom slash commands, add UI elements, and hook into Discord's Flux event system. Vencord prioritizes privacy by blocking Discord analytics and telemetry while maintaining a lightweight footprint despite its extensive feature set. The project includes TypeScript types for Discord's internal APIs through the `@vencord/discord-types` package. ## Plugin Definition API The `definePlugin` function is the primary way to create Vencord plugins. It accepts a plugin definition object with metadata, lifecycle hooks, patches, commands, and UI integration points. ```typescript import definePlugin, { OptionType } from "@utils/types"; import { definePluginSettings } from "@api/Settings"; import { Devs } from "@utils/constants"; const settings = definePluginSettings({ enableFeature: { type: OptionType.BOOLEAN, description: "Enable the main feature", default: true, }, messagePrefix: { type: OptionType.STRING, description: "Prefix to add to messages", default: "[Custom] ", }, volume: { type: OptionType.SLIDER, description: "Volume level", markers: [0, 25, 50, 75, 100], default: 50, } }); export default definePlugin({ name: "MyPlugin", description: "A custom plugin that demonstrates the API", authors: [Devs.Ven], settings, // Lifecycle hooks start() { console.log("Plugin started!"); }, stop() { console.log("Plugin stopped!"); }, // Webpack patches to modify Discord's code patches: [ { find: ".dispatch({type:\"MESSAGE_CREATE\"", replacement: { match: /(\w+)\.dispatch\(\{type:"MESSAGE_CREATE"/, replace: "$self.onMessage($1);$&" } } ], onMessage(dispatcher: any) { if (settings.store.enableFeature) { console.log("Message received!"); } } }); ``` ## Commands API Register custom slash commands that users can invoke in Discord's chat. Commands support options, subcommands, and can send bot messages as responses. ```typescript import { ApplicationCommandInputType, ApplicationCommandOptionType, findOption, sendBotMessage } from "@api/Commands"; import { registerCommand, unregisterCommand } from "@api/Commands"; // Register a command with options registerCommand({ name: "greet", description: "Send a greeting message", inputType: ApplicationCommandInputType.BUILT_IN_TEXT, options: [ { name: "name", description: "Name to greet", type: ApplicationCommandOptionType.STRING, required: true, }, { name: "formal", description: "Use formal greeting", type: ApplicationCommandOptionType.BOOLEAN, required: false, } ], execute: (args, ctx) => { const name = findOption(args, "name", "User"); const formal = findOption(args, "formal", false); const greeting = formal ? `Good day, ${name}. How may I assist you?` : `Hey ${name}! What's up?`; sendBotMessage(ctx.channel.id, { content: greeting, }); } }, "MyPlugin"); // Using commands directly in a plugin definition export default definePlugin({ name: "GreetingPlugin", commands: [{ name: "hello", description: "Say hello", inputType: ApplicationCommandInputType.BUILT_IN_TEXT, execute: (args, ctx) => { sendBotMessage(ctx.channel.id, { content: "Hello, World!" }); } }] }); ``` ## Settings API The Settings API provides reactive state management for plugin configuration with automatic persistence and React hooks for UI updates. ```typescript import { definePluginSettings, useSettings, Settings, migratePluginSettings } from "@api/Settings"; import { OptionType } from "@utils/types"; // Define typed settings with validation const settings = definePluginSettings({ apiEndpoint: { type: OptionType.STRING, description: "API endpoint URL", default: "https://api.example.com", restartNeeded: true, }, maxRetries: { type: OptionType.NUMBER, description: "Maximum retry attempts", default: 3, }, theme: { type: OptionType.SELECT, description: "Color theme", options: [ { label: "Light", value: "light", default: true }, { label: "Dark", value: "dark" }, { label: "System", value: "system" } ] } }); // Migrate settings from old plugin names migratePluginSettings("NewPluginName", "OldPluginName", "AnotherOldName"); // Access settings in code function doSomething() { const endpoint = settings.store.apiEndpoint; const retries = settings.store.maxRetries; console.log(`Using ${endpoint} with ${retries} retries`); } // React hook for components (triggers re-render on change) function SettingsPanel() { const pluginSettings = settings.use(["theme", "maxRetries"]); return (

Current theme: {pluginSettings.theme}

); } // Access global Vencord settings const globalSettings = useSettings(["notifications.*", "plugins.*"]); console.log("Notification timeout:", globalSettings.notifications.timeout); ``` ## DataStore API A wrapper around IndexedDB for persistent storage of large or complex data. Supports all structured-cloneable types including Blobs, Maps, Sets, and ArrayBuffers. ```typescript import { DataStore } from "@api/index"; // Store and retrieve simple values await DataStore.set("myPlugin_userPrefs", { theme: "dark", fontSize: 14 }); const prefs = await DataStore.get<{ theme: string; fontSize: number }>("myPlugin_userPrefs"); console.log(prefs); // { theme: "dark", fontSize: 14 } // Store complex data types const cache = new Map(); cache.set("user123", { name: "Alice", lastSeen: new Date() }); await DataStore.set("myPlugin_cache", cache); // Batch operations (atomic) await DataStore.setMany([ ["myPlugin_setting1", "value1"], ["myPlugin_setting2", { nested: true }], ["myPlugin_setting3", [1, 2, 3]] ]); // Retrieve multiple values const [s1, s2, s3] = await DataStore.getMany([ "myPlugin_setting1", "myPlugin_setting2", "myPlugin_setting3" ]); // Update existing value atomically await DataStore.update("myPlugin_counter", (old = 0) => old + 1); // Delete data await DataStore.del("myPlugin_oldData"); await DataStore.delMany(["myPlugin_temp1", "myPlugin_temp2"]); // List all keys and values const allKeys = await DataStore.keys(); const allValues = await DataStore.values(); const allEntries = await DataStore.entries(); // Clear all plugin data (use with caution) // await DataStore.clear(); ``` ## Notifications API Display custom notifications to users with support for rich content, images, click handlers, and native OS notifications. ```typescript import { showNotification, requestPermission } from "@api/Notifications"; // Simple notification showNotification({ title: "Update Available", body: "A new version of the plugin is available.", permanent: false, // Auto-dismiss after timeout }); // Rich notification with all options showNotification({ title: "New Message from Alice", body: "Hey! Are you coming to the party tonight?", icon: "https://cdn.discordapp.com/avatars/123/abc.png", // Profile picture image: "https://example.com/party-banner.jpg", // Large image color: "#7289da", // Accent color permanent: true, // Don't auto-dismiss noPersist: false, // Save to notification log dismissOnClick: true, // Close when clicked onClick() { console.log("Notification clicked!"); // Navigate to channel, open modal, etc. }, onClose() { console.log("Notification dismissed"); } }); // Rich body with React components showNotification({ title: "Plugin Action Required", body: "Click to configure settings", richBody: (

Your plugin needs configuration:

  • Set API key
  • Choose default language
), }); // Request native notification permission const hasPermission = await requestPermission(); if (hasPermission) { console.log("Can show native notifications"); } ``` ## Context Menu API Add custom items to Discord's right-click context menus on messages, users, channels, and more. ```typescript import { addContextMenuPatch, removeContextMenuPatch, findGroupChildrenByChildId, addGlobalContextMenuPatch } from "@api/ContextMenu"; import { Menu } from "@webpack/common"; // Patch a specific context menu const messagePatch = (children, { message }) => { // Find the group containing "copy-text" to insert nearby const group = findGroupChildrenByChildId("copy-text", children); if (!group) return; // Insert after copy-text const idx = group.findIndex(c => c?.props?.id === "copy-text"); group.splice(idx + 1, 0, translateMessage(message)} />, bookmarkMessage(message)} /> ); }; // Register the patch for message context menus addContextMenuPatch("message", messagePatch); // Patch user context menu with submenu addContextMenuPatch("user-context", (children, { user }) => { children.push( navigator.clipboard.writeText(user.id)} /> toggleTracking(user.id)} /> ); }); // Global patch that applies to ALL context menus addGlobalContextMenuPatch((navId, children, ...args) => { console.log(`Context menu opened: ${navId}`); children.push( console.log(navId, args)} /> ); }); // Remove patch when plugin stops removeContextMenuPatch("message", messagePatch); ``` ## Message Events API Intercept and modify messages before they are sent or edited, and listen for message click events. ```typescript import { addMessagePreSendListener, removeMessagePreSendListener, addMessagePreEditListener, addMessageClickListener } from "@api/MessageEvents"; // Modify messages before sending const preSendListener = (channelId, messageObj, options) => { // Add timestamp to all messages messageObj.content = `[${new Date().toLocaleTimeString()}] ${messageObj.content}`; // Replace text patterns messageObj.content = messageObj.content.replace(/:shrug:/g, "¯\\_(ツ)_/¯"); // Cancel message if it contains blocked words if (messageObj.content.includes("spoiler")) { return { cancel: true }; } // Access reply info if (options.replyOptions.messageReference) { console.log("Replying to:", options.replyOptions.messageReference.message_id); } }; addMessagePreSendListener(preSendListener); // Modify messages during editing const preEditListener = async (channelId, messageId, messageObj) => { // Async operations are supported const filtered = await filterContent(messageObj.content); messageObj.content = filtered; // Return cancel to abort the edit return { cancel: false }; }; addMessagePreEditListener(preEditListener); // Listen for message clicks const clickListener = (message, channel, event) => { if (event.altKey) { console.log("Alt+clicked message:", message.id); // Copy message content navigator.clipboard.writeText(message.content); } }; addMessageClickListener(clickListener); // Clean up in plugin stop() removeMessagePreSendListener(preSendListener); ``` ## Badges API Add custom badges to user profiles that appear next to usernames. ```typescript import { addProfileBadge, removeProfileBadge, BadgePosition } from "@api/Badges"; // Simple image badge const customBadge = { description: "Custom Plugin User", iconSrc: "https://example.com/badge.png", position: BadgePosition.END, shouldShow: ({ userId }) => isPluginUser(userId), onClick: (event, { userId }) => { openUserProfile(userId); } }; addProfileBadge(customBadge); // Component-based badge with dynamic content const dynamicBadge = { key: "vencord-level", // Required for component badges component: ({ userId }) => { const level = getUserLevel(userId); return ( Lvl {level} ); }, position: BadgePosition.START, shouldShow: ({ userId }) => getUserLevel(userId) > 0, }; addProfileBadge(dynamicBadge); // Badge with multiple variants const multiBadge = { description: "Achievement Badge", position: BadgePosition.END, getBadges: ({ userId }) => { const achievements = getUserAchievements(userId); return achievements.map(a => ({ description: a.name, iconSrc: a.icon, link: `https://example.com/achievement/${a.id}` })); } }; addProfileBadge(multiBadge); // Remove badge when plugin stops removeProfileBadge(customBadge); ``` ## Chat Bar Buttons API Add custom buttons to Discord's message input area (chat bar) next to emoji, GIF, and sticker buttons. ```typescript import { ChatBarButton, addChatBarButton, removeChatBarButton } from "@api/ChatButtons"; import { ChannelStore } from "@webpack/common"; // Define the button icon const MyIcon = ({ height = 24, width = 24 }) => ( ); // Create the button factory const MyButton = ({ channel, isMainChat }) => { const [active, setActive] = useState(false); // Only show in main chat, not threads/DMs if (!isMainChat) return null; return ( { setActive(!active); showNotification({ title: "Feature " + (active ? "Disabled" : "Enabled"), body: `In channel #${channel.name}` }); }} onContextMenu={(e) => { e.preventDefault(); openSettingsModal(); }} > {active &&
} ); }; // Register the button addChatBarButton("MyFeature", MyButton, MyIcon); // Or use in plugin definition export default definePlugin({ name: "ChatBarDemo", chatBarButton: { icon: MyIcon, render: MyButton } }); // Remove when done removeChatBarButton("MyFeature"); ``` ## Message Popover Buttons API Add custom buttons to the message action popover (the toolbar that appears when hovering over messages). ```typescript import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover"; import { ChannelStore } from "@webpack/common"; const BookmarkIcon = () => ( ); // Add a popover button addMessagePopoverButton("bookmark", (message) => { // Return null to hide button for certain messages if (message.author.bot) return null; return { label: "Bookmark Message", icon: BookmarkIcon, message: message, channel: ChannelStore.getChannel(message.channel_id), onClick: () => { saveBookmark(message); showNotification({ title: "Bookmarked!", body: `Message saved from #${message.channel_id}` }); }, onContextMenu: (e) => { e.preventDefault(); showBookmarkOptions(message); } }; }, BookmarkIcon); // Plugin definition approach export default definePlugin({ name: "MessageActions", messagePopoverButton: { icon: BookmarkIcon, render(message) { if (!message.content) return null; return { label: "Quick Action", icon: BookmarkIcon, message, channel: ChannelStore.getChannel(message.channel_id), onClick: () => performAction(message) }; } } }); ``` ## Webpack Module Finding Vencord provides utilities to find and access Discord's internal webpack modules for patching and using Discord's internal APIs. ```typescript import { findByPropsLazy, findByCodeLazy, findStoreLazy, proxyLazyWebpack } from "@webpack"; import { findCssClassesLazy } from "@webpack"; // Find module by exported property names const MessageActions = findByPropsLazy("sendMessage", "editMessage"); const UserStore = findByPropsLazy("getUser", "getCurrentUser"); // Find function by code snippet it contains const openUserProfile = findByCodeLazy("openUserProfileModal"); const createBotMessage = findByCodeLazy('username:"Clyde"'); // Find Flux stores const GuildStore = findStoreLazy("GuildStore"); const ChannelStore = findStoreLazy("ChannelStore"); // Lazy proxy for complex lookups const SomeModule = proxyLazyWebpack(() => { const mod = findByProps("specificProp"); return mod?.nestedProperty; }); // Find CSS class names const ButtonClasses = findCssClassesLazy("button", "buttonWrapper"); const MessageClasses = findCssClassesLazy("message", "messageContent"); // Usage example function sendCustomMessage(channelId: string, content: string) { MessageActions.sendMessage(channelId, { content, tts: false, invalidEmojis: [], validNonShortcutEmojis: [] }); } const currentUser = UserStore.getCurrentUser(); console.log(`Logged in as ${currentUser.username}`); ``` ## Flux Events Integration Subscribe to Discord's Flux event system to react to application state changes like message creation, presence updates, and more. ```typescript export default definePlugin({ name: "FluxListener", description: "React to Discord events", flux: { // Called when a message is created MESSAGE_CREATE({ message, channelId }) { console.log(`New message in ${channelId}:`, message.content); if (message.mentions.some(m => m.id === getCurrentUserId())) { showNotification({ title: "You were mentioned!", body: message.content.slice(0, 100) }); } }, // User presence updates PRESENCE_UPDATES({ updates }) { for (const update of updates) { console.log(`${update.user.id} is now ${update.status}`); } }, // Voice state changes VOICE_STATE_UPDATES({ voiceStates }) { for (const state of voiceStates) { if (state.channelId) { console.log(`User ${state.userId} joined voice`); } } }, // Guild member updates GUILD_MEMBER_UPDATE({ guildId, user }) { console.log(`Member ${user.username} updated in guild ${guildId}`); }, // Typing indicators TYPING_START({ channelId, userId }) { console.log(`User ${userId} started typing in ${channelId}`); }, // Async handlers are supported async CHANNEL_SELECT({ channelId }) { if (channelId) { const messages = await fetchRecentMessages(channelId); console.log(`Loaded ${messages.length} recent messages`); } } } }); ``` ## Summary Vencord provides a comprehensive plugin development framework that allows deep integration with Discord's client through webpack patching, Flux event subscriptions, and UI injection points. The API design follows React patterns with hooks for reactive state management, and TypeScript support throughout ensures type safety when working with Discord's complex internal structures. Plugins can range from simple command additions to complete UI overhauls using the patching system. The modular architecture separates concerns clearly: Settings API handles configuration persistence, DataStore provides IndexedDB access for complex data, Commands API manages slash commands, and the various UI APIs (ChatButtons, MessagePopover, ContextMenu, Badges) allow inserting custom elements at predefined extension points. This design enables plugins to be developed independently while maintaining compatibility with Discord updates through the abstraction layer provided by Vencord's webpack module finders and type definitions.