Nyatastic 2024
A Discord bot built as a learning project that grew to serve 1,000+ servers. Built on discord.py with a cog-based module system, a MariaDB data layer, and a companion API service.
- Python
- discord.py
- Discord API
- MariaDB
- REST APIs
- Netdata
Context & problem
Nyatastic started as a learning project rather than a product. The goal was to build a Discord bot that served real users and, in the process, learn how a bot actually works: how commands are registered, how the gateway connection is managed, how state is persisted, and what it takes to deploy changes without interrupting service.
Much of the work was unfamiliar. I was integrating APIs I had never used before, so a large part of the project was reading documentation, testing assumptions, and figuring out how another system expected to be called. The bot grew to 1,000+ servers, which gave the learning a real audience instead of a local test harness.
The bot was never the most useful tool in its category, and that is part of the story. Its value was in the engineering habits it taught me: structuring a codebase for change, owning a data layer, and operating a service that other people depended on.
Role & scope
I was the lead developer. I built the bot base, the helper functions, and the loaders that brought modules into the running bot. I also wrote the administration commands, which were restricted to the two people who ran the project: my development partner and me.
Beyond command code, I owned the database models layer and deployment. That meant the bot's state had a consistent place to live, and changes had a repeatable path from development to a running service. I later expanded the project with a companion API service for fetching some of the information the bot provides.
Working with a partner made the boundary between individual work and shared architecture explicit. I owned the core structure and the operational path, and the project depended on both of us being able to work inside that structure.
How the system fits together
Nyatastic was built on discord.py. The library registers commands and maintains the connection to the Discord API gateway, so the bot process receives events and turns them into command invocations without hand-rolling the gateway protocol.
Cogs as module boundaries. Each feature area lived in its own Cog — a class that groups related commands, listeners, and state. I used that structure to keep modules separate and to load or unload them independently. A cog could be swapped or reloaded without restarting the whole bot, which is what made seamless updates possible.
Data layer. Bot state lived in MariaDB. I owned the models layer, which gave commands a single, consistent way to read and write data instead of embedding queries throughout the codebase.
Companion API. The bot was paired with a small API service for fetching certain information the bot provides. Keeping that access behind a service gave the bot one internal interface to call and gave me a second deployable component to operate.
Observability. Netdata monitored the host, which was enough to see whether the bot and its supporting services were healthy.
- RuntimePython
- Frameworkdiscord.py · Cogs
- Data storeMariaDB
- ObservabilityNetdata
The parts that took real expertise
Cog-based module system
The most important design decision was to treat each feature as a Cog rather than adding every command to one growing bot file. A Cog is a class: it groups related commands and listeners, and it can be loaded or unloaded at runtime. That gave the bot a module boundary that matched the way we actually worked — one area could change without forcing a full restart.
The practical benefit was deployment. A new version of a module could be loaded while the bot stayed connected to the gateway, so updates did not have to take the service down. It also made the codebase easier to reason about: if a command behaved badly, the surrounding module was small enough to inspect on its own.
The trade-off is that modularity only works if the boundaries are real. A cog that reaches into another cog's state is not really a module. Keeping that discipline was part of the learning.
Command registration & the gateway
discord.py handled the low-level gateway connection and command registration. My work was above that layer: defining the command surface, deciding which commands belonged together, and making sure the bot's startup path loaded the right modules in the right order.
That separation is useful because the gateway is mostly infrastructure. The bot does not need to reimplement heartbeats or event dispatch; it needs a clean command model and predictable startup. Most of the engineering effort went into the structure around the framework rather than the protocol itself.
Database models layer
I owned the models layer on top of MariaDB. The goal was to give every feature the same way to persist and retrieve its data, rather than letting each command invent its own query. That kept data access consistent and made changes to the schema easier to trace.
The modelling work was also where I learned how much of a bot's behaviour is really about state: what belongs to a guild, what belongs to a user, what has to be written on every command, and what can be derived. Those questions matter more than the database engine once the bot has real users.
Administration commands & permissions
Administration commands were restricted to the project's two developers. These were the commands we used to operate the bot, not the commands end users interacted with. Keeping them in their own module made the permission boundary explicit: the code that could change the bot's behaviour was separate from the code that served normal users.
It was a small feature set, but it taught me the value of separating operator tooling from user-facing features. The two have different risk profiles and different reasons to change.
Companion API service
Alongside the bot, I built an API service for fetching some of the information the bot provides. The bot could call our own service for that data instead of embedding the fetch in every command.
That gave me a second component to deploy and operate, and it clarified where the boundary should sit: the bot handled Discord interactions, while the API handled data access. For a learning project, that was a useful step up in system shape — two services with a clear contract instead of one process doing everything.
Deployment & observability
I handled deployment for both the bot and the API service. The cog system made updates less disruptive: modules could be reloaded without dropping the bot's gateway connection, so a change did not have to mean a visible outage.
Netdata provided host-level visibility. At this scale the monitoring need was simple — is the service up, and is the machine healthy — but having it in place made the difference between guessing and knowing when something went wrong.
Hard problems & trade-offs
The hardest part of Nyatastic was not scale; it was unfamiliarity. Each integration came with its own documentation, its own assumptions, and its own failure modes. A large share of the work was reading, testing, and building a mental model of an API before writing the code that depended on it.
The other recurring problem was keeping the modules independent. Cogs make separation easy to claim and harder to maintain: it is tempting to let one module reach into another because the shortcut is faster in the moment. The code only stayed maintainable when those boundaries were enforced.
And the honest product lesson is that the bot was not especially useful. It reached real servers, but it did not solve a painful problem better than existing tools. That is a different kind of failure from an outage, and it is the one I think about most when I start something new.
Results & impact
- 1,000+Discord servers
- 2Developers
- 1Companion API
- 0Downtime on updates
The numbers are modest, and that is appropriate for a learning project. The bot reached 1,000+ servers and was built by a two-person team. The more durable result is the engineering experience: a real service with real users, a modular codebase, a database layer, a second API service, and a deployment path that did not require taking the bot offline.
What I would do differently
The decision that aged best was the cog structure. Splitting features into loadable modules kept the codebase from turning into one large bot file and made updates less disruptive. If I rebuilt Nyatastic, I would keep that idea and push it further: define the module boundaries before writing commands, and make the contract between modules explicit.
The decision I would reverse is the product direction. I built the bot to learn, and it shows: the features were interesting to implement but not always valuable to users. I would start from a problem people actually had, then choose the framework and architecture around it. Learning by building is effective, but learning by building something useful is more effective.
I would also document the external APIs earlier. Most of the friction came from discovering an API's real behaviour while already committed to a design. A short written model of each integration — inputs, outputs, limits, failure modes — would have saved time and made the code easier to change.
Expertise demonstrated
-
Python & discord.py
Built the bot base, helper functions, and loaders on discord.py, including command registration and gateway integration.
-
Object-oriented module design
Split features into cogs that could be loaded and unloaded independently, enabling updates without a full restart.
-
API integration
Integrated unfamiliar external APIs by working from documentation, and built a companion API service for data the bot provides.
-
Database modelling
Owned the MariaDB models layer and the data access used by the bot's features.
-
Deployment & operations
Deployed the bot and companion API, and used Netdata for host-level observability.
-
Technical leadership
Led development as lead developer, owned the core architecture, and worked with a development partner inside the same module structure.