Building a Production-Ready Discord Bot for Usable - Advanced Guide
Building a Production-Ready Discord Bot for Usable
Category: Tutorials | Difficulty: Advanced | Last Updated: 2025-10-16
Overview
Build a production-ready Discord bot that syncs forum posts to Usable with real-time updates and zero database infrastructure. This advanced guide covers database-free architecture, event-driven design, and full conversation tracking.
This builds on our basic integration guide. Complete that first if you’re new to Discord bots.
What You’ll Learn
- Database-free architecture using Discord as storage
 - Event-driven design with clean handler separation
 - Real-time updates for conversations, tags, and titles
 - Flexible forum mapping to fragment types
 - Production patterns for logging and validation
 - Type-safe development with TypeScript and Zod
 
Production-Ready Features
✅ No Database - Discord stores fragment IDs
✅ Real-Time Syncing - Updates as conversations evolve
✅ Graceful Errors - Bot continues running after failures
✅ Structured Logging - JSON logs for monitoring
✅ Type Safety - TypeScript + Zod validation
✅ Flexible Config - JSON forum-to-fragment mapping
Prerequisites
- TypeScript/JavaScript proficiency
 - Discord server with admin access
 - Usable account with API access
 - Bun or Node.js 18+ installed
 - Completed basic integration
 
Estimated Time
⏱️ 2-3 hours to complete
Key Concepts
Database-Free Architecture
Traditional:
Discord → Bot → Database (stores IDs) → Usable
Our approach:
Discord (bot message contains ID) → Bot → Usable
How it works:
- Bot creates fragment in Usable
 - Bot replies to Discord thread with fragment ID
 - When updates occur, bot searches its own messages
 - Extract fragment ID, update Usable
 
Benefits:
- ✅ Zero infrastructure
 - ✅ Built-in redundancy (Discord’s reliability)
 - ✅ Simpler deployment
 - ✅ Natural data locality
 
Event-Driven Design
Three focused handlers instead of one monolith:
- ThreadCreate → Create fragment
 - ThreadUpdate → Sync tags/title changes
 - MessageCreate → Update conversation
 
Each handler: one responsibility, independently testable, no shared state.
Real-Time Update Strategy
We use full conversation replacement:
// ✅ Rebuild from sourcefetchAllMessages() → format() → replaceFragment()
// ❌ Not this (complex diffs)oldContent + newMessageWhy? Simpler logic, handles deleted messages, always consistent.
Project Structure
usable-discord-bot/├── src/│   ├── config/│   │   └── env.ts                  # Zod validation│   ├── handlers/│   │   ├── ready.handler.ts│   │   ├── thread-create.handler.ts│   │   ├── thread-update.handler.ts│   │   └── message-create.handler.ts│   ├── services/│   │   └── usable-api.service.ts│   ├── types/│   │   ├── discord.ts│   │   └── usable.ts│   ├── utils/│   │   └── logger.ts│   ├── bot.ts│   └── index.ts├── .env├── package.json└── tsconfig.jsonQuick Start
1. Install Dependencies
bun add discord.js axios zodbun add -d @types/node typescript biome2. Configure Environment
.env:
DISCORD_BOT_TOKEN=your_tokenDISCORD_CLIENT_ID=your_idDISCORD_FORUM_MAPPINGS={"forumId":"fragmentTypeId"}USABLE_API_KEY=your_keyUSABLE_WORKSPACE_ID=your_workspace_id3. Environment Validation
src/config/env.ts:
import { z } from 'zod';
const forumMappingSchema = z.record(z.string(), z.string().uuid());
const envSchema = z.object({  DISCORD_BOT_TOKEN: z.string().min(1),  DISCORD_FORUM_MAPPINGS: z.string().transform((str) =>    forumMappingSchema.parse(JSON.parse(str))  ),  USABLE_API_KEY: z.string().min(1),  USABLE_WORKSPACE_ID: z.string().uuid(),});
export const env = envSchema.parse(process.env);
export function isForumTracked(id: string): boolean {  return id in env.DISCORD_FORUM_MAPPINGS;}
export function getFragmentTypeForForum(id: string): string | null {  return env.DISCORD_FORUM_MAPPINGS[id] || null;}4. Structured Logger
src/utils/logger.ts:
class Logger {  private log(level: string, message: string, context?: Record<string, unknown>) {    console.log(JSON.stringify({      level,      timestamp: new Date().toISOString(),      message,      ...context,    }));  }
  error(msg: string, ctx?: Record<string, unknown>) { this.log('error', msg, ctx); }  info(msg: string, ctx?: Record<string, unknown>) { this.log('info', msg, ctx); }  debug(msg: string, ctx?: Record<string, unknown>) { this.log('debug', msg, ctx); }}
export const logger = new Logger();5. Types
src/types/discord.ts:
export const GUILD_FORUM = 15;src/types/usable.ts:
export interface CreateFragmentRequest {  title: string;  content: string;  workspaceId: string;  fragmentTypeId: string;  tags?: string[];}
export interface CreateFragmentResponse {  fragmentId: string;  title: string;}6. Usable API Service
src/services/usable-api.service.ts:
import axios, { type AxiosInstance } from 'axios';import { env } from '../config/env.js';
export class UsableApiService {  private client: AxiosInstance;
  constructor() {    this.client = axios.create({      baseURL: 'https://api.usable.dev/api',      headers: {        Authorization: `Bearer ${env.USABLE_API_KEY}`,      },    });  }
  async createFragment(data: CreateFragmentRequest) {    const response = await this.client.post('/memory-fragments', data);    return response.data;  }
  async updateFragment(fragmentId: string, data: { content?: string; tags?: string[] }) {    await this.client.patch(`/memory-fragments/${fragmentId}`, data);    return true;  }
  formatThreadContent(author: string, content: string, meta: any): string {    return `## Discord Thread
**Author:** ${author}**Posted:** ${meta.timestamp}
---
${content}`;  }
  generateTags(ctx: { guildName?: string; channelName?: string }): string[] {    return ['discord', 'forum-post', `server:${ctx.guildName}`];  }}
export const usableApiService = new UsableApiService();7. Thread Create Handler
src/handlers/thread-create.handler.ts:
import type { ThreadChannel } from 'discord.js';import { env, getFragmentTypeForForum, isForumTracked } from '../config/env.js';import { usableApiService } from '../services/usable-api.service.js';import { GUILD_FORUM } from '../types/discord.js';import { logger } from '../utils/logger.js';
export async function handleThreadCreate(thread: ThreadChannel): Promise<void> {  try {    if (!thread.parent || thread.parent.type !== GUILD_FORUM) return;    if (!thread.parentId || !isForumTracked(thread.parentId)) return;
    const fragmentTypeId = getFragmentTypeForForum(thread.parentId);    if (!fragmentTypeId) return;
    const starterMessage = await thread.fetchStarterMessage();    if (!starterMessage) return;
    const fragment = await usableApiService.createFragment({      title: thread.name,      content: usableApiService.formatThreadContent(        starterMessage.author.username,        starterMessage.content,        { timestamp: starterMessage.createdAt }      ),      workspaceId: env.USABLE_WORKSPACE_ID,      fragmentTypeId,      tags: usableApiService.generateTags({ guildName: thread.guild.name }),    });
    if (fragment) {      await thread.send(        `✅ **Registered in Usable!**📝 Fragment ID: \`${fragment.fragmentId}\``      );      logger.info('Created fragment', { fragmentId: fragment.fragmentId });    }  } catch (error) {    logger.error('Error creating fragment', { error });  }}8. Message Create Handler
src/handlers/message-create.handler.ts:
import type { Message } from 'discord.js';import { isForumTracked } from '../config/env.js';import { usableApiService } from '../services/usable-api.service.js';import { GUILD_FORUM } from '../types/discord.js';import { logger } from '../utils/logger.js';
export async function handleMessageCreate(message: Message): Promise<void> {  if (message.author.bot || !message.channel.isThread()) return;
  const thread = message.channel;  if (!thread.parent || thread.parent.type !== GUILD_FORUM) return;  if (!thread.parentId || !isForumTracked(thread.parentId)) return;
  const fragmentId = await findFragmentIdInThread(thread, message.client.user?.id);  if (!fragmentId) return;
  const conversation = await buildThreadConversation(thread);  if (!conversation) return;
  await usableApiService.updateFragment(fragmentId, { content: conversation });  logger.info('Updated fragment', { fragmentId });}
async function findFragmentIdInThread(thread: any, botUserId?: string): Promise<string | null> {  const messages = await thread.messages.fetch({ limit: 50 });  const botMessage = messages.find(    (msg: any) => msg.author.id === botUserId && msg.content.includes('Fragment ID:')  );  const match = botMessage?.content.match(/Fragment ID: \`([a-f0-9-]+)\`/i);  return match ? match[1] : null;}
async function buildThreadConversation(thread: any): Promise<string | null> {  const messages = await thread.messages.fetch({ limit: 100 });  const userMessages = messages    .filter((msg: any) => !msg.author.bot)    .sort((a: any, b: any) => a.createdTimestamp - b.createdTimestamp);
  return userMessages    .map((msg: any) => `### ${msg.author.username}
${msg.content}`)    .join('\n\n---\n\n');}9. Bot Class
src/bot.ts:
import { Client, Events, GatewayIntentBits } from 'discord.js';import { env } from './config/env.js';import { handleMessageCreate } from './handlers/message-create.handler.js';import { handleThreadCreate } from './handlers/thread-create.handler.js';
export class DiscordBot {  private client: Client;
  constructor() {    this.client = new Client({      intents: [        GatewayIntentBits.Guilds,        GatewayIntentBits.GuildMessages,        GatewayIntentBits.MessageContent,      ],    });
    this.client.on(Events.ThreadCreate, handleThreadCreate);    this.client.on(Events.MessageCreate, handleMessageCreate);  }
  async start() {    await this.client.login(env.DISCORD_BOT_TOKEN);  }
  async stop() {    this.client.destroy();  }}10. Entry Point
src/index.ts:
import { DiscordBot } from './bot.js';
const bot = new DiscordBot();
process.on('SIGTERM', () => bot.stop());process.on('SIGINT', () => bot.stop());
bot.start();Testing
# Start botbun run dev
# In Discord:# 1. Create forum post# 2. Bot replies with fragment ID# 3. Check Usable for fragment# 4. Reply to post# 5. Check Usable for updated conversationBest Practices
✅ Do’s
- Validate environment with Zod
 - Use structured logging
 - Handle errors gracefully
 - Document your code
 - Test locally first
 
❌ Don’ts
- Don’t commit 
.env - Don’t hardcode IDs
 - Don’t ignore rate limits
 - Don’t delete bot messages
 
Troubleshooting
Bot doesn’t respond
Check:
- Message Content Intent enabled in Discord Portal
 - Forum in 
DISCORD_FORUM_MAPPINGS - Bot has Read/Send message permissions
 
Updates don’t work
Check:
- Bot message not deleted
 - Bot has Read Message History permission
 - Fragment ID in bot message
 
Architecture Diagrams
Database-Free Flow
sequenceDiagram    User->>Discord: Create post    Discord->>Bot: threadCreate    Bot->>Usable: Create fragment    Usable-->>Bot: fragmentId    Bot->>Discord: Reply with ID    Note over Discord: ID stored in message!    User->>Discord: Add reply    Discord->>Bot: messageCreate    Bot->>Discord: Find bot message    Discord-->>Bot: Extract ID    Bot->>Usable: Update fragmentRelated Resources
- Basic Discord Bot Integration - Start here
 - Discord.js Guide - Framework docs
 - Usable API Reference - API endpoints
 - Source Code - Full implementation
 
FAQs
Q: Why no database?
A: Discord messages provide free, reliable storage with natural data locality.
Q: What if bot message is deleted?
A: The link is lost. Updates won’t work. Educate users not to delete bot messages.
Q: Can I track multiple fragment types?
A: Yes! Map different forums to different types in DISCORD_FORUM_MAPPINGS.
Footer Note: This documentation is part of the Usable Public workspace.