Tady

A public Discord bot for communities: 2,500+ servers and 260,000+ users at peak, a custom shard manager in PHP, Redis for live state and a bilingual interface.

Period
August 2021 — present
Status
Active
My role
Sole developer: the idea, the architecture, the server-side logic, the scaling and everything that came after.
Tady banner: a multilingual Discord bot — utilities, giveaways, moderation

Stack

  • PHP
  • DiscordPHP
  • MySQL
  • Redis
  • JavaScript
  • discord.js
  • Node.js

Problem

The Discord communities I spent time in kept needing the same handful of things: splitting members into random teams, setting up counting channels, running giveaways in Russian with the right conditions, and moderating the server with less friction. The bots that existed covered single cases, and even putting several of them together did not add up to the whole set. So Tady began as a private bot for a few Discord servers.

Solution

Once it was clear other communities wanted the same things, the bot went public. It reached a hundred servers quickly, and that threshold brought Discord's mandatory verification with it. The review for the privileged intents some commands depend on was a process of its own. As the project grew, I adapted the architecture to its scale: sharding, Redis for the active state, and a multilingual interface.

Architecture

Tady sharding and stateA shard on its own is a separate Gateway connection: guilds are pinned to it by a formula on their ID, and a single shard supports at most 2,500 servers. In Tady each shard runs in its own PHP process. DiscordPHP provides no process management, so Tady implements starting, monitoring and restarting those processes itself. The state stays shared: Redis holds the running giveaways and their timers, MySQL remains the source of truth and survives a restart.each opens its own connectiona new giveaway goes to both at onceon startDiscord Gatewaya shard is a Gateway connection, not a processguild → shard: (guild_id >> 22) % num_shardsone shard supports at most 2,500 servers; past 2,500 sharding is requiredTady — PHP / DiscordPHPCustom shard managerstart, watch, restartShard 0processShard 1process…processShard NprocessShared stateRedisrunning giveaways and timersMySQLsource of truth, survives a restart
A shard on its own is a separate Gateway connection: guilds are pinned to it by a formula on their ID, and a single shard supports at most 2,500 servers. In Tady each shard runs in its own PHP process. DiscordPHP provides no process management, so Tady implements starting, monitoring and restarting those processes itself. The state stays shared: Redis holds the running giveaways and their timers, MySQL remains the source of truth and survives a restart.

Key technical decisions

  • Shard manager: the connection to Discord is split across several Gateway connections, each in its own PHP process. DiscordPHP takes a shard number but doesn't manage processes, so the project has its own process manager to start, monitor and restart them.
  • Redis as the active-state layer with the database as the source of truth: on restart the bot rebuilds state from the database, and new giveaways are written to both at once.
  • The language comes from the user's own Discord settings, with English as the fallback, so a Russian-speaking server never has to switch anything by hand.
  • Error handling tells whether the user or the bot is missing a permission, and names the permission in the caller's language.
  • Privileged intents trimmed to what is still needed: all three were requested at verification, but Presence is no longer used, so the current review covers only Members and Message Content.

Scale and sharding

Once it grew past 2,500 servers, the project moved to Discord sharding, which is mandatory at that scale.

A shard in Discord is a single Gateway connection. Guilds are distributed across shards by a formula on their id, and one shard supports at most 2,500 servers. DiscordPHP accepts sharding parameters but ships no ready-made manager for shard processes, so Tady got one of its own: each shard ran in its own PHP process, and starting, watching and restarting them was automated. State stayed shared across the processes: Redis held the active giveaways and their timers, while MySQL served as the source of truth and kept the persistent state. A shard process restarting did not lose the active state: it was rebuilt from MySQL into Redis, covering giveaways, LFG, counting channels and the other scenarios that persist.

A separate version on Node.js and discord.js was built over the course of the project. Work on that branch was stopped later, and production stayed on PHP and DiscordPHP.

Multilingual interface

The bot supports Russian and English. Replies, button labels and help messages follow the language the user has selected in Discord. If that language is not supported, it falls back to English.

Multilingual interface
The same /help in Russian and English: the categories, the command list and the counters all follow the language the viewer set in Discord.

Error handling

The bot tells refusals apart by cause and answers each one differently. When a permission is missing, it works out whether the user or the bot lacks it and names the permission in the caller’s language. Cases that have nothing to do with permissions are handled separately: a member whose role is equal to or above the caller’s, or an administrator who is immune to mute. Every reply names the reason and, where there is one, the next step, and if that doesn’t help, it links straight to the support server.

Error handling: four kinds of refusal
Four kinds of refusal: the user lacks a permission; the bot lacks one — with a prompt to enable it and try again; the member’s role is equal to or above the caller’s; and an administrator is immune to mute. Only the person who ran the command sees the reply.

Durations

Durations are written as text, with no need to convert units by hand. 1d 3h 12m 10s, for instance, is read as 1 day, 3 hours, 12 minutes and 10 seconds. The same format works in every command that takes a duration.

Durations
The /mute help card: the command’s syntax, the duration format “1d 3h 12m 10s”, and two worked examples. Only the person who ran it sees the reply.

Giveaways

The bot runs giveaways that end automatically on a timer, through the /giveaway command. The duration uses the same format as everywhere else. Once started, the bot tracks the time left, wraps the giveaway up on its own and picks one or several winners at random from the entrants. Whoever started it can see their running giveaways, end them early, and reroll the winners if needed.

Giveaways
The original 2022 screenshot, in the bot’s Russian interface. The announcement names the host, the prize and its sponsor, the number of winners and when results are due; on its timer the bot closes with four winners and 3,237 entrants. The counter reads 3,286 because the capture was taken a month after the draw and reactions kept arriving. The result is fixed at the moment of the draw and never read off the counter, which always carries the bot’s own extra reaction.

Help and hints

Every command comes with a short description, instructions and examples: /help lists the commands and /help <command> explains one in detail. That reply is visible only to whoever asked for it, so it does not clutter the channel.

Help and hints
/help lists the commands by category; /help <command> opens one. Here that is giveaway, with its create, list, delete and reroll subcommands and worked examples.

Support server

The bot has a support server where users get help, find answers to common questions and follow project news. The bot never sends broadcasts in direct messages or on users’ servers.

Support server
The support server’s news channel: every announcement goes out in both languages at once, with live server, user and member counters kept in the sidebar.

Utilities and fun

Beyond the main use cases, Tady has a few small commands for a community’s everyday needs: /teams splits people into teams at random, /translit converts Cyrillic nicknames to Latin, /info and /avatar show account details, and /counter sets up counting channels. /lfg stands apart: the bot posts a group with its slots and deadline, people join with a button, and it announces the group itself once the slots are filled.

Utilities and fun
Five commands: /teams splits people into teams, /info shows account and server join dates, /avatar pulls up an avatar, /lfg gathers a group for a game with slots, a deadline and join buttons, /translit turns Cyrillic into Latin.

Results

  • Passed Discord verification and the privileged-intents review, without which the commands don't work at scale.
  • Grew from a private bot for a few communities into a public project that is still running and still maintained.
  • Sharding and the shared state let the bot scale without tying the whole load to a single Gateway connection or process.

Metrics

Discord servers at peak
2500+
users at peak
260 000+

Facts

  • A bug in Discord's ticket system made verification take four months. For all of that time, platform restrictions meant users could not invite the bot to their servers.