- Go 64.6%
- Svelte 20.6%
- TypeScript 14.5%
- CSS 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The board now requests a Screen Wake Lock while the page is visible, releases it when hidden, and reacquires it on visibility/first input. Unsupported browsers are a no-op. The voice presence listener was per bot client, but a bot token receives guild events for every guild it is in. OnGuildReady replaces the per-server member map, so the last guild to load could erase members already sitting in this server's channel; a member who never changed state afterwards stayed invisible. Both GuildReady and GuildVoiceStateUpdate are now filtered to the configured guild. A restart reseeds the presence and deNs appears. |
||
| .forgejo/workflows | ||
| apps/web | ||
| cmd/orebuoys | ||
| deploy/kubernetes | ||
| docs | ||
| internal | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .npmrc | ||
| AGENTS.md | ||
| compose.yaml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| Makefile | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| SPEC.md | ||
| user-guide.md | ||
Orebuoys Soundboard
A self-hosted Discord soundboard. Members sign in with Discord, browse, search and tag a library of short audio clips in a compact web UI, and drop them into a Discord voice channel through one bot. A second switch keeps clicks local to the browser so nobody spams the channel by accident.
The library is the point: clips are indexed where they live, tagged from their filename prefix on import, and curated tags can be added on top. The backend is Go and the frontend is a SvelteKit static build embedded into the same binary, so one process serves the JSON API, the audio and the UI on one port.
New here? The user guide walks through using the board — servers, libraries, playing, capture, editing and administration. This README is for running it.
Features
- Library index. Walks a folder recursively, probes durations with ffprobe, derives a tag from each filename prefix, and skips unchanged files on a rescan. A file that disappears from disk is hidden rather than deleted, so its play history survives.
- Board UI. Search, tag chips (any/all), sorts including a seeded random, several view modes and page sizes, favourites, pinned tags, saved sections, browser preview, and tag editing on one clip or across a selected set in one atomic request.
- Playback into Discord voice. A FIFO queue feeds one mixed Opus stream per server. Up to
MAX_CONCURRENT_PLAYSclips play at once (default 3;1restores strict serialisation), and a play request may ask to overlap the queue within its own ceiling. DAVE end-to-end encryption is provided by a pure-Go session, so the binary stays static. - Authentication. Discord OAuth2 with PKCE, server-side sessions stored hashed, guild membership verified with the caller's own token and rechecked periodically. Admin access is an allowlist of numeric Discord user IDs; roles granted on the board can add narrower permissions without making somebody an admin.
- Multi-server libraries. Libraries are first-class and independent of servers, with two independent grants per server: play (list, hear, queue) and import (upload, import, edit). One server can play several libraries; one library can be shared with several servers. The board shows one server at a time, and every clip action is checked against that server's grants.
- Upload, import and editing. Admin upload with two-pass FFmpeg loudness levelling, directory import driven through the same API, clip composition, and voice capture with per-speaker lanes.
- API and automation. Every action available in the UI is available to an authenticated principal, including personal access tokens that carry their owner's capabilities. Desktop apps pair through a short-lived code approved in the browser, so nobody copies a token. The OpenAPI document is served at
/api/openapi.json.
Requirements
- Go 1.26 to build the server.
- Node 22 with pnpm 10 to build the SPA.
ffmpegandffprobeonPATHat runtime.- A Discord application and bot for anything beyond development mode.
The produced binary is static (CGO_ENABLED=0) when built with the provided Makefile or Dockerfile.
Discord application setup
Create an application in the Discord Developer Portal, add a bot, and copy the application ID, client secret and bot token. Enable no privileged intents: the bot never reads messages and membership is checked over REST with the user's own OAuth token.
Add this redirect URI on the OAuth2 page, replacing the host with the address the board is served from:
https://soundboard.example.com/auth/discord/callback
The app requests the identify and guilds.members.read scopes and uses the authorization code flow with state and PKCE (S256). Invite the bot with View Channel, Connect and Speak in the target voice channel. The full walkthrough, including the permissions integer and the invite URL, is in docs/discord-setup.md.
Quick start with Docker Compose
The default compose service runs a development instance: no Discord credentials, requests handled as a synthetic admin, and fake play counts so sorting has something to show. It is refused at startup when PUBLIC_BASE_URL is https.
cp .env.example .env
mkdir -p library # put audio clips here, or set LIBRARY_PATH
docker compose up --build
Open http://localhost:3000. The first start indexes the library folder; on later starts unchanged files are skipped by size and mtime.
The compose service is deliberately a development instance: DEV_NO_AUTH is set to true in compose.yaml. For a real deployment, run the image with DEV_NO_AUTH=false, fill in the Discord values, and terminate TLS in front of it; the Kubernetes example in deploy/kubernetes shows the full set.
Configuration
Everything is environment-driven; there is no config file. .env.example is the contract and internal/config/config.go is the authority. The settings that matter most:
| Variable | Purpose |
|---|---|
PUBLIC_BASE_URL |
Public origin. The OAuth redirect URI is derived from it, and an https value refuses development auth. |
BOARD_TITLE |
Name shown in the tab, header and login page. Defaults to Soundboard. |
LIBRARY_DIR |
Folder holding the clips. Read-only is the intended mount. |
SQLITE_PATH |
SQLite database file. |
CACHE_DIR |
Uploads, levelled renditions and waveform caches. Back this up. |
DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_BOT_TOKEN |
Discord application credentials. |
DISCORD_GUILD_ID, DISCORD_VOICE_CHANNEL_ID |
Seed server and voice channel for a first run. The board learns further servers from the bot. |
ADMIN_DISCORD_IDS |
Comma-separated numeric user IDs. Usernames are ignored. |
SESSION_SECRET |
Session signing material. Generate with openssl rand -base64 48 and keep it stable. |
DEV_NO_AUTH, ALLOW_ANY_ORIGIN |
Development relaxations. Both are refused on an https public URL. |
MAX_CLIP_SECONDS, MAX_UPLOAD_BYTES, MAX_QUEUE_LENGTH, MAX_CONCURRENT_PLAYS, MAX_OVERLAP_PLAYS, PLAY_COOLDOWN_MS |
Ingest and playback limits. |
TRUST_PROXY |
Trust X-Forwarded-* only when a reverse proxy you control sets them. |
A bad configuration exits at startup and lists every problem at once.
Local development
make run # builds the SPA, then serves a dev instance
make run LIBRARY_DIR=/path/to/clips
./bin/orebuoys --dev # the same thing spelled out
--dev (or DEV_MODE=true) turns on development mode: no Discord login, a relaxed cross-origin guard and trusted forwarded headers. It exists to shape the UI, not to be deployed.
For the SPA with hot reload:
pnpm install
pnpm dev:web # Vite on :5173, proxying /api to the Go process
Point the proxy elsewhere with SOUNDBOARD_API=http://127.0.0.1:9994 pnpm dev:web when the API is not on port 3000.
Tests and checks
make test # go test ./... plus the SPA unit tests
make lint # gofmt check, go vet, ESLint and Prettier check
The Go tests cover auth boundaries, cross-origin guards, path traversal, the queue, dedupe, play counting, role escalation and ingest validation. Discord login and voice cannot be exercised in CI; those paths are tested against fakes and must be verified against a real guild.
.forgejo/workflows/ci.yaml.example is a template, deliberately inert because Forgejo only runs files ending in .yaml/.yml. Copy it to .forgejo/workflows/ci.yaml and set runs-on: to a runner label that exists on your instance; ubuntu-latest is the GitHub convention and queues forever on a self-hosted Forgejo with different labels.
Deployment sketch
The container is a static Go binary on debian:bookworm-slim with ffmpeg. The SPA is embedded at build time, so the image needs no Node runtime. Run one replica: the playback queues are in-process and SQLite is a local file. Terminate TLS at a reverse proxy and expose only the HTTP port. Discord voice needs outbound TCP 443 and outbound UDP to the voice server; a policy that allows HTTPS only will let the bot join and then fail to send audio.
Persist the database and the cache volume together. LIBRARY_DIR is either a read-only mount that is indexed in place or an empty folder inside the data volume filled through the API.
The Kubernetes example and its operator notes cover the shape; docs/deployment.md explains why it is one replica.
Repository layout
| Path | Owns |
|---|---|
cmd/orebuoys |
Entry point and the push import client. |
internal/config |
Environment parsing and validation. |
internal/store |
Every SQL statement and the embedded migrations. |
internal/library |
Filename conventions, ffprobe and the recursive index. |
internal/ingest |
Upload, normalisation, composition and the background worker. |
internal/httpapi |
JSON API, guards, sessions, playback routes and SPA serving. |
internal/auth |
OAuth2, sessions, membership, roles and tokens. |
internal/voice |
Queue, mixer, bot and the disgo/DAVE connection. |
internal/capture |
Voice capture, snapshots and WAV/Ogg helpers. |
internal/realtime, internal/limit, internal/prefs, internal/ops |
Websocket fan-out, rate limiting, per-user preferences and the integrity check. |
internal/static |
The built SPA, embedded at build time. |
apps/web |
SvelteKit static SPA. |
deploy/kubernetes |
Generic manifest and operator notes. |
docs |
Discord setup, API, permissions, capture and deployment design notes. |
Licence
AGPL-3.0. See LICENSE.