Soundbored is an unlimited, no-cost, self-hosted soundboard for Discord. It allows you to play sounds in a voice channel.
Install the cross-platform CLI with npm i -g soundbored for quick automation. Source: christomitov/soundbored-cli.
- Copy the sample environment and set the minimum values:
cp .env.example .env # Required for local testing # DISCORD_TOKEN=... # DISCORD_CLIENT_ID=... # DISCORD_CLIENT_SECRET=... # PHX_HOST=localhost # SCHEME=http
- Run the published container:
docker run -d -p 4000:4000 --env-file ./.env christom/soundbored
- Visit http://localhost:4000, invite the bot, and trigger your first sound.
Create the bot in the Discord Developer Portal, enable Presence, Server Members, and Message Content intents, and grant Send Messages, Read History, View Channels, Connect, and Speak permissions when you invite it.
- In the Discord Developer Portal, open your application and go to Bot → enable Presence, Server Members, and Message Content intents.
- Still in the portal, go to OAuth2 → Redirects and add every URL that will serve Soundbored to the Redirects list. For example:
http://localhost:4000/auth/discord/callback(local development)https://your.domain.com/auth/discord/callback(production, replace with your domain) Discord requires the redirect in your app configuration to match exactly what the browser uses during login; otherwise, OAuth will fail.
- Copy the Client ID and Client Secret from the same page—add them to your
.envfile asDISCORD_CLIENT_IDandDISCORD_CLIENT_SECRET. - Use OAuth2 → URL Generator (scope
bot) to produce the invite link with the permissions listed above.
mix setup # Fetch deps, prepare DB, build assets
mix phx.server # or iex -S mix phx.serverUseful commands:
mix test– run the test suite (coverage viamix coveralls).mix credo --strict– linting.
docker compose up also works for a containerized local run; it respects the same .env configuration.
All available keys live in .env.example. Configure the ones that match your setup:
| Variable | Required | Purpose |
|---|---|---|
DISCORD_TOKEN |
✔ | Bot token used to play audio in voice channels. |
EDA_DAVE |
optional | Override for Discord E2EE voice negotiation in EDA. Default is enabled; set false only for troubleshooting. |
DISCORD_CLIENT_ID / DISCORD_CLIENT_SECRET |
✔ | OAuth credentials for Discord login. |
BASIC_AUTH_USERNAME / BASIC_AUTH_PASSWORD |
optional | Protect the browser UI with HTTP basic auth. API routes stay behind API token auth. |
SECRET_KEY_BASE |
✔ | Signing/encryption secret; generate via mix phx.gen.secret or openssl rand -base64 48. Takes precedence over SECRET_KEY_BASE_FILE. |
SECRET_KEY_BASE_FILE |
optional | Path to file containing signing/encryption secret (e.g. for docker secrets). Preferred for security. |
PHX_HOST |
✔ | Hostname the app advertises (localhost for local runs). |
SCHEME |
✔ | http locally, https in production. |
AUTO_JOIN |
optional | Voice join mode. play (default) — bot joins when you play a sound. presence — bot follows users into channels. false — manual !join only. |
VOICE_IDLE_TIMEOUT_SECONDS |
optional | Seconds of inactivity before the bot auto-leaves. Defaults to 600 (10 min). Set to 0 to disable. In play mode: timer resets per sound, bot also leaves when the last user departs. In false mode: timer starts after the last user leaves. In presence mode: ignored. |
BIND_IP |
optional | IP address the HTTP server binds to. Defaults to 127.0.0.1; set to 0.0.0.0 to bind all interfaces (e.g. Docker dev). |
The application is published to Docker Hub as christom/soundbored.
docker pull christom/soundbored:latest
docker run -d -p 4000:4000 --env-file ./.env christom/soundboredIf you place the container behind your own reverse proxy, set PHX_HOST and SCHEME in .env to match the external URL and terminate TLS in your proxy. No additional compose files are required.
After inviting the bot to your server, join a voice channel and type !join to have the bot join the voice channel. Type !leave to have the bot leave. You can upload sounds to Soundbored and trigger them there and they will play in the voice channel.
The bot manages voice channels automatically in two ways:
The AUTO_JOIN variable controls three modes:
play(default): the bot joins automatically when you play a sound from the web UI or API. It leaves when the last user departs or afterVOICE_IDLE_TIMEOUT_SECONDSseconds of no playback activity (default: 600 s / 10 min).presence: the bot proactively follows users into voice channels on join events, and leaves immediately when the last user departs. The idle timeout is ignored in this mode.false: fully manual — use!join/!leave. IfVOICE_IDLE_TIMEOUT_SECONDSis set, the bot leaves automatically that many seconds after the last user departs.
The API is used to trigger sounds from other applications. Create a personal API token in Settings after signing in, then send it as Authorization: Bearer <USER_API_TOKEN>.
Current API workflow supports:
- listing sounds
- uploading local files
- creating URL-backed sounds
- queueing playback for a specific sound
- stopping active playback
curl https://soundboardurl.com/api/sounds \
-H "Authorization: Bearer <USER_API_TOKEN>"Returns 200 OK with %{data: [...]}.
curl -X POST https://soundboardurl.com/api/sounds \
-H "Authorization: Bearer <USER_API_TOKEN>" \
-F "source_type=local" \
-F "name=wow" \
-F "file=@/path/to/wow.mp3" \
-F "tags[]=meme" \
-F "volume=90"Returns 201 Created with %{data: sound}.
curl -X POST https://soundboardurl.com/api/sounds \
-H "Authorization: Bearer <USER_API_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"source_type":"url","name":"wow","url":"https://example.com/wow.mp3","tags":["meme","reaction"],"volume":90}'Returns 201 Created with %{data: sound}.
curl -X POST https://soundboardurl.com/api/sounds/123/play \
-H "Authorization: Bearer <USER_API_TOKEN>"Returns 202 Accepted with %{data: %{status: "accepted", ...}} because playback is queued asynchronously.
curl -X POST https://soundboardurl.com/api/sounds/stop \
-H "Authorization: Bearer <USER_API_TOKEN>"Returns 202 Accepted with %{data: %{status: "accepted", ...}} because the stop request is also asynchronous.
Errors use %{error: message} or %{errors: changeset_errors} depending on whether the failure is request-level or validation-level.
- Switched the Discord voice/runtime integration over to EDA, bringing DAVE support for current Discord voice encryption negotiation.
- Expanded the authenticated API so external tools can list sounds, upload local files, create URL-backed sounds, queue playback, and stop playback with personal user tokens.
- Public URL handling is now centralized so Discord invite/auth links and API examples stay aligned with the configured host and scheme.
- Audio playback startup is faster and more resilient, reducing common delay/glitch cases during sound playback.
- Voice runtime handling was split into smaller policy/command/presence modules, making Discord connection behavior easier to reason about and maintain.
- Upload and tag persistence flows were consolidated so the LiveView and API paths share the same domain logic.
- The app now boots in a degraded mode when optional voice runtime capabilities are unavailable instead of failing startup entirely.
- Added coverage for command handling, runtime capability detection, public URL behavior, API auth, upload flows, and collaborative sound management rules.
- Clarified the intended collaboration model: any signed-in user can edit shared sound details, but only the original uploader can delete a sound.
- Removed stale dependencies and cleanup scaffolding while continuing the broader code-health refactor.
- New consolidated
Settingsview replaces the standalone API tokens screen and keeps token creation, revocation, and inline API examples in one place. - Stats dashboard adds a week picker, richer recent activity stream, and refreshed layout under the new name “Stats”.
- “Play Random” now respects whatever filters are active, pulling from the current search results or selected tags only.
- Shared tag components and modal tweaks streamline sound management and reduce layout shifts.
- Navigation highlights the active page and keeps Settings aligned with the rest of the app.
- Mobile refinements across the main board and settings eliminate horizontal scrolling and polish button spacing.
- Basic Auth now quietly skips enforcement when credentials are not configured instead of blocking the UI.
- Expanded LiveView coverage for the new Settings page, Stats interactions, and filtered random playback.
- Updated CI workflow and Dependabot configuration keep coverage and dependency checks automated.
- Bumped Phoenix stack and related dependencies, plus cleaned up mix configuration and docs to match the new release.
- User-scoped API tokens with DB storage (generate/revoke in Settings > API Tokens).
- API requests authenticated via
Authorization: Bearer <token>are attributed to the token’s user and increment stats accordingly. - In-app API help with copy-to-clipboard curl commands that auto-fill your site URL and token.
- Added Settings link in the navbar for quick access.
- Released a new CLI for easier local and CI integrations.
- Search bar: reduced debounce to 200ms and added inline spinner while searching.
- Recent Plays: fixed item “disappearing” by using stable DB ids and deterministic ordering; clicked items now bump to the top correctly.
- Added tests for API token lifecycle, API auth with DB tokens, Basic Auth, and the Settings LiveView.
- Coverage improved to ~96% (via mix coveralls).
- DB-backed personal API tokens are the supported authentication path for API access.
- Fixed sounds not playing due to Discord API changes
- Optimized audio playback for faster sound loading and playback
- Updated all dependencies to latest versions
- Added API to get and trigger sounds.
- Added "stop all sounds" button.
- Implemented auto leave and join voice channels.
- Sorting sounds alphabetically
- Added ability to disable basic auth (just comment out BASIC_AUTH_USERNAME and BASIC_AUTH_PASSWORD in .env)
- Added random sound button.
- Added ability to add and trigger sounds from a URL.
- Allow ability to click tags inside sound Cards for filtering.
- Show what user uploaded a sound in the sound Card.
- Fixed bug where if you uploaded a sound and edited its name before uploading a file it would crash.
- Fixed bug where changing an uploaded sound name created a new sound in entry and didn't update the old.
- Implemented join/leave sound notifications
- Added Discord avatar support for member profiles
- Added week selector functionality to statistics page
- Fixed mobile menu navigation issues on statistics page
- Fixed statistics page not updating in realtime
- Fixed styling issues on stats page