# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Commands ```bash # Development bun --watch bot/index.ts # Run bot + API with hot reload docker compose up # Start all services (bot, API, database) docker compose up app # Start just the app (bot + API) docker compose up db # Start just the database # Testing bun test # Run all tests bun test path/to/file.test.ts # Run a single test file bun test shared/modules/economy # Run tests in a directory bun test --watch # Watch mode # Database bun run db:push:local # Push schema changes (local) bun run db:studio # Open Drizzle Studio (localhost:4983) bun run generate # Generate Drizzle migrations (Docker) bun run migrate # Apply migrations (Docker) # Admin Panel bun run panel:dev # Start Vite dev server for dashboard bun run panel:build # Build React dashboard for production ``` ## Architecture Aurora is a Discord RPG bot + REST API running as a **single Bun process**. The bot and API share the same database client and services. ``` bot/ # Discord bot ├── commands/ # Slash commands by category (admin, economy, inventory, etc.) ├── events/ # Discord event handlers ├── lib/ # BotClient, handlers, loaders, embed helpers, commandUtils ├── modules/ # Feature modules (views, interactions per domain) └── graphics/ # Canvas-based image generation (@napi-rs/canvas) shared/ # Shared between bot and API ├── db/ # Drizzle ORM client + schema (users, economy, inventory, quests, etc.) ├── lib/ # env, config, errors, logger, types, utils └── modules/ # Domain services (economy, user, inventory, quest, moderation, etc.) api/ # REST API (Bun HTTP server) └── src/routes/ # Route handlers for each domain panel/ # React admin dashboard (Vite + Tailwind + Radix UI) ``` **Key architectural details:** - Bot and API both import from `shared/` — do not duplicate logic. - Services in `shared/modules/` are singleton objects, not classes. - The database uses PostgreSQL 16+ via Drizzle ORM with `bigint` mode for Discord IDs and currency. - Feature modules follow a strict file suffix convention (see below). ## Import Conventions Use path aliases (defined in `tsconfig.json`). Order: external packages → aliases → relative. ```typescript import { SlashCommandBuilder } from "discord.js"; // external import { economyService } from "@shared/modules/economy/economy.service"; // alias import { users } from "@db/schema"; // alias import { createErrorEmbed } from "@lib/embeds"; // alias import { localHelper } from "./helper"; // relative ``` **Aliases:** - `@/*` → `bot/` - `@shared/*` → `shared/` - `@db/*` → `shared/db/` - `@lib/*` → `bot/lib/` - `@modules/*` → `bot/modules/` - `@commands/*` → `bot/commands/` ## Code Patterns ### Module File Suffixes - `*.view.ts` — Creates Discord embeds/components - `*.interaction.ts` — Handles button/select/modal interactions - `*.service.ts` — Business logic (lives in `shared/modules/`) - `*.types.ts` — Module-specific TypeScript types - `*.test.ts` — Tests (co-located with source) ### Command Definition ```typescript export const commandName = createCommand({ data: new SlashCommandBuilder().setName("name").setDescription("desc"), execute: async (interaction) => { await withCommandErrorHandling(interaction, async () => { const result = await service.method(); await interaction.editReply({ embeds: [createSuccessEmbed(result)] }); }, { ephemeral: true }); }, }); ``` `withCommandErrorHandling` (from `@lib/commandUtils`) handles `deferReply`, `UserError` display, and unexpected error logging automatically. ### Service Pattern ```typescript export const serviceName = { methodName: async (params: ParamType): Promise => { return await withTransaction(async (tx) => { // database operations }); }, }; ``` ### Error Handling ```typescript import { UserError, SystemError } from "@shared/lib/errors"; throw new UserError("You don't have enough coins!"); // shown to user throw new SystemError("DB connection failed"); // logged, generic message shown ``` ### Database Transactions ```typescript import { withTransaction } from "@/lib/db"; return await withTransaction(async (tx) => { const user = await tx.query.users.findFirst({ where: eq(users.id, id) }); await tx.update(users).set({ coins: newBalance }).where(eq(users.id, id)); return user; }, existingTx); // pass existing tx for nested transactions ``` ### Testing Mock modules **before** imports. Use `bun:test`. ```typescript import { describe, it, expect, mock, beforeEach } from "bun:test"; mock.module("@shared/db/DrizzleClient", () => ({ DrizzleClient: { query: mockQuery }, })); describe("serviceName", () => { beforeEach(() => mockFn.mockClear()); it("should handle expected case", async () => { mockFn.mockResolvedValue(testData); const result = await service.method(input); expect(result).toEqual(expected); }); }); ``` ## Naming Conventions | Element | Convention | Example | | ---------------- | ---------------------- | -------------------------------- | | Files | camelCase or kebab-case | `BotClient.ts`, `economy.service.ts` | | Classes | PascalCase | `CommandHandler`, `UserError` | | Functions | camelCase | `createCommand`, `handleShopInteraction` | | Constants | UPPER_SNAKE_CASE | `EVENTS`, `BRANDING` | | Enums | PascalCase | `TimerType`, `TransactionType` | | Services | camelCase singleton | `economyService`, `userService` | | Types/Interfaces | PascalCase | `Command`, `Event`, `GameConfigType` | | DB tables | snake_case | `users`, `moderation_cases` | | Custom IDs | snake_case with prefix | `shop_buy_`, `trade_accept_` | | API routes | kebab-case | `/api/guild-settings` | ## Key Files | Purpose | File | | ----------------- | -------------------------- | | Bot entry point | `bot/index.ts` | | Discord client | `bot/lib/BotClient.ts` | | DB schema index | `shared/db/schema.ts` | | Error classes | `shared/lib/errors.ts` | | Environment vars | `shared/lib/env.ts` | | Config loader | `shared/lib/config.ts` | | Embed helpers | `bot/lib/embeds.ts` | | Command utils | `bot/lib/commandUtils.ts` | | API server | `api/src/server.ts` |