Bot · 2025
Nova Discord Bot
Nova runs three servers with 40k+ combined members. The old bot crashed on every raid. The rewrite is sharded, cache-lean and ships moderation, tickets, an economy and automod behind a web dashboard.
Client
Nova Community Network
Industry
Gaming Communities
Timeline
6 weeks + ongoing
My role
Backend Engineer & Bot Architect
The problem
Moderation was fully manual across three servers and the old bot crashed every time the community spiked.
The solution
A sharded Node.js bot with tickets, an economy system, automod and Redis-backed caching built to survive traffic bursts.
40k+
Members served
-75%
Moderator workload
0
Outages in 8 months
What I built
Ticket system
Private channels, transcripts saved to S3 and searchable from the dashboard.
Automod
Regex + heuristic spam scoring with per-server thresholds and mute escalation.
Economy
Currency, shop, daily streaks — all transactional so double-spend is impossible.
Web dashboard
OAuth2 login, live config, and command usage analytics.
Architecture
- Node.js + discord.js v14, 4 shards behind a shard manager
- PostgreSQL for persistence, Redis for cooldowns and cache
- BullMQ workers for scheduled tasks (unmutes, giveaways, reminders)
- Docker Compose on a Hetzner VPS with health checks and auto-restart
Hard problems
Memory blowups
Disabling presence and member caches cut RAM from 1.4GB to 240MB per shard.
Rate-limit bans
All outbound calls go through a token-bucket queue with jittered backoff — zero 429 penalties since.
Code from the build
A few real excerpts from the repository.
export class TokenBucket {
private tokens: number;
constructor(private cap: number, private refillMs: number) {
this.tokens = cap;
setInterval(() => (this.tokens = this.cap), refillMs);
}
async run<T>(task: () => Promise<T>): Promise<T> {
while (this.tokens <= 0) await sleep(50 + Math.random() * 50);
this.tokens--;
return task();
}
}export default defineCommand({
name: "ticket",
description: "Open a private support ticket",
async execute(ctx) {
const open = await db.ticket.findFirst({
where: { userId: ctx.user.id, status: "OPEN" },
});
if (open) return ctx.reply({ content: `You already have <#${open.channelId}>`, ephemeral: true });
const channel = await ctx.guild.channels.create({
name: `ticket-${ctx.user.username}`,
permissionOverwrites: ticketPerms(ctx),
});
await db.ticket.create({ data: { userId: ctx.user.id, channelId: channel.id } });
return ctx.reply({ content: `Ticket opened: ${channel}`, ephemeral: true });
},
});"Raid nights used to mean a dead bot. Eight months in, we haven't had a single outage."
How it was built
- Discovery call, scope and fixed timeline agreed up front
- Custom design system built in Figma before development
- Typed, reviewed codebase with CI checks on every commit
- Deployed with monitoring, backups and post-launch support