Sylo

Sylo

Docker app from Ferdinand99's Repository

Overview

Sylo is a multi-function Discord bot with a web dashboard. It has a per-guild module system (31 modules) - moderation with warning thresholds and a numbered case log (/history, /case), server logging, tickets/modmail answered from the dashboard, reaction roles & autoroles, verification (Verify button or Cloudflare Turnstile captcha), welcome/leave messages, a Welcome Channel builder, birthday announcements, sticky messages, auto-moderation (invite/link/flood/mention/caps/word filters, optionally pushed to Discord's native AutoMod), a counting mini-game, action-based custom slash commands, an autoresponder, reminders (one-off or recurring channel posts), leveling (text + optional voice XP, per-role/channel multipliers, all-time/weekly/monthly public leaderboard), AFK, server statistics (live-count voice channels), server insights (activity charts - messages, joins/leaves and voice-channel usage over time), a free games notifier (Epic Games Store, plus more stores with an IsThereAnyDeal key), ban appeals (banned members get a DM'd link to an appeal form; staff accept/deny from the dashboard), MEE6-style temporary voice channels ("join to create" hubs plus /voice-* control commands), a starboard (re-posts messages that get enough of a reaction into a highlights channel), an invite tracker (credits who invited each member and ranks inviters), reaction polls (/poll with a timer or vote cap), prize giveaways (Enter button, auto-drawn winners), optional Battlefield-series and RuneScape (OSRS/RS3) player stats (/stats, gated by the Game stats module), Twitch / YouTube / Kick go-live & new-video alerts (with an optional "delete the message when the stream ends" mode), RSS/Atom feed alerts (plus Reddit / Mastodon / Bluesky handles), scheduled channel cleanup (auto-delete messages older than a threshold from a channel, on a weekly schedule you pick), and a message/embed creator - plus per-server command management, a Prometheus /metrics endpoint (a ready-made Grafana dashboard ships in the repo), and an editable Discord presence/activity. State is stored in a single SQLite file under /app/data and survives updates. The database is snapshotted automatically (before every schema migration and on a schedule) into /app/data/backups - optionally shipped off-site as well (WebDAV and/or a Discord webhook; see BACKUP_WEBDAV_URL / BACKUP_WEBHOOK_URL); from the dashboard's Health page you can also create, download, import and restore a snapshot (restore swaps the file and lets the container restart on the restored data). Setup: create a Discord application at https://discord.com/developers/applications, then set DISCORD_TOKEN (Bot tab) and DISCORD_CLIENT_ID (General Information). Invite the bot with the scopes "bot" and "applications.commands". Intents: the member/message-driven modules (logging, welcome, reaction roles, verification, leveling, server statistics, invite tracker, auto-moderation, counting, autoresponder, starboard) need the "Server Members" and/or "Message Content" intents enabled on the Developer Portal Bot page. Leave INTENT_GUILD_MEMBERS / INTENT_MESSAGE_CONTENT at their defaults, or set one to false to boot without that intent (its modules stay unavailable). Dashboard: runs open by default (use only on a trusted LAN). To require "Log in with Discord" for server admins, set DISCORD_CLIENT_SECRET and add http://[Unraid-IP]:[HOST-PORT]/auth/discord/callback to the app's OAuth2 Redirects. Community & support: https://discord.gg/GAzR9k5hhS
Sylo

Sylo

Multi-function Discord bot with a MEE6-style web dashboard — add the hosted instance, or self-host on Unraid via Docker.

Add to Discord Discord Test Release Docker Hub Pulls GHCR Node License: MIT Website Roadmap

Thirty-three per-guild modules, Discord OAuth2 login, a public leveling leaderboard, and — via the optional Game stats module — Battlefield-series and RuneScape (OSRS / RS3) player lookups. Everything runs in one Node process, one container, no build step — and stays that way past Discord's 2,500-guild gateway limit via in-process sharding.

Don't want to run your own instance? Sylo is a Discord-verified bot — add it to your server directly. The rest of this README, and self-hosting below, is for running your own copy instead.

The 33 modules

moderation · logging · tickets · reaction roles · verification · welcome · welcome channel · birthdays · sticky messages · auto-moderation · counting · custom commands · autoresponder · auto-react · reminders · leveling · AFK · server statistics · server insights · free games · ban appeals · temporary voice channels · starboard · invite tracker · polls · giveaways · game stats · Twitch alerts · YouTube alerts · Kick alerts · RSS alerts · channel cleanup · GitHub alerts

Documentation

  • sylobot.com — the project site: what Sylo does at a glance, the "Add to Discord" link for the hosted instance, and a self-host quick-start.
  • docs/modules/ — a page per module: what it does, the permissions and intents it needs, its settings, and its commands.
  • docs/dashboard-v2.md — the new React dashboard (Beta, opt-in): what's different, module coverage, and how to try it.
  • docs/self-hosting.md — install, environment variables, reverse proxy, Docker, Unraid, backups, upgrades and rollback, troubleshooting.
  • docs/postgres.md — optional Postgres backend for hosted-scale deployments, and how to move an existing SQLite install over without losing data.
  • Roadmap — what's planned and what's shipped; suggest features and vote.
  • Discord — the community server: support, announcements, and where development discussion happens.
  • Privacy policy · Terms of service

Contents

Screenshots

The dashboard is server-rendered EJS with a small htmx + Alpine layer (no build step): a fixed sidebar with a server switcher, a MEE6-style plugin grid, and a settings panel per module — saves swap in place with a toast.

Breaking changes in 3.0

  • Node 22 is now required (Node 20 is end-of-life). The Docker image is node:22-alpine.
  • DISCORD_GUILD_ID → DISCORD_DEV_GUILD_IDS. The old name still works but logs a deprecation warning on boot — rename it in your .env.
  • Game stats module (off by default) — enable it per server before /stats responds. /stats is one flat command: pick the game (7 Battlefield titles, Old School RuneScape, RuneScape 3) from the game dropdown.
  • The "Reminders" module's id changed from scheduled-messages to reminders. A migration updates existing servers automatically; only matters if you script against the module id or a config-export JSON.

What's new since 2.0

  • Giveaways — /giveaway start prize:… duration:30m posts an embed with a 🎉 Enter button; members join with one click (click again to leave). Winners are drawn automatically at the end time — no reactions, no Message Content intent. Optional winners: count and required_role:. /giveaway end, /giveaway reroll (excludes previous winners) and /giveaway list, plus an End / Reroll list on the dashboard. Winner ping (@here / @everyone) and optional winner DMs are configurable.
  • Image rank & leaderboard cards — /rank replies with a rendered card (avatar, level, server rank, XP bar, message count) and /leaderboard with a rendered top-10 (medal badges, avatars, level, XP), both drawn in-process with @napi-rs/canvas. Each falls back to the old embed automatically if the image renderer is unavailable on the host.
  • Button & dropdown self-assign roles — a reaction-role message can now use clickable buttons or a select menu instead of emoji reactions (pick the style in the builder). Buttons/dropdowns need no Add Reactions permission and no Message Content intent, handle up to 25 roles, and still honour the exclusive / reverse options. Emoji reactions remain the default.
  • Structured logging + error history — every log line is now <ISO timestamp> LEVEL scope message; set LOG_LEVEL (debug/info/ warn/error) and LOG_FORMAT=json (or LOG_JSON=1) for machine-readable output. LOG_LEVEL=debug also emits one line per HTTP request (method path status durationMs). The Health page shows the last ~25 errors (time, scope, message) and a gateway-ping sparkline; GET /health reports errorCount, errorsByScope, commands and a ping history. Dashboard forms and JSON actions now carry a CSRF token (enforced when "Log in with Discord" is enabled).
  • Prometheus metrics — GET /metrics serves the standard text format (unauthenticated, rate-limited, same trust level as the /health JSON): sylo_up, sylo_uptime_seconds, sylo_guilds, sylo_gateway_ping_ms, sylo_db_bytes, sylo_module_enabled{module}, plus counters sylo_commands_total{command}, sylo_component_interactions_total{scope}, sylo_errors_total{scope} and sylo_http_requests_total{route,status}. Import docs/grafana-dashboard.json for a ready-made Grafana overview.
  • Automatic database backups + restore — Sylo writes compacted single-file snapshots of sylo.db to <data>/backups: just before every schema migration, shortly after boot, and on a schedule (BACKUP_INTERVAL_HOURS, default 24; keeps BACKUP_RETENTION, default 14). The dashboard's Health page lists them and can create, download, import (upload a .db from elsewhere, with validation), and restore one — restore snapshots the current database first, then swaps the file and restarts the bot on the restored data. A WAL checkpoint runs alongside each backup and on shutdown so the -wal sidecar stays small. Optionally each snapshot is also shipped off-site (gzipped) to a WebDAV target (BACKUP_WEBDAV_URL + _USER/_PASS, e.g. a Nextcloud folder) and/or a Discord webhook (BACKUP_WEBHOOK_URL) — best-effort, and the Health page shows which targets are active.
  • YouTube alerts — announce a channel's new uploads and when it goes live. Add channels by URL or @handle (resolved on save); new videos come from YouTube's public feed and live status from a light page check — no API key needed. Per-channel Discord channel, ping role and custom messages.
  • Twitch alerts — announce in a channel when a streamer goes live. Add streamers on the dashboard (channel + optional ping role + custom message with {name}/{title}/{game}/{url} placeholders); Sylo polls the Twitch API ~once a minute. Needs a free TWITCH_CLIENT_ID / TWITCH_CLIENT_SECRET.
  • Kick alerts — the same "go live" notification for Kick.com streamers (channel + optional ping role + {name}/{title}/{game}/{url}/{viewers} message). Sylo polls the official Kick API ~once a minute; needs a free KICK_CLIENT_ID / KICK_CLIENT_SECRET (kick.com/settings/developer).
  • Twitch / YouTube / Kick alerts each have a per-streamer Embed / Plain text choice, and a when the stream ends action — delete the "went live" message (default), mark it as ended, or leave it.
  • RSS alerts — follow any RSS 2.0 or Atom feed, or give a Reddit, Mastodon or Bluesky handle and Sylo resolves it to that platform's feed. New items post to a channel with a {title}/{link}/{author}/{feed} template. Up to 15 feeds per server, polled every ~5 minutes; the first check only seeds, so no backlog dump.
  • Polls — members run /poll question:… choices:A | B | C (optional duration, multiple, max_votes); people vote by reacting with the option letter. Auto-closes on its timer or vote cap, posts a results embed with a bar chart, and /poll-end closes one early. Optional allow/deny role list for who may vote — disallowed reactions are removed.
  • Invite tracker — credits the member whose invite link each new joiner used, keeps a per-member tally (joined / left / bonus), and ranks everyone. /invites (your count + a personal link), /inviter, /invites-leaderboard; optional join/leave log channel; a fake-invite grace window so a quick leave removes the credit. Needs the Manage Server permission.
  • Custom commands, rebuilt MEE6-style — each command is now a /slash command that runs an ordered list of actions: reply in the channel (text / embed, optionally private, or a random pick from several messages), post to another channel, or add / remove a role. Per-command role and channel restrictions and a cooldown. Existing text/embed commands migrate automatically (they become a single reply action); the chat-prefix trigger is retired.
  • Starboard — react to a message enough times and Sylo re-posts it into a highlights channel. MEE6-style per-server boards: pick the emoji(s) and threshold, auto-react on the post, remove it if it drops back below the bar or the original is deleted, ignore self-stars and bot messages, and restrict which roles' reactions count and which channels a board watches.
  • MEE6-style dashboard redesign — a fixed left sidebar with a server switcher and collapsible plugin categories; the dashboard is a plugin grid; a Bot Personalizer (username / avatar / banner / presence), a per-server Settings page (bot-master roles, mod-log channel, embed colour, backup), a Leaderboard page, and a tabbed Moderator page (automod, warning auto-actions, infractions, immunity roles, audit logging, command permissions). Welcome Channel and Reaction Roles get a WYSIWYG embed editor.
  • Eight more modules: Verification (button or Cloudflare Turnstile captcha), Autoresponder, AFK, Server statistics (live count voice channels), Free games (Epic + IsThereAnyDeal), Ban appeals (DM'd appeal link → dashboard review → decision shown on the link, with a single-use rejoin invite on accept), Temporary voice channels (MEE6-style hubs + 13 /voice-* control commands), and Welcome Channel (a builder for one pinned message in a read-only channel) — all functional and configurable from the dashboard (26 in total)
  • Editable Discord presence / activity from the dashboard (/settings)
  • 2.0 groundwork still current: action-based custom /slash commands, scheduled messages, full leveling with a public leaderboard, auto-moderation, the Counting mini-game, a YAGPDB-style combined overview and a topbar server switcher; /mydata export + /forget + guild-leave data purge; per-server audit log and JSON config export; CI test gate before any image publish; rate-limited public/auth routes; a database integrity check on boot

Features

  • Discord bot (discord.js v14, slash commands)
    • /help — command overview + a link to the dashboard
    • /ping — health check with gateway + round-trip latency
    • /version — the release this instance is running · /about — version, uptime and runtime info
    • /rank · /leaderboard — leveling progress (when the module is enabled)
    • /mydata — DMs you a JSON export of the data Sylo stores about you in the current server · /forget — delete it
    • /stats game:<Battlefield … | Old School RuneScape | RuneScape 3> username:… [platform:…] — player stats as an embed (Battlefield: K/D, win rate, best class, …; RuneScape: combat / total level, top skills, boss KCs)
    • /history @user · /case view|reason|delete|note — the moderation case log
    • Friendly, non-crashing error handling (unknown player, API down, rate-limited)
  • Moderation — /kick, /ban, /unban, /timeout, /untimeout, /purge, /slowmode, /warn, /modlog, plus a numbered case log: every action is a case, browsable with /history @user and manageable with /case (view / reason / delete / note). Role-hierarchy and permission checks, optional DM to the target, and per-command default permissions so Discord hides them from non-moderators.
  • Extensible game adapters — one file per game, registered in a central registry. Adding a game does not touch bot or web code.
  • Per-guild modules — 33 feature groups, each toggled and configured from the dashboard:
    • Moderation — warning thresholds that auto-timeout/kick/ban, one-click unban
    • Server logging — member / message / role / channel events to a log channel
    • Reaction roles & autoroles — dashboard-built reaction-role embeds; roles on join
    • Verification — gate new members behind a Verify button, or a Cloudflare Turnstile captcha on the dashboard, before granting a role; optional auto-kick of unverified members
    • Welcome & leave — join/leave messages with placeholders, optional DM, plus a shared "give roles to new members" picker
    • Welcome channel — a MEE6-style builder for one rich, pinned message in a dedicated read-only channel (welcome / rules / links / banner elements, live preview); can create the #welcome channel for you and publish/update it
    • Sticky messages — keep a message pinned to the bottom of a channel; per-channel option to also bump it for other bots/webhooks and a configurable minimum gap between reposts
    • Tickets (modmail) — members DM the bot; staff read and reply from the dashboard (replies arrive as an anonymous "Staff" DM)
    • Moderator — a tabbed page consolidating Auto-moderation (bad words, repeated text, invites, links, caps, emojis, spoilers, mentions, zalgo, anti-spam — each Disabled / Delete / Delete + Warn / Delete + Timeout), with an optional push to Discord's native AutoMod for the mappable checks (bad words, mentions, anti-spam) plus Discord's profanity / sexual-content / slurs preset lists — reconciled as Sylo:-named rules, needs Manage Server, auto-actions (warning-count thresholds → timeout/kick/ban), infractions (warnings + ban manager), immunity roles (skip automod + auto-actions), audit logging (server events → a channel), and per-command permissions
    • Counting — members count upward one number per message in a chosen channel; correct or reset the running number from the dashboard
    • Custom commands — MEE6-style /slash commands built from an ordered list of actions: reply in the channel (text and/or embed, optionally private, or a random pick from several messages), post a message to another channel, or add / remove a role. Per-command role/channel restrictions and cooldown; synced to Discord on save
    • Invite tracker — attributes each new member to the invite link they used and ranks inviters. /invites gives a member a personal invite link plus their count, /inviter looks up who invited someone, /invites-leaderboard ranks the server. Optional join/leave log channel and a fake-invite grace window (a join that leaves inside it doesn't count). Needs Manage Server
    • Polls — /poll question:… choices:A | B | C with optional duration, multiple and max_votes; members vote by reacting with the option letter. Auto-closes on the timer or vote cap and posts a bar-chart results embed; /poll-end closes one early. Optional allow/deny role list for voters
    • Twitch alerts — post a "go live" notification to a channel when a Twitch streamer starts streaming. Per-streamer channel, optional ping role and a custom message ({name} {title} {game} {url} {viewers}). Polls the Twitch Helix API ~once a minute; needs TWITCH_CLIENT_ID / TWITCH_CLIENT_SECRET (free app at dev.twitch.tv)
    • YouTube alerts — announce a channel's new uploads and/or "went live" in a Discord channel. Add channels by URL or @handle (Sylo resolves them on save); new videos read from YouTube's public Atom feed, live status from a light /live page check — no API key. Per-channel ping role and custom messages ({name} {title} {url}); polled every ~3 minutes
    • Kick alerts — post a "go live" notification when a Kick.com streamer starts. Per-streamer channel, optional ping role and a custom message ({name} {title} {game} {url} {viewers}). Polls the official Kick API ~once a minute; needs KICK_CLIENT_ID / KICK_CLIENT_SECRET (free app at kick.com/settings/developer)
    • RSS alerts — follow up to 15 RSS 2.0 / Atom feeds per server and post new items to a channel. Per-feed channel, optional ping role and a {title} / {link} / {author} / {feed} template. No API key; polled every ~5 minutes; the first check only seeds so there is no backlog dump. Dedupe rides on the shared posted_keys table (scope rss:<feedId>)
    • Autoresponder — auto-reply when a message matches a trigger (contains / exact / starts-with / whole-word), optionally deleting the trigger
    • Auto-react — automatically react to messages from chosen users or roles, every message or at a random chance, with an optional role add/remove on the same trigger (e.g. tracking a status like "infected"); per-rule channel lock and an optional log channel
    • Scheduled messages — recurring posts to a channel, every minute to every 4 weeks, with pause/resume
    • Leveling — 15–25 XP per message on a MEE6-style curve, optional voice XP, per-role/channel XP multipliers, level-up announcements, per-level role rewards, /rank, and a public leaderboard with all-time / weekly / monthly views
    • AFK — /afk [reason]; Sylo replies to anyone who mentions an away member and clears the status when they next speak
    • Server statistics — keep chosen voice channels named with a live member / role / boost count, on a configurable 5–60 minute refresh
    • Server insights — charts of messages, joins and leaves, member growth, voice-channel usage (minutes, peak in voice) and the busiest text/voice channels, per hour or per day (24h / 48h / 7d / 30d / 90d range). Off by default; enabling it starts the counting. Only aggregate counts are stored — never message content or per-user rows — with ~180-day (hourly: ~3-day) retention
    • Free games — announces games that become free to claim (hourly poll): the Epic Games Store, plus Steam / GOG / Fanatical / Humble and more with an ITAD_API_KEY. Optional ping role; /freegames [dlc] on demand
    • Ban appeals — banned members are DM'd a signed link to an appeal form (sent before the ban lands, since a bot can't DM a user it no longer shares a server with). Staff accept (auto-unban + single-use rejoin invite) or deny with a reason from the Appeals tab. The decision, reason and rejoin invite are always shown on the same link when the user reopens it, so notification never depends on the DM. Configurable questions, post-denial cooldown, and an optional "appeals server" invite so Sylo can DM the outcome too; needs DASHBOARD_URL set
    • Temporary voice channels — MEE6-style hubs: a member joins a "join to create" voice channel and Sylo spawns their own VC (name template with {index}/{username}, user limit, bitrate, keep-alive delay, ownership lock, permission sync, role allow/deny + moderator/ignored roles, owner permissions, an optional paired text channel). Owners and voice moderators control it with 13 /voice-* commands (lock/unlock, hide/reveal, limit, rename, kick, ban/unban, claim/transfer, owner, clean). Multiple hubs; survives a restart via a sweep
    • Starboard — when a message gets enough of a chosen reaction, Sylo re-posts it (as an embed with a jump link) into a highlights channel and keeps the count live. Per-server boards: custom emoji(s) and threshold, auto-react on the post, remove-below-threshold and remove-on-delete, self-star and bot filtering, message-age limits, and role / channel restrictions
  • Command management — disable a command per server or restrict it to channels / roles (admins bypass).
  • Operations — per-server config audit log and JSON config export, /forget self-service data deletion, automatic data purge on guild removal, a database integrity check at startup, and rate-limited public / auth routes.
  • Web dashboard (Express + EJS, server-rendered) — htmx 2 and Alpine.js 3 are vendored as static files under src/web/public/vendor/ (pinned, no CDN, no build step). Settings saves swap a page fragment in place with a toast; every route keeps a no-JS fallback (full render + redirect). The builders (embed editor, reaction roles, custom commands, welcome channel, …) are Alpine components. The public pages below stay framework-free.
    • GET /health — JSON status (uptime, guild count, gateway-ping history, per-scope error counts, command counts) for healthchecks
    • GET /metrics — Prometheus text exposition (uptime, guilds, gateway ping, per-command / per-scope counters, module adoption), unauthenticated like /health; scrape it from your LAN or your own proxy
    • / — bot status, activity stats and module adoption; sidebar server switcher
    • /stats — cached lookups · /health — status page (JSON for monitors)
    • /settings — Bot Personalizer: bot username / avatar / banner + presence
    • /guilds/<id> — per-server control panel: a MEE6-style plugin grid, a Leaderboard page, Settings (bot-master roles, mod-log channel, embed colour, backup), Commands, Moderator (auto-mod + warnings/bans), Tickets, Ban appeals, Embed messages, an audit log, and a settings panel per module
    • public, zero-JS pages — the shareable leaderboard (/leaderboard/:id, /lb/:slug), member verification (/verify) and ban-appeal forms (/appeal)
    • Discord OAuth2 login (optional) — gate the dashboard to server admins (and configurable staff roles for tickets); runs open on a trusted LAN when unconfigured. See Dashboard authentication.
  • SQLite persistence (better-sqlite3) — guild settings, module config, warnings, tickets and a TTL stats cache, in a single volume-mounted file.
  • Lean Docker image — multi-stage node:22-alpine, non-root, HEALTHCHECK. Ships prebuilt native modules (better-sqlite3, @napi-rs/canvas for the rank card) and the DejaVu fonts the card needs.

Project structure

src/
  index.js              Entrypoint — boots DB, bot, and web in one process
  config.js             Loads/validates env vars
  runtime.js            Shared in-memory state (uptime, last error, client)
  bot/
    index.js            Discord client bootstrap (intents, partials)
    loadCommands.js / registerCommands.js
    commands/           help, ping, about, version, stats, rank, leaderboard, forget,
                        afk, freegames, history, case, kick/ban/unban/timeout/untimeout/purge/slowmode/warn/modlog
    events/             ready, interactionCreate, moduleEvents (gateway → modules),
                        dmTickets (DM → ticket), guildDelete (purge on leave)
    embeds/ lib/        embed builders; duration, moderation, modlog, custom-command sync
  modules/
    registry.js         module catalogue (id, intents, defaults)
    dispatch.js         fans gateway events out to enabled modules
    index.js            loads module implementations
    moderation automod logging tickets roles welcome welcomeChannel sticky
    counting customCommands autoresponder verification scheduledMessages
    leveling afk serverStats insights freeGames appeals tempVoice polls giveaways
    starboard inviteTracker twitchAlerts youtubeAlerts kickAlerts rss
    messageCreator
  adapters/games/       gameAdapter, registry, battlefield, runescape
  db/
    index.js            SQLite connection + migrations
    cache guildSettings modules commandOverrides modCases tickets audit
    composedMessages counting scheduledMessages leveling appeals tempVoice
    polls giveaways starboard inviteTracker twitchAlerts youtubeAlerts
    leaderboardVanity backup exportConfig purge
  web/
    server.js           Express app (createApp() is exported for tests)
    routes/             health, dashboard, commands, stats, settings, guilds,
                        guildTickets, guildMessages,
                        verify + appeal + leaderboard (public, framework-free)
    middleware/         auth (OAuth2), csrf, rateLimit, ticketAccess
    lib/                guildContext, sidebarNav, moduleIcons, overviewSummary, …
    views/              EJS templates; guild.ejs + guild/_*.ejs fragment partials
    public/             styles.css, app.js (CSRF + confirm), htmx-setup.js,
                        alpine-components.js, vendor/{htmx,alpine}.min.js
scripts/register-commands.js
test/                   node --test; unit tests + dashboardRoutes.test.js (HTTP)
data/                   SQLite file lives here (git-ignored, volume-mounted)

Local setup

Requires Node.js 22+ and a Discord application with a bot.

git clone <this repo>
cd Sylo
npm install
cp .env.example .env     # then edit .env (see docs/self-hosting.md)
npm test                 # optional: run the adapter test suite
npm start

For fast iteration, set DISCORD_DEV_GUILD_IDS in .env to a test server's ID — commands then register instantly instead of taking up to ~1 hour globally. Pass several ids comma-separated (DISCORD_DEV_GUILD_IDS=id1,id2) to cover more than one test server. npm run register re-syncs commands without a restart.

Self-hosting

Don't need your own instance? Add the hosted bot instead — this section is for running your own copy.

The full guide — every environment variable, Discord application setup, running behind a reverse proxy, Docker, Unraid, backups, upgrades and rollback, and a troubleshooting table — is in docs/self-hosting.md.

Only two variables are required:

Variable Description
DISCORD_TOKEN Bot token (Developer Portal → Bot → Reset Token)
DISCORD_CLIENT_ID Application ID (Developer Portal → General Information)

The dashboard runs open (no login) until you set DISCORD_CLIENT_SECRET — only expose it beyond localhost / a trusted LAN after you do. Both privileged gateway intents default on; disable the ones you don't need with INTENT_GUILD_MEMBERS=false / INTENT_MESSAGE_CONTENT=false.

Docker

cp .env.example .env      # DISCORD_TOKEN + DISCORD_CLIENT_ID
docker compose up -d --build

Or a prebuilt multi-arch image (linux/amd64 + linux/arm64):

Tag What it is
iwgamin/sylo:latest, :X.Y.Z, :X.Y (Docker Hub · GHCR) Stable releases. What the Unraid template pulls.
ghcr.io/ferdinand99/sylo:main, :sha-<short> (GHCR) Rolling build of main.

Unraid

Sylo is in Community Applications — search "Sylo". The template's source of truth is Ferdinand99/unraid-templates; if CA hasn't picked that repo up yet, add https://github.com/Ferdinand99/unraid-templates under Docker → Template repositories to get the current template directly. Put the data directory on a real local disk (e.g. /mnt/cache/appdata/sylo), not /mnt/user — SQLite in WAL mode needs working file locks. See docs/self-hosting.md for the container fields and the SQLite-on-a-network-mount note.

Releases & CI

Three GitHub Actions workflows drive the images. test.yml runs the suite (npm test + syntax + template compile) and is a required dependency of every image build — nothing publishes on a red suite.

Workflow Trigger Publishes
.github/workflows/test.yml called by the two below — (gate only)
.github/workflows/docker-publish.yml every push to main (PRs build only) GHCR :main, :sha-<short> — rolling dev image
.github/workflows/release-please.yml merging a release PR Docker Hub and GHCR :latest, :X.Y.Z, :X.Y — stable release

Cutting a release is automated with release-please:

  1. Land changes on main using Conventional Commits (feat: …, fix: …, refactor: …, chore: …, feat!: …/BREAKING CHANGE: for a major bump).
  2. release-please keeps a PR titled "chore(main): release X.Y.Z" open, accumulating a changelog. When you're ready, merge it.
  3. Merging tags the commit vX.Y.Z, creates the GitHub Release with notes, bumps package.json + CHANGELOG.md, then builds and pushes the versioned images — including :latest, which is what the Unraid template tracks.

docker-publish.yml needs no secrets (built-in GITHUB_TOKEN). release-please.yml also pushes to Docker Hub, so it needs a repo variable DOCKERHUB_USERNAME (e.g. iwgamin) and a repo secret DOCKERHUB_TOKEN (a Docker Hub personal access token with Read & Write). Set both under Settings → Secrets and variables → Actions. Note that a tag/Release created by CI does not trigger other workflows, which is why release-please.yml builds the image itself rather than relying on the tag trigger.

For a clean starting point, tag the current commit once so release-please has a baseline: git tag v1.0.0 && git push origin v1.0.0 (keep .release-please-manifest.json at 1.0.0).

Unraid users who want the bleeding edge can point the container at ghcr.io/ferdinand99/sylo:main (dev images are GHCR-only) instead of :latest; pin to :X.Y.Z on either registry to freeze a version.

Adding another game

  1. Create src/adapters/games/<game>.js exporting an adapter with id, titles(), platformsFor(title), and getPlayerStats(username, platform, { title }) — throw the typed errors from gameAdapter.js for failure cases.
  2. Register it in src/adapters/games/index.js (one import + register(...)).
  3. Add a game choice (<adapter>:<title>) to GAME_CHOICES in src/bot/commands/stats.js, and an embed builder if the stats shape differs. The shared runStatsLookup() helper and the cache layer need no changes.

Tests

npm test

Runs the node:test suite — module normalisers and helpers, the CSRF middleware, and dashboardRoutes.test.js, which boots createApp() over HTTP to assert the htmx fragment contract. No network: fetch is stubbed where adapters need it, and DB tests write to a throwaway SQLite file.

Legal

For the instances operated by Ferdinand99 (the public Sylo and Sylo - Test Discord applications):

Self-hosted instances are run by their own operators; adjust these documents if you publish your own.

License

MIT © Ferdinand99

Download Statistics

4,658
Total Downloads

Related apps

Details

Repository
docker.io/iwgamin/sylo:latest
Last Updated2026-09-10
First Seen2026-08-31

Runtime arguments

Web UI
http://[IP]:[PORT:3000]/
Network
bridge
Shell
sh
Privileged
false

Template configuration

WebUI PortPorttcp

Host port for the Sylo dashboard. Only change the host side; the container always listens on 3000.

Target
3000
Default
3000
Value
3000
Data DirectoryPathrw

Where the SQLite database (sylo.db) is stored. Persists across updates/restarts.

Target
/app/data
Default
/mnt/user/appdata/sylo/data
Value
/mnt/user/appdata/sylo/data
DISCORD_TOKENVariable

Discord bot token (Developer Portal -> your app -> Bot -> Reset Token).

DISCORD_CLIENT_IDVariable

Discord application (client) ID (Developer Portal -> your app -> General Information).

DISCORD_DEV_GUILD_IDSVariable

Optional. Set to a server ID (or several, comma-separated) to register slash commands to those guild(s) instantly (useful for testing). Leave blank to register globally (can take up to ~1 hour the first time).

DISCORD_SHARD_COUNTVariable

Optional. Number of Discord gateway shards this one container runs. Leave blank (or 'auto') to follow Discord's recommendation - it stays at 1 until the bot is in ~2,500+ servers, so most instances never need this. Multi-process sharding is not supported.

DISCORD_CLIENT_SECRETVariable

Optional. Set this (Developer Portal -> OAuth2 -> Client Secret) to require 'Log in with Discord' on the dashboard, restricted to server admins. Also add http://[UNRAID-IP]:[HOST-PORT]/auth/discord/callback to the app's OAuth2 Redirects. Leave blank to run the dashboard open (trusted LAN only).

INTENT_GUILD_MEMBERSVariable

Whether to request the Server Members privileged intent (needed by logging, welcome, reaction roles/autoroles, verification, leveling, server statistics and the invite tracker). Must also be enabled on the Developer Portal Bot page. Set to false to boot without it - those modules then stay unavailable.

Default
true
Value
true
INTENT_MESSAGE_CONTENTVariable

Whether to request the Message Content privileged intent (needed by logging, auto-moderation, the autoresponder, the counting game and the starboard). Must also be enabled on the Developer Portal Bot page. Set to false to boot without it - those modules then stay unavailable.

Default
true
Value
true
SESSION_SECRETVariable

Optional. Any long random string; signs the dashboard session cookie so logins survive a container restart. Auto-generated when blank.

DASHBOARD_URLVariable

Optional. The public dashboard URL (e.g. https://sylo.example.com or http://[UNRAID-IP]:[HOST-PORT]). Needed behind a reverse proxy for the OAuth2 redirect URI, and to build the links members open for verification-captcha and ban appeals. Leave blank for direct IP access without those features.

TURNSTILE_SITE_KEYVariable

Optional. Cloudflare Turnstile site key — enables the Verification module's captcha mode. Free at dash.cloudflare.com -&gt; Turnstile. Needs both keys and DASHBOARD_URL set; otherwise captcha mode falls back to a plain Verify button.

TURNSTILE_SECRET_KEYVariable

Optional. Cloudflare Turnstile secret key — pairs with TURNSTILE_SITE_KEY for the Verification captcha.

ITAD_API_KEYVariable

Optional. IsThereAnyDeal API key (free at isthereanydeal.com/apps) — broadens the Free games module beyond the Epic Games Store to Steam, GOG, Fanatical, Humble and more. Without it, Free games is Epic-only.

TWITCH_CLIENT_IDVariable

Optional. Twitch application client id for the Twitch alerts module ('go live' notifications). Free app at dev.twitch.tv/console. Needs TWITCH_CLIENT_SECRET too; without both, the module's poll loop does nothing.

TWITCH_CLIENT_SECRETVariable

Optional. Twitch application client secret, paired with TWITCH_CLIENT_ID for the Twitch alerts module.

KICK_CLIENT_IDVariable

Optional. Kick application client id for the Kick alerts module ('go live' notifications). Free app at kick.com/settings/developer. Needs KICK_CLIENT_SECRET too; without both, the module's poll loop does nothing.

KICK_CLIENT_SECRETVariable

Optional. Kick application client secret, paired with KICK_CLIENT_ID for the Kick alerts module.

STATS_CACHE_TTL_MINUTESVariable

How long (minutes) to cache a stats lookup before hitting the API again.

Default
5
Value
5
GAMETOOLS_API_BASEVariable

Base URL of the Battlefield stats API.

Default
https://api.gametools.network
Value
https://api.gametools.network
DATABASE_PATHVariable

Path to the SQLite file inside the container. Must sit inside the Data Directory mount.

Default
/app/data/sylo.db
Value
/app/data/sylo.db
BACKUP_INTERVAL_HOURSVariable

How often (hours) to write an automatic compacted database snapshot into Data Directory/backups. A snapshot is also always taken just before a schema migration, and on demand from the Health page. Set to 0 to disable the scheduled backup.

Default
24
Value
24
BACKUP_RETENTIONVariable

How many database snapshots to keep in Data Directory/backups. Older ones are pruned automatically.

Default
14
Value
14
BACKUP_DIRVariable

Optional. Directory (inside the container) for database snapshots. Leave blank to use Data Directory/backups; must sit inside the Data Directory mount if set.

BACKUP_WEBDAV_URLVariable

Optional. Off-site backups: a WebDAV folder URL (e.g. a Nextcloud path like https://cloud.example.com/remote.php/dav/files/me/sylo-backups). After every local snapshot a gzipped copy is PUT to url/name.db.gz. Best-effort and logged; never blocks the local backup.

BACKUP_WEBDAV_USERVariable

Optional. Username for BACKUP_WEBDAV_URL (HTTP basic auth). Use an app password, not your account password.

BACKUP_WEBDAV_PASSVariable

Optional. Password / app password for BACKUP_WEBDAV_URL.

BACKUP_WEBHOOK_URLVariable

Optional. Off-site backups: a Discord webhook URL. Each gzipped snapshot is uploaded as an attachment (skipped when it is over ~8 MiB — use WebDAV for a larger database).

WEB_PORTVariable

Port the app listens on inside the container. Leave at 3000 unless you also change the container side of the WebUI Port mapping.

Default
3000
Value
3000
NODE_ENVVariable

Node environment.

Default
production
Value
production
LOG_LEVELVariable

Log verbosity: debug, info, warn or error. debug also emits one line per HTTP request.

Default
info
Value
info
LOG_FORMATVariable

Log line format: text or json (LOG_JSON=1 is shorthand for json).

Default
text
Value
text
TZVariable

IANA timezone name, used for timestamps in ticket transcripts and logs. Defaults to Europe/Oslo.

Default
Europe/Oslo
Value
Europe/Oslo