This page collects the main engineering conventions for OmegaBot: structure, testing, logging, error handling, and a few Discord-specific implementation rules.
Use this page for team conventions. Use Project structure for the codebase map and Troubleshooting for operational debugging.
- Codebase Shape
- Comments And Documentation
- Testing
- Logging
- User-Facing Errors
- Autocomplete And Interaction Handling
- Configuration And Feature Gating
- TypeScript And Discord.js Notes
- Troubleshooting And Future Work
- Keep source and test files under roughly 300 lines when practical.
- Aim for at most 10 direct children per folder so navigation and imports stay manageable.
- Split large files by extracting helpers, types, or focused modules instead of adding more internal sectioning.
src/services is organized into four broad areas:
core/- config, database, logging, metrics, cache, dashboard, circuit breaker, analytics, timediscord/discord/- command loading, interaction handling, rate limits, Discord-specific utilitiesintegrations/- AI, GitHub, weather, statuspage, FAQ, welcome, starboard, summary, Notionstores/- quotes, reminders, timezone, transcript, game stats, fun, joke, roles
- Commands stay thin and delegate work to services.
- Services are grouped by domain rather than by command.
- Pure helpers stay pure where possible.
- Side effects like network, filesystem, database, and Discord I/O should be explicit and easy to trace.
- Runtime state and server configuration must live in SQLite, not ad-hoc JSON
files. Keep JSON files limited to required project metadata such as
package.json,package-lock.json, andtsconfig.json.
For the full folder layout, see Project structure.
| Area | Location |
|---|---|
| Database typed helpers | src/services/core/database/db.ts |
| Help topic content | src/commands/core/help/topics/*.ts |
| Interaction routing and errors | src/services/discord/discord/interaction/ |
| Interaction handlers | src/services/discord/discord/handlers/ |
| Fun subcommand groups | src/commands/games/fun/funSubcommands/ |
| Giveaway button logic | src/commands/games/giveaway/buttonHandler.ts |
| Quote store | src/services/stores/quotes/quoteStore.ts |
| Command usage analytics | src/services/core/analytics/commandUsageStore.ts |
| Request context and correlation IDs | src/services/core/logging/requestContext.ts |
| i18n | src/i18n/index.ts |
- Prefer a short file-purpose comment at the top only when the module is not already obvious.
- Use JSDoc for exported functions or behavior with side effects, recovery rules, or unusual assumptions.
- Use section headers like
/* ----- Section ----- */sparingly in long files. - Comment why something is done, not just what the code already says.
- When docs overlap, keep one page as the source of truth and link to it instead of restating the same instructions.
- Unit and integration tests are colocated near the code they exercise.
- Use
*.test.ts,*.spec.ts, or*.integration.test.ts. - There is no central
src/testfolder.
- Tests that need SQLite should use
useInMemoryDb()fromsrc/services/core/database/dbTestUtils.ts. - Prefer in-memory DBs for fast, isolated tests.
npm test- watch modenpm run test:run- one-shot CI-style runnpm run test:coverage- coverage report
When changing commands or interactions, test:
- success paths
- invalid input paths
- permission failures
- DM vs guild behavior
- ephemeral vs public replies where relevant
- OmegaBot uses a shared pino logger in
src/utils/logger.ts. - Use
LOG_LEVELto control verbosity andLOG_PRETTYfor local readability. - Use
getContextLogger()inside command handlers and interaction flows so logs includerequestId. - Use the base
loggerfor startup, bootstrapping, and code that does not run inside an interaction context.
infofor lifecycle events and normal observabilitywarnfor recoverable problemserrorfor failures that need investigationdebugfor noisy investigation detail
- Log command boundaries, service entry points, and recovery decisions.
- Include
errobjects in structured logs instead of flattening them into strings. - Never log secrets such as
DISCORD_TOKEN, API keys, or raw credential values.
safeReplymay fall back tofollowUpafter an initial failure.- Known Discord interaction errors like
10008,10062, and40060are logged intentionally with context rather than treated as mysterious crashes. - Look for
interactionFailedRecovery: truewhen debugging "interaction failed" reports.
- Users should not see stack traces, raw internal exceptions, secret names, or configuration internals unless there is a strong admin-only reason.
- Prefer short, actionable replies.
- Use
getUserFacingReason(err)fromsrc/utils/errors.tswhen mapping internal failures to user-safe language. - Use
errorReply(interaction, message, suggestions?)fromsrc/utils/interactions.tswhen you want a consistent error format. - Keep detailed diagnosis in logs and, where appropriate, in admin audit channels instead of regular user replies.
When an option uses setAutocomplete(true), implement the command module's optional autocomplete handler and respond within Discord's time window.
export const data = new SlashCommandBuilder().addStringOption((o) =>
o.setName("zone").setDescription("Timezone").setAutocomplete(true),
);
export async function autocomplete(interaction: AutocompleteInteraction) {
const focused = interaction.options.getFocused();
const choices = getMatchingTimezones(focused).slice(0, 25);
await interaction.respond(choices.map((z) => ({ name: z, value: z })));
}If a command does not implement autocomplete, the interaction handler should respond safely with an empty list rather than timing out.
Collectors and interactive handlers should:
- Acknowledge early with
deferReply(),deferUpdate(),reply(), orupdate(). - Do expensive work only after acknowledgement.
- Catch handler errors and try to recover gracefully.
For deeper debugging, see Troubleshooting.
Environment variables control both secrets and optional feature enablement.
Guidelines:
- Required config should fail fast at startup.
- Optional features should disable cleanly when their config is missing.
- Feature-specific code should never assume optional config exists.
Examples:
- GitHub polling only runs when its required GitHub config is present.
- LLM summaries only run when
SUMMARY_MODE=llmand the needed API key exists. - Hangman word management only appears for users with the configured role.
- Notion admin actions only work when the Notion config is valid and the caller has the right Discord-side access.
See Environment Configuration for the full config surface.
OmegaBot uses TypeScript strict mode. Prefer narrowing over assertions whenever possible.
GatewayIntentBitsreplaces old intent flagsChatInputCommandInteractionreplaces older chat-command typesMessageFlags.Ephemeralreplaces olderephemeral: trueusage in many code pathsPermissionFlagsBitsreplaces older permission flag enums
Always narrow interaction types before using specific APIs:
if (interaction.isChatInputCommand()) {
// Safe to use chat-input methods
}
if (interaction.inGuild()) {
// Safe to use guild-specific data
}When checking result objects, narrow by property presence before consuming optional fields.
For operational debugging, see Troubleshooting.
Areas still worth improving:
- replace remaining file stores with database-backed storage where appropriate
- add more integration tests for collector-heavy flows
- keep reducing doc overlap by linking to source-of-truth pages instead of repeating setup steps