Architecture
PhoenixCord has four parts.
| Part | Crate | Role |
|---|---|---|
| Plugin API | crates/sdk |
The WIT definition every plugin is written against, plus the Rust bindings |
| Discord state | crates/discord |
Discord’s data models and a copy of the client state, built from gateway events |
| Plugin host | crates/host |
Loads plugins as WebAssembly components and dispatches hooks and events |
| Adapter | not yet written | Connects the host to Discord’s native client |
The adapter boundary
Section titled “The adapter boundary”The host talks to Discord only through the adapter. The adapter forwards gateway events and outgoing messages to the host, calls the host’s hooks while Discord renders its UI, and performs actions such as sending a message or showing a toast.
Everything that depends on how Discord’s client is implemented lives in the adapter. The plugin API describes intent, for example “a button next to the message input”, so it does not change when Discord’s internals do. A Discord update means an adapter update. Plugins are untouched.
Client state
Section titled “Client state”Discord’s gateway protocol is public and stable. PhoenixCord keeps its own copy of guilds, channels, roles, members, messages, presences, voice states and relationships, built from the gateway events the client receives. Plugins read that copy through the client interface, and it powers permission calculations and the old message content in edit and delete events.
Sandboxing
Section titled “Sandboxing”Each plugin is a WebAssembly component in its own instance:
- It can only call the functions in the plugin API. It has no file system, process or network access other than
host.fetch. - Every call into a plugin has a time limit. A plugin that runs too long or crashes is stopped, marked as failed and recreated the next time it is enabled. The rest of Discord keeps running.
- Plugin state lives in the plugin’s own memory. Settings and storage are kept per plugin on disk.
Events and re-entrancy
Section titled “Events and re-entrancy”A plugin is never called while it is already running. Anything a call triggers for the same plugin, such as setting-changed after host.set-setting, is queued and delivered after the call returns. Asynchronous work, such as HTTP requests, timers and Discord API calls, returns an ID immediately and delivers the result later as an event carrying that ID.