Thanks for your interest in contributing! BunMail is an open-source project and we welcome contributions of all kinds.
# Clone the repo
git clone https://github.com/mohamedboukari/bunmail.git
cd bunmail
# Install dependencies
bun install
# Copy environment config
cp .env.example .env
# Edit .env: at minimum set DATABASE_URL
# Push schema to dev database
bun run db:push
# Seed the first API key
bun run src/db/seed.ts
# Start the dev server
bun run dev| Command | Description |
|---|---|
bun run dev |
Build the dashboard CSS, start dev server with watch mode |
bun run dev:css |
Rebuild the dashboard CSS on change (Tailwind watcher, 2nd terminal) |
bun run build:css |
Build src/pages/assets/dashboard.css once (minified) |
bun test |
Run all tests |
bunx tsc --noEmit |
Type-check without emitting |
bun run lint |
Run oxlint (type-aware) |
bun run lint:fix |
Run oxlint with auto-fix |
bun run lint:mocks |
Check every mock.module is complete |
bun run db:push |
Push schema to dev DB |
bun run db:generate |
Generate migration files |
bun run db:migrate |
Run migrations |
bun run db:studio |
Open Drizzle Studio |
BunMail follows a module-per-feature architecture:
src/modules/<feature>/
├── <feature>.plugin.ts ← Elysia plugin (route group)
├── services/ ← Business logic (only layer touching DB)
├── dtos/ ← Request validation schemas
├── models/ ← Drizzle pgTable definitions
├── serializations/ ← Response mappers
└── types/ ← TypeScript types
See ARCHITECTURE.md for the full system design.
We use Conventional Commits. The commit message is validated by commitlint on commit.
Format: <type>(<scope>): <description>
Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
Examples:
feat(templates): add Mustache variable renderingfix(queue): recover interrupted emails on startupdocs: update API reference with webhook endpoints
- TypeScript strict mode: no
anytypes - Use proper types, not unsafe casts
- Keep route handlers thin; business logic goes in services
- Only services access the database
- Use kebab-case for filenames, PascalCase for classes, camelCase for variables
- Document public functions with JSDoc comments
The tiers, scripts and conventions are in docs/testing.md. One rule trips people up: Bun's mock.module leaks across test files, so a mock.module factory must provide every export of the module it mocks (stub each one, or spread the real module under the stubs). bun run lint:mocks checks this and runs in CI and on commit; CI also runs the unit and e2e tiers in random order with fixed seeds, so an order-dependent failure reproduces with bun test test/unit --randomize --seed=<seed>.
- Fork the repo and create a branch from
main - Make your changes following the conventions above
- Add/update tests for new functionality
- Run
bun test && bunx tsc --noEmit && bun run lint - Open a PR with a clear description of what and why
The repo ships project config for Claude Code. None of it is needed to build or test BunMail.
/ship(.claude/skills/ship/): takes an issue from plan to merged PR using this repo's workflow (plan and approval first, gates in CI order, docs and CHANGELOG, PR template, CI watch)./release(.claude/skills/release/): the release flow (CHANGELOG prep commit,bun run release, then verifying the GitHub Release and the GHCR image attestation).- No-em-dash hook (
.claude/hooks/no-em-dash.ts, wired in.claude/settings.json): enforces the CLAUDE.md rule on files Claude writes, and on lines added to the working tree when a turn ends. - Permission allowlist (
.claude/settings.json): pre-approves read-only and test commands only (type-check, lint, tests,git/ghreads, graphify queries). Nothing that pushes, merges or deletes. - Context7 MCP (
.mcp.json): up-to-date docs for Elysia, Drizzle and Bun viahttps://mcp.context7.com/mcp. It receives library-name queries, never your code. Claude Code asks you to approve it on first use. - graphify (
.claude/skills/graphify/): a local knowledge graph of the codebase (tree-sitter AST, no code leaves the machine). Install the CLI withuv tool install graphifyy, thengraphify hook installonce per clone to rebuild the graph after commits, andgraphify update .to build it.graphify-out/and the generated.husky/post-commit/.husky/post-checkouthooks are gitignored (the hooks embed an absolute local path). Useful forgraphify explain <symbol>and call graphs; natural-languagegraphify queryis keyword-based and can miss.- Its installer also registers
PreToolUsehooks that push "rungraphify queryfirst" on nearly every search and file read. They're deliberately not used here (they fired on unrelated commands and steered toward the weakest query mode). If you re-rungraphify install, remove them from.claude/settings.jsonagain.
- Its installer also registers
Please use GitHub Issues. Include:
- Steps to reproduce
- Expected vs actual behavior
- BunMail version and environment (OS, Bun version, PostgreSQL version)
By contributing, you agree that your contributions will be licensed under the MIT License.