Recipe: music bot + remote web controller
Use Shardwire for Discord voice and member actions while a separate channel carries player/queue state for a dashboard—no custom wire primitives.
Music bots usually need two different problems solved:
- Discord gateway facts — who is in which voice channel, move/mute/deafen, stage permissions — Shardwire’s built-ins cover this well (Voice state, move member voice).
- Product state — queue, now playing, Lavalink node health — that is not Discord’s API surface and should not be forced through the Shardwire manifest.
This recipe keeps that separation explicit: Shardwire = Discord, your HTTP/WS/Redis = player state.
Architecture
Shardwire manifest (Discord slice only)
import { defineShardwireApp, generateSecretScope } from 'shardwire';
export const musicControlManifest = defineShardwireApp({
name: 'music-control',
events: ['voiceStateUpdate', 'interactionCreate'],
actions: ['moveMemberVoice', 'deferInteraction', 'editInteractionReply', 'replyToInteraction'],
filters: {
voiceStateUpdate: ['guildId', 'channelId', 'userId', 'voiceChannelId'],
interactionCreate: ['guildId', 'channelId', 'interactionKind', 'commandName'],
},
});
export const musicControlSecretScope = generateSecretScope(musicControlManifest);Bot intents must include GuildVoiceStates (and whatever you need for interactions/messages).
App process responsibilities
- Subscribe to
voiceStateUpdateto reflect “who is where” in your controller UI (via your state API, not by overloading Shardwire). - Use
moveMemberVoice(and related voice actions) for join-to-speak or stage moves—always branch onresult.ok. - Handle
interactionCreatefor slash/dashboard commands that only delegate to your player service (HTTP to the same host as Lavalink, or an internal queue).
Strict startup with expectedScope: musicControlSecretScope keeps this process from silently receiving broader Discord powers than you intended (Strict startup).
Dashboard (React)
Use @shardwire/react for the Shardwire slice only:
import { useMemo } from 'react';
import { useShardwireConnection, useShardwireListener } from '@shardwire/react';
const options = useMemo(
() => ({
url: import.meta.env.VITE_SHARDWIRE_URL,
secret: import.meta.env.VITE_SHARDWIRE_SECRET,
appName: 'music-control',
}),
[],
);
// const sw = useShardwireConnection(options, { strict: true, manifest: musicControlManifest, botIntents: [...] })
// useShardwireListener(sw.status === 'ready' ? sw.app : null, {
// event: 'voiceStateUpdate',
// onEvent: ({ state }) => {
// void fetch('/api/voice/presence', {
// method: 'POST',
// body: JSON.stringify({ guildId: state.guildId, userId: state.userId, channelId: state.channelId }),
// });
// },
// filter: { guildId: 'YOUR_GUILD_ID' },
// enabled: sw.status === 'ready',
// });Player position, queue, and “now playing” should come from fetch('/api/player/...') or a small WebSocket you own—see Custom domain contracts (spike) for why not to tunnel those through Shardwire core.
Related
- Secret cookbook if the dashboard and the bot host share multiple secrets
- Remote bridge (wss) when the dashboard runs in the browser against a remote bot host