
Async - Community
This project is under the GNU GPL v3 license
All the projects in the AsyncCommunityDiscord organisation are used by the discord server discord.gg/graven both by the moderators and the members.
Most of the contributors are part of the staff but the members are also allowed to contribute.
OmniBot is a modular Discord bot: each feature is a self-contained module
that is auto-discovered at startup and can be installed/uninstalled per Discord
server via /modules. Modules declare their own slash commands, event
listeners, interaction handlers and a typed configuration schema (edited live
with /config <module>) — the core wires everything together, so adding a
feature never means touching the bootstrap.
| Area | Choice |
|---|---|
| Language | TypeScript (ESM, NodeNext), Node.js 24 |
| Discord | discord.js v14 |
| Database | PostgreSQL 17 via Prisma (multi-file schema) |
| Package mgr | pnpm |
| Toolchain | mise (tool versions), oxlint + oxfmt (lint/format), lefthook (git hooks), commitlint (commit messages) |
| Tests | Vitest |
| Logging | pino |
| Dev runner | pitchfork (one-command stack) |
- mise — pins Node.js, pnpm and pitchfork (
mise install). Without mise, install Node.js 24 and pnpm manually (versions are in.mise.toml). - Docker (for the PostgreSQL container).
- A Discord bot token — from the Discord Developer Portal.
mise install # installs Node, pnpm, pitchfork from .mise.toml
pnpm install # installs dependencies and git hooks (lefthook)
cp .env.example .env # then fill in the values (see Environment below)One command brings up PostgreSQL and the bot (daemons defined in pitchfork.toml):
pitchfork start bot # starts the db daemon, waits until it is ready, then runs the bot
pitchfork logs bot # tail logs · pitchfork stop bot db # stop everythingPrefer to run things yourself?
docker compose up -d # PostgreSQL 17
pnpm prisma:migrate # apply migrations (first run)
pnpm dev # run the bot with tsxCopy .env.example to .env:
DISCORD_TOKEN— bot token from the Discord Developer Portal.DATABASE_URL— PostgreSQL connection string (default matchescompose.yaml).DEV_GUILD_ID— dev only: guild where slash commands are registered instantly (global commands take ~1h to propagate). Required bypnpm dev.
compose.prod.yaml deploys the images published to GHCR on each v* tag. It
builds nothing, so a host needs Docker, that file and the environment variables
— no source checkout, no compiler.
Production runs it as a Portainer stack pointing at this repository, so the
compose file in use is always the one on master rather than a copy that drifts
from it. Two of its settings matter: Re-pull image, without which a redeploy
reuses the image already cached under latest, and the stack environment
variables, which take the place of an env file. Cutting a release then deploys
itself — the deploy job calls the stack webhook once the images are published.
latest only ever moves to a non-prerelease tag, so cutting a -rc publishes
it without deploying it, and the deploy job skips those tags for the same
reason.
To run the stack by hand instead:
cp .env.prod.example .env.prod # then fill in the values
docker compose --env-file .env.prod -f compose.prod.yaml pull
docker compose --env-file .env.prod -f compose.prod.yaml up -d
docker compose --env-file .env.prod -f compose.prod.yaml logs -f botUpdating is then the same two commands: pull, then up -d — compose recreates
only what changed.
The images come from the multi-stage Dockerfile (Node 24 on Alpine, non-root
user, tini as PID 1), built by CI on every pull request and published on tag.
Variables to provide (all required, none has a default) — in the Portainer
stack configuration, or in .env.prod when running the stack by hand:
DISCORD_TOKEN— bot token from the Discord Developer Portal.POSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_DB— database credentials.DATABASE_URLis assembled from them by the compose file; PostgreSQL sits on an internal network and publishes no port.
What the stack does:
migrate— a one-shot container that runsprisma migrate deploybefore the bot starts (depends_on: service_completed_successfully). The bot image itself ships no Prisma CLI, and two bot instances can never race on the migration history.bot— read-only root filesystem, all capabilities dropped, restarted by Docker (restart: unless-stopped). It has no healthcheck on purpose: the bot exposes no HTTP or TCP endpoint, so any probe would either be a lie about the Discord gateway state or a redundant "is PID 1 alive?" check.tiniforwardsSIGTERM, which the bot handles by closing the Discord client and the Prisma pool.
Building the image alone:
docker build -t omnibot .src/
├── core/ # framework: module loader, /config & /modules, command/event/interaction loaders
├── modules/ # one folder per feature module (commands/, listeners/, services/, *.config.ts, *.module.ts)
├── lib/ # shared contracts (module, config, registry, listener, …)
└── prisma/ # consolidated schema + migrations (generated — never edit by hand)
Each module is a plugin: drop a folder under src/modules/ and it is discovered
automatically — no registration anywhere else.
pnpm dev # run with tsx
pnpm build # prisma:generate + tsc → dist/
pnpm test # run the full test suite (unit + integration)
pnpm format # oxfmt + oxlint --fix
pnpm prisma:migrate # create & apply a migration
pnpm prisma:studio # open Prisma StudioContributions are welcome — staff and members alike.
- Start with the developer guide in
docs/: how to create a module, slash commands, listeners, interactions, services, the Prisma schema, and the user-facing behaviour. - Conventional Commits are enforced (commitlint, via a lefthook
commit-msghook). A lefthookpre-commithook runs oxfmt + oxlint on staged files —pnpm installsets the hooks up for you. - Before opening a PR, make sure
pnpm build,pnpm test,pnpm exec oxlint --deny-warningsandpnpm exec oxfmt --checkall pass (CI runs the same). - Agent instructions live in
AGENTS.md(also symlinked asCLAUDE.md) — a concise orientation that is handy for humans too. - Known tech-debt and follow-ups are tracked in
AUDIT.md.