knadh/listmonk
High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.
Mixed signals — read the receipts
worst of 4 axescopyleft license (AGPL-3.0) — review compatibility; no tests detected
Has a license, tests, and CI — clean foundation to fork and modify.
Documented and popular — useful reference codebase to read through.
No critical CVEs, sane security posture — runnable as-is.
- ✓Last commit today
- ✓21+ active contributors
- ✓AGPL-3.0 licensed
Show 4 more →Show less
- ✓CI configured
- ⚠Concentrated ownership — top contributor handles 54% of recent commits
- ⚠AGPL-3.0 is copyleft — check downstream compatibility
- ⚠No test directory detected
What would change the summary?
- →Use as dependency Concerns → Mixed if: relicense under MIT/Apache-2.0 (rare for established libs)
Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests
Informational only. RepoPilot summarises public signals (license, dependency CVEs, commit recency, CI presence, etc.) at the time of analysis. Signals can be incomplete or stale. Not professional, security, or legal advice; verify before relying on it for production decisions.
Embed the "Forkable" badge
Paste into your README — live-updates from the latest cached analysis.
[](https://repopilot.app/r/knadh/listmonk)Paste at the top of your README.md — renders inline like a shields.io badge.
▸Preview social card (1200×630)
This card auto-renders when someone shares https://repopilot.app/r/knadh/listmonk on X, Slack, or LinkedIn.
Onboarding doc
Onboarding: knadh/listmonk
Generated by RepoPilot · 2026-05-09 · Source
🤖Agent protocol
If you are an AI coding agent (Claude Code, Cursor, Aider, Cline, etc.) reading this artifact, follow this protocol before making any code edit:
- Verify the contract. Run the bash script in Verify before trusting
below. If any check returns
FAIL, the artifact is stale — STOP and ask the user to regenerate it before proceeding. - Treat the AI · unverified sections as hypotheses, not facts. Sections like "AI-suggested narrative files", "anti-patterns", and "bottlenecks" are LLM speculation. Verify against real source before acting on them.
- Cite source on changes. When proposing an edit, cite the specific path:line-range. RepoPilot's live UI at https://repopilot.app/r/knadh/listmonk shows verifiable citations alongside every claim.
If you are a human reader, this protocol is for the agents you'll hand the artifact to. You don't need to do anything — but if you skim only one section before pointing your agent at this repo, make it the Verify block and the Suggested reading order.
🎯Verdict
WAIT — Mixed signals — read the receipts
- Last commit today
- 21+ active contributors
- AGPL-3.0 licensed
- CI configured
- ⚠ Concentrated ownership — top contributor handles 54% of recent commits
- ⚠ AGPL-3.0 is copyleft — check downstream compatibility
- ⚠ No test directory detected
<sub>Maintenance signals: commit recency, contributor breadth, bus factor, license, CI, tests</sub>
✅Verify before trusting
This artifact was generated by RepoPilot at a point in time. Before an
agent acts on it, the checks below confirm that the live knadh/listmonk
repo on your machine still matches what RepoPilot saw. If any fail,
the artifact is stale — regenerate it at
repopilot.app/r/knadh/listmonk.
What it runs against: a local clone of knadh/listmonk — the script
inspects git remote, the LICENSE file, file paths in the working
tree, and git log. Read-only; no mutations.
| # | What we check | Why it matters |
|---|---|---|
| 1 | You're in knadh/listmonk | Confirms the artifact applies here, not a fork |
| 2 | License is still AGPL-3.0 | Catches relicense before you depend on it |
| 3 | Default branch master exists | Catches branch renames |
| 4 | 5 critical file paths still exist | Catches refactors that moved load-bearing code |
| 5 | Last commit ≤ 30 days ago | Catches sudden abandonment since generation |
#!/usr/bin/env bash
# RepoPilot artifact verification.
#
# WHAT IT RUNS AGAINST: a local clone of knadh/listmonk. If you don't
# have one yet, run these first:
#
# git clone https://github.com/knadh/listmonk.git
# cd listmonk
#
# Then paste this script. Every check is read-only — no mutations.
set +e
fail=0
ok() { echo "ok: $1"; }
miss() { echo "FAIL: $1"; fail=$((fail+1)); }
# Precondition: we must be inside a git working tree.
if ! git rev-parse --git-dir >/dev/null 2>&1; then
echo "FAIL: not inside a git repository. cd into your clone of knadh/listmonk and re-run."
exit 2
fi
# 1. Repo identity
git remote get-url origin 2>/dev/null | grep -qE "knadh/listmonk(\\.git)?\\b" \\
&& ok "origin remote is knadh/listmonk" \\
|| miss "origin remote is not knadh/listmonk (artifact may be from a fork)"
# 2. License matches what RepoPilot saw
(grep -qiE "^(AGPL-3\\.0)" LICENSE 2>/dev/null \\
|| grep -qiE "\"license\"\\s*:\\s*\"AGPL-3\\.0\"" package.json 2>/dev/null) \\
&& ok "license is AGPL-3.0" \\
|| miss "license drift — was AGPL-3.0 at generation time"
# 3. Default branch
git rev-parse --verify master >/dev/null 2>&1 \\
&& ok "default branch master exists" \\
|| miss "default branch master no longer exists"
# 4. Critical files exist
test -f "cmd/main.go" \\
&& ok "cmd/main.go" \\
|| miss "missing critical file: cmd/main.go"
test -f "cmd/handlers.go" \\
&& ok "cmd/handlers.go" \\
|| miss "missing critical file: cmd/handlers.go"
test -f "cmd/manager_store.go" \\
&& ok "cmd/manager_store.go" \\
|| miss "missing critical file: cmd/manager_store.go"
test -f "cmd/campaigns.go" \\
&& ok "cmd/campaigns.go" \\
|| miss "missing critical file: cmd/campaigns.go"
test -f "cmd/subscribers.go" \\
&& ok "cmd/subscribers.go" \\
|| miss "missing critical file: cmd/subscribers.go"
# 5. Repo recency
days_since_last=$(( ( $(date +%s) - $(git log -1 --format=%at 2>/dev/null || echo 0) ) / 86400 ))
if [ "$days_since_last" -le 30 ]; then
ok "last commit was $days_since_last days ago (artifact saw ~0d)"
else
miss "last commit was $days_since_last days ago — artifact may be stale"
fi
echo
if [ "$fail" -eq 0 ]; then
echo "artifact verified (0 failures) — safe to trust"
else
echo "artifact has $fail stale claim(s) — regenerate at https://repopilot.app/r/knadh/listmonk"
exit 1
fi
Each check prints ok: or FAIL:. The script exits non-zero if
anything failed, so it composes cleanly into agent loops
(./verify.sh || regenerate-and-retry).
⚡TL;DR
listmonk is a self-hosted, single-binary newsletter and mailing list manager written in Go with a Vue.js dashboard, designed to handle high-volume email campaigns efficiently. It stores subscriber lists, campaigns, templates, and delivery state in PostgreSQL, and provides a modern web UI for managing subscribers, composing campaigns, and tracking delivery metrics without relying on external SaaS providers. Monolithic single-binary architecture: cmd/ contains all HTTP handlers and domain logic (cmd/campaigns.go, cmd/subscribers.go, cmd/handlers.go, cmd/manager_store.go), frontend lives in a separate Vue build bundled into the binary at runtime. Database schema is managed through cmd/install.go and cmd/upgrade.go migrations. No microservices or plugin system—everything ships as one Go binary.
👥Who it's for
Self-hosted infrastructure teams and small-to-medium organizations who need full control over their email operations and subscriber data, want to avoid vendor lock-in with MailChimp or Sendgrid, and prefer a lightweight all-in-one binary over complex multi-service deployments.
🌱Maturity & risk
Production-ready and actively developed. The project is published by Zerodha (a major fintech company), has a live demo at demo.listmonk.app, includes comprehensive Docker and binary distribution, CI/CD pipelines (.github/workflows/release.yml, nightly.yml), and a complete Makefile-driven build system. The codebase shows mature patterns with proper versioning (VERSION file), upgrade migrations (cmd/upgrade.go), and configuration management (config.toml.sample).
Low-to-medium risk for stable use. Single primary maintainer (knadh) is a potential concern for long-term maintenance, but the project is backed by Zerodha's resources. The AGPLv3 license requires any derivative/hosted modifications to share source code. No obvious red flags in test structure, but frontend test coverage is unclear from the file list. PostgreSQL dependency is non-negotiable and introduces operational responsibility.
Active areas of work
Active development with recent CI pipelines (build-sanity.yml, nightly.yml, release.yml), feature flags for experimental work (hodor-review.yml), and organized issue templates (CONTRIBUTING.md references developer setup). The .goreleaser.yml and .goreleaser-nightly.yml configs suggest regular release cycles with multi-platform binaries.
🚀Get running
Clone, install Go, build frontend, run database setup: git clone https://github.com/knadh/listmonk && cd listmonk && make build (Makefile present). Then ./listmonk --new-config && ./listmonk --install to initialize PostgreSQL, then ./listmonk to start on http://localhost:9000.
Daily commands:
make build compiles the Go binary and bundles Vue frontend. ./listmonk --new-config generates config.toml, edit database/SMTP settings. ./listmonk --install runs migrations on PostgreSQL. ./listmonk starts the server (default :9000). For development: check dev/README.md and .devcontainer/devcontainer.json for containerized dev setup.
🗺️Map of the codebase
cmd/main.go— Application entry point and initialization; every contributor must understand the startup flow and CLI architecture.cmd/handlers.go— Core HTTP route definitions and middleware setup; essential for understanding how requests are dispatched and processed.cmd/manager_store.go— Database abstraction layer and core data access patterns; critical for understanding how listmonk interacts with PostgreSQL.cmd/campaigns.go— Campaign management logic including creation, scheduling, and execution; the heart of the newsletter sending functionality.cmd/subscribers.go— Subscriber CRUD operations and list management; foundational for user data handling and segmentation.config.toml.sample— Configuration schema and defaults; required reading for understanding all available runtime options and deployment.Dockerfile— Single-binary production build definition; essential for understanding deployment model and runtime dependencies.
🛠️How to make changes
Add a New Admin API Endpoint
- Define the handler function in the appropriate domain file (e.g., cmd/campaigns.go for campaign endpoints, cmd/subscribers.go for subscriber endpoints) (
cmd/campaigns.go or cmd/subscribers.go) - Register the HTTP route in cmd/handlers.go within the router setup section, typically in an
r.Post(),r.Get(), orr.Put()call (cmd/handlers.go) - Add authentication/authorization checks using the middleware patterns already established in cmd/auth.go (check user roles/permissions) (
cmd/auth.go) - Implement database queries using the manager_store pattern (e.g.,
m.GetCampaign(),m.UpdateSubscriber()) (cmd/manager_store.go) - Return JSON response using the existing error/success response structures from cmd/utils.go (
cmd/utils.go)
Add a New Campaign Field or Subscriber Metadata
- Identify if a database schema migration is needed; create or modify the schema migrations referenced in cmd/init.go (
cmd/init.go) - Update the database query builders in cmd/manager_store.go to SELECT, INSERT, or UPDATE the new field (
cmd/manager_store.go) - Expose the field in API responses via cmd/campaigns.go or cmd/subscribers.go handlers (
cmd/campaigns.go or cmd/subscribers.go) - Add validation logic in the same handler file to ensure data integrity before persisting (
cmd/campaigns.go or cmd/subscribers.go)
Add Support for a New Email Messenger/Provider
- Create or extend provider configuration in cmd/settings.go to store provider credentials and settings (
cmd/settings.go) - Implement the messenger interface (typically in a new handler or within cmd/handlers.go) that sends emails via the external provider API (
cmd/handlers.go) - Integrate the messenger into cmd/campaigns.go execution logic where emails are queued and dispatched during campaign sends (
cmd/campaigns.go) - Add bounce/delivery tracking callbacks in cmd/bounce.go to handle provider-specific webhook events (
cmd/bounce.go) - Expose provider configuration endpoints via cmd/admin.go for dashboard integration (
cmd/admin.go)
Add a New Bulk Import Format or Data Source
- Extend the import handler in cmd/import.go to parse and validate the new format (CSV, JSON, etc.) (
cmd/import.go) - Reuse existing transaction patterns from cmd/tx.go for atomic batch inserts of imported subscribers (
cmd/tx.go) - Add data transformation/normalization logic in cmd/import.go before persisting to database via cmd/manager_store.go (
cmd/import.go) - Expose import API endpoint in cmd/admin.go with progress tracking and validation feedback (
cmd/admin.go)
🔧Why these technologies
- Go (Golang) — Single compiled binary for easy deployment, fast execution, excellent concurrency primitives for managing bulk email operations and background job queues.
- PostgreSQL — Relational model suits structured campaign, subscriber, and list data; supports complex queries for segmentation; ACID guarantees for data consistency in email delivery tracking.
- SMTP/Messenger abstraction — Decouples email sending logic from specific providers, enabling multi-provider support and failover capabilities.
- REST API + JSON — Standard web interface for programmatic access and dashboard integration; language-agnostic client support.
- Docker + docker-compose — Simplifies deployment (single binary + PostgreSQL) for self-hosted use; enables quick local development setup.
⚖️Trade-offs already made
-
Single monolithic Go binary instead of microservices
- Why: Simplifies deployment, reduces operational complexity, and aligns with the self-hosted use case where a single machine/container is common.
- Consequence: Limited horizontal scaling for specific components (e.g., email queue workers), but acceptable for most single-instance deployments.
-
PostgreSQL as the only data store (no Redis cache layer visible in core code)
- Why: Reduces external dependencies and deployment complexity; PostgreSQL is sufficiently performant for typical newsletter workloads.
- Consequence: High database load during large campaign sends; no distributed caching for cross-instance state (acceptable for single-instance architecture).
-
Synchronous campaign execution model with async email dispatch queuing
- Why: Simplicity: campaigns are triggered immediately upon user request; emails are queued and sent asynchronously in background.
- Consequence: If the queue backs up, email delivery may be delayed; requires monitoring and tuning of worker concurrency.
-
In-process
- Why: undefined
- Consequence: undefined
🪤Traps & gotchas
PostgreSQL must be running and accessible before --install or runtime (no SQLite fallback). config.toml requires valid SMTP credentials to send—misconfiguration silently queues unsent emails. Database migrations in cmd/upgrade.go are idempotent but you must run them explicitly; there's no auto-migration on startup. Frontend assets are embedded in the binary at compile time—frontend changes require a full rebuild. The bouncing logic (cmd/bounce.go) requires mailbox configuration to process bounce responses. Role-based access control (cmd/roles.go, cmd/auth.go) may have subtle permission edge cases.
🏗️Architecture
💡Concepts to learn
- Email Queue Management — listmonk's reliability depends on a persistent queue (cmd/manager_store.go) that decouples campaign submission from actual SMTP sending, allows retries, and tracks delivery state—essential for high-volume mailing
- Bounce Handling & Feedback Loops — cmd/bounce.go processes bounce notifications to automatically remove invalid addresses—critical for deliverability and reputation management
- Database Migrations & Versioning — cmd/upgrade.go implements idempotent migrations to evolve the PostgreSQL schema safely across versions—a pattern every self-hosted app must handle
- Server-Sent Events (SSE) or Webhook Callbacks — Likely used for real-time campaign progress updates in the dashboard and for notifying external systems of delivery events
- Role-Based Access Control (RBAC) — cmd/roles.go and cmd/auth.go implement multi-user permissions—required for shared team workflows and compliance in production deployments
- Template Rendering & Variable Substitution — cmd/templates.go handles dynamic email body generation (inserting subscriber names, unsubscribe links, custom fields)—core to personalization without XSS vulnerabilities
- PostgreSQL JSON/JSONB for Metadata — Likely used for storing dynamic subscriber attributes, campaign settings, and delivery metadata—enables flexibility without schema bloat
🔗Related repos
mailgun/mailgun-go— Email delivery SDK that listmonk likely uses or could integrate with for SMTP abstraction and bounce handlingvolatiletech/sqlc— Type-safe SQL code generation tool—useful for hardening the database layer in cmd/manager_store.go and cmd/campaigns.gosendgrid/sendgrid-go— Alternative email backend—users often compare listmonk as a self-hosted replacement for SendGridmattermost/mattermost-server— Similar self-hosted, Go-based communication platform with PostgreSQL backend and Vue frontend—architectural patterns are comparablenextcloud/server— Open-source self-hosted suite with similar deployment model (single binary/container, PostgreSQL, Vue UI) and AGPLv3 license strategy
🪄PR ideas
To work on one of these in Claude Code or Cursor, paste:
Implement the "<title>" PR idea from CLAUDE.md, working through the checklist as the task list.
Add API endpoint documentation for transactional emails in docs/docs/content/apis/transactional.md
The transactional.md file exists in the file structure but is listed without content. Given that listmonk is a mailing list manager with transactional email capabilities (evidenced by cmd/tx.go), this API documentation is likely incomplete or missing endpoint specifications, request/response examples, and integration guides. This is high-value for new users trying to integrate transactional features.
- [ ] Review cmd/tx.go to identify transactional email handlers and endpoints
- [ ] Document all transactional email API endpoints with method, path, and parameters
- [ ] Add request/response examples for common use cases (e.g., sending transactional emails, tracking)
- [ ] Include authentication requirements and rate limiting information
- [ ] Add code examples in popular languages or cURL commands
Add GitHub Actions workflow for running Go unit tests on PRs and commits
The repo has build-sanity.yml, nightly.yml, and release.yml workflows, but there's no dedicated test workflow in .github/workflows/ that runs go test on every PR. Given that listmonk is a Go application with multiple cmd/ handlers (admin.go, campaigns.go, subscribers.go, etc.), automated test execution would catch regressions early and ensure code quality before merging.
- [ ] Create .github/workflows/go-tests.yml with steps to run go test ./... on push and PRs
- [ ] Include database setup (PostgreSQL) for integration tests if needed
- [ ] Configure test coverage reporting (e.g., with codecov)
- [ ] Set Go version from .go-version file dynamically
- [ ] Add notifications for test failures
Add integration tests for core subscriber and list management operations in cmd/
The repo has dedicated files for subscribers.go, lists.go, campaigns.go, and other handlers, but there are no visible test files (no *_test.go files shown). For a production-grade self-hosted app handling critical operations like subscriber imports, list creation, and campaign execution, integration tests would catch bugs in data persistence and business logic before release.
- [ ] Create cmd/subscribers_test.go with tests for subscriber CRUD operations and validation
- [ ] Create cmd/lists_test.go with tests for list creation, deletion, and subscriber assignment
- [ ] Create cmd/import_test.go with tests for CSV import parsing and batch operations
- [ ] Use a test database fixture or Docker container for each test suite
- [ ] Verify transaction handling (cmd/tx.go) in error scenarios
🌿Good first issues
- Add unit tests for cmd/subscribers.go subscriber list filtering and segmentation logic—currently no visible test files for core business logic.
- Document the bounce handling flow (cmd/bounce.go) in CONTRIBUTING.md with examples—complex feature with no inline developer guide.
- Create a validation helper for config.toml in cmd/utils.go to catch common misconfigurations (invalid SMTP host, missing DB credentials) at startup with clear error messages.
⭐Top contributors
Click to expand
Top contributors
- @knadh — 54 commits
- @dependabot[bot] — 19 commits
- @mr-karan — 5 commits
- @Bjornftw — 3 commits
- @Ggpsv — 2 commits
📝Recent commits
Click to expand
Recent commits
d359bf9— Prevent confirmed subscriber becoming unconfirmed when re-subscribing. Closes #2987 (#2996) (blu3id)b502186— Skip empty config fields in Postgres connect DSN. Closes #2275. (knadh)ee099f6— Add template expression support to campaign custom headers like subjects. (knadh)d416cc5— Bump postcss from 8.4.49 to 8.5.12 in /frontend (#3020) (dependabot[bot])5220619— Remove accidentally (!) committed local lib path. Ughhhhh ;( (knadh)cb19ceb— Add ability to export campaign clicks/views from the Maintenance admin UI. (knadh)b77b571— Add ability to set a retry delay for failed SMTP pushes. Closes #2309. (knadh)d89b410— Fix users withlists:get_alland:getseeing '*Unknown' mask on list names. Closes #3016. (knadh)78f6af0— GenerateMessage-Idheader withFromdomain. (knadh)52dc85c— Bump follow-redirects from 1.15.11 to 1.16.0 in /frontend (#3003) (dependabot[bot])
🔒Security observations
Failed to generate security analysis.
LLM-derived; treat as a starting point, not a security audit.
👉Where to read next
- Open issues — current backlog
- Recent PRs — what's actively shipping
- Source on GitHub
Generated by RepoPilot. Verdict based on maintenance signals — see the live page for receipts. Re-run on a new commit to refresh.