Loading

All projects

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.

Node.jsDiscord.jsRedis

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.

src/lib/rateQueue.ts
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();
  }
}
src/commands/ticket.ts
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."
Aiman K. · Community Lead, Nova

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

Want something like this?

Start a Project