Skip to main content
This walks through building a small plugin end to end. Plugins are the richest addon kind, so they make the best example; themes and icon packs follow the same manifest and lifecycle with their own content. By the end you’ll have a working plugin that finds a Discord module, patches it, and stores a setting.

Describe it with a manifest

Every addon starts with a manifest: the metadata Unbound uses to list, attribute, and validate it. Create a manifest.json with at least the required fields.
manifest.json
{
	"id": "hello-world",
	"name": "Hello World",
	"description": "Logs a greeting when a message is sent.",
	"authors": [{ "name": "you", "id": "1234567890" }],
	"version": "1.0.0",
	"main": "index.js"
}
main points at your entry file, and id is the name you’ll use everywhere else. See the manifest for the full field list.

Write the entry point

Your main file exports a default object with optional start and stop hooks. Unbound calls start when the plugin is enabled and stop when it’s disabled. Set everything up in start; undo it all in stop.
index.ts
export default {
	start() {
		// set up patches, listeners, subscriptions
	},
	stop() {
		// undo everything start() did
	},
};
Your bundle is evaluated at startup even while the plugin is disabled. Keep the top level cheap and do real work inside start. See how plugins are loaded.

Find what you want to change

Use Metro to locate the Discord module you need. Declare the handle lazily so the search doesn’t run until the plugin is actually active.
import { metro, utils } from '@unbound-app/api';

const Messages = utils.lazy(() =>
	metro.findByProps('sendMessage', 'receiveMessage'),
);

Patch it

Wrap the function with the patcher. Create a patcher scoped to your plugin so every patch can be reverted together.
import { patcher } from '@unbound-app/api';

const Patcher = patcher.createPatcher('hello-world');

// inside start():
Patcher.before(Messages, 'sendMessage', ({ args }) => {
	console.log('Sending message to channel', args[0]);
});

Remember a setting

Persist configuration with storage. Scope a store to your plugin id and read or write keys on it. Values survive a reload for free.
import { storage } from '@unbound-app/api';

const settings = storage.getStore('hello-world');
settings.set('greeting', 'Hello from Unbound');
settings.get('greeting', 'Hi'); // default if unset

Clean up in stop

When the plugin is disabled, revert everything. One unpatchAll call undoes every patch the patcher made.
stop() {
	Patcher.unpatchAll();
}
A patch that outlives a disabled plugin keeps running with nothing behind it. Cleaning up in stop is not optional. If you patched a component’s render, also force a re-render. See cleaning up.

The full plugin

Putting the steps together:
index.ts
import { metro, patcher, storage, utils } from '@unbound-app/api';

const Patcher = patcher.createPatcher('hello-world');
const settings = storage.getStore('hello-world');

const Messages = utils.lazy(() =>
	metro.findByProps('sendMessage', 'receiveMessage'),
);

export default {
	start() {
		Patcher.before(Messages, 'sendMessage', ({ args }) => {
			const greeting = settings.get('greeting', 'Hi');
			console.log(greeting, '→ channel', args[0]);
		});
	},
	stop() {
		Patcher.unpatchAll();
	},
};

Where to go next

Function Patching

The full patcher API: before, after, instead, and typing.

Patching Components

Change what Discord’s components render.

Flux Stores

Read Discord’s state and react to its events.

Debugging

Tools for developing and inspecting your addon.