Phased implementation plan for the multi-tenant newsletter SaaS. Full listmonk feature parity is the target; it is delivered across phases so each phase is independently shippable. Each phase ends with passing tests and a working deployment.
See architecture.md for design detail and user-stories.md for the requirements each phase
satisfies.
- 0.1 Create the
nveloperepository; commitdocs/architecture.md,docs/user-stories.md, and this plan. - 0.2 Scaffold the three Go services (
cmd/api,cmd/worker,cmd/scheduler) with shared config loading and structured logging. - 0.3 Set up PostgreSQL,
golang-migratemigration tooling, and a React/Vite frontend skeleton. - 0.4 CI pipeline (build, test, lint), Dockerfiles for each service, and base Kubernetes/Helm manifests.
Exit criteria: all three services build and start; CI is green; a migration applies cleanly.
- 1.1 Control-plane schema:
tenants,platform_users,platform_user_tenants. - 1.2 RLS pattern: per-request transaction helper that runs
SET LOCAL app.tenant_id; connect as a non-superuser, non-BYPASSRLSrole. - 1.3 Tenant resolution middleware (path
/t/{slug}/...) with a cross-check against the authenticated session. - 1.4 Platform signup/login, tenant creation, and team invites.
- 1.5 Automated tests proving cross-tenant isolation — tenant A cannot read or write tenant B's rows even when an application-level filter is omitted.
Exit criteria: a user can sign up, create a tenant, and invite a teammate; isolation tests pass. (Satisfies Epic A.)
- 2.1 Tenant-plane schema:
lists,subscribers,subscriber_lists,roles,users,sessions,settings— each withtenant_id+ RLS. - 2.2 Tenant RBAC: user-level and per-list roles, permission strings, scoped API keys, 2FA (TOTP).
- 2.3 Subscriber and list CRUD, custom JSON attributes, query/segment-based selection.
- 2.4 CSV/ZIP subscriber import (with upsert) and export.
Exit criteria: a tenant user can manage lists and subscribers, import/export, and the RBAC gates work. (Satisfies Epic D, part of Epic H.)
- 3.1 River integration; job queue definitions and worker registration. Three new job
kinds (
domain.verify,campaign.start,campaign.batch) on a dedicatedsendingqueue. - 3.2
sending_domainsschema; Postbox domain provisioning and thedomain.verifypolling job, with a scheduler recovery sweep that re-arms lost verification jobs. - 3.3 Postbox SES-compatible messenger with AWS SigV4 request signing.
- 3.4 Redis-coordinated per-tenant and global sliding-window rate limiting.
- 3.5 Templates and campaigns schema; the
campaign.start→campaign.batchsend pipeline with per-recipient dedup/resumability; open-pixel and click-tracking link generation served from public, tenant-resolving endpoints. - 3.6 Transactional
txAPI endpoint authenticated by a scoped API key.
Exit criteria: a tenant can verify a domain and send a campaign through Postbox with tracking. (Satisfies Epic C, core of Epic E and Epic F.) — met.
Delivered notes / divergences from the original outline. The
txendpoint and the sending-domain/campaign routes use a small set of new permission strings (sending:*,campaigns:*,transactional:send) added to the IAM catalogue rather than reusing existing scopes. Usage events (usage_events) are deferred to Phase 5, where the usage table and rollup actually live; the send pipeline does not emit them yet. SPF and DMARC records are composed by the platform (Postbox returns only DKIM tokens).
- 4.1 Postbox bounce/complaint webhook ingestion with signature verification.
- 4.2
suppression_list, configurable bounce actions, and pre-send suppression checks. - 4.3 Campaign analytics (opens/clicks/bounces/complaints) and dashboard materialized views.
Exit criteria: bounces/complaints are attributed and suppressed automatically; analytics and dashboard render. (Completes Epic F.)
- 5.1
plans,tenant_subscriptions,invoices,invoice_line_items,payment_attempts; thePaymentGatewayinterface with a deterministicMockGateway. - 5.2 In-house subscription engine: lifecycle state machine,
billing.sweep/billing.chargejobs, invoice generation, and dunning (retries → suspension). - 5.3
usage_events, theusage.rollupjob, andusage_counters. - 5.4 Quota enforcement at campaign start and transactional send (
blockvsmeteroverage modes); tenant suspension on payment failure.
Exit criteria: a tenant can subscribe to a plan, recurring renewals charge through the mock gateway, usage is metered, quotas are enforced, and payment failure runs dunning then suspends sending. (Satisfies Epic B.) Real Russian payment-provider integration is a later phase.
- 6.1 Public subscription page, double-opt-in flow, and preference management —
server-rendered Go
html/templatepages under/t/{slug}/subscribe/{page-slug},/c/{token},/p/{token},/u/{token}; confirmation email delivered via the durableoptin.sendRiver job from the page's verified sending domain. - 6.2 Campaign archive and RSS feed with per-tenant branding/CSS — sent campaigns
marked archive-visible appear on
/t/{slug}/archive,/t/{slug}/archive/{id}, and/t/{slug}/feed.xml. Tenant branding (logo, primary colour, sanitised custom CSS) is applied via a single.nv-public-scoped<style>block. - 6.3 Media library backed by S3-compatible object storage — new
mediabounded context, RLS-protectedmedia_assetsmetadata, tenant-prefixed unguessable capability URLs (media/{tenantID}/{assetID}/{filename}); admin endpointsGET/POST /t/{slug}/api/mediaandDELETE .../{id}.
Exit criteria: subscribers can self-serve via public pages; media uploads work. (Satisfies Epic G, completes Epic H.)
- 7.1 Visual email editor — TipTap-based authoring surface with bubble
menu, slash command, multi-column layouts, merge-tag picker, media-library
/ drag-and-drop image insertion, code view, and per-row theming pinned or
inherited from tenant branding. Rendering runs on a new
TanStack Start + Nitro BFF tier that converts the structured
VisualDocto email-ready HTML + plain text via@react-email/componentsand forwards to Go for validation, sanitization (bluemonday), and persistence. Subscriber-fields registry + send-time merge-tag substitution wired through the batch worker; audit-log rows emitted for every visual-save and registry mutation. Seespecs/014-visual-email-editor/and § 9 (Frontend) for the BFF tier split. - A/B testing and advanced segmentation remain ahead.
- 7.2 Platform admin console and audit-log UI.
- 7.3 Load testing; security review (RLS, SigV4, webhook signatures, API keys);
observability (metrics, tracing, alerting — Phase 7 added the first
/metricsendpoints on both Go API and BFF). - 7.4 Production Kubernetes rollout, backups, and operational runbooks.
Exit criteria: full listmonk feature parity reached; security and load reviews signed off; production-ready. (Completes Epic E and Epic A.)
- Isolation: automated tests create two tenants and assert, via RLS, that tenant A cannot read or write tenant B's rows even with a missing application-level filter.
- Sending: end-to-end test sends a campaign through Postbox in a staging account; confirm DKIM-signed delivery, open/click tracking, and bounce/complaint attribution.
- Domains: add a real test domain; verify DKIM/SPF/DMARC detection and status transitions.
- Billing: subscribe a tenant and drive renewals through the
MockGateway; simulate usage; confirm quota enforcement, overage line items, and the dunning → suspension path on a declined charge. - Jobs: kill a worker pod mid-campaign; confirm River retries and the campaign resumes without duplicate sends.
- Load: simulate concurrent campaigns across multiple tenants; confirm per-tenant fairness and that global rate limits protect the Postbox account.
- Standard, every phase:
go test ./..., frontend tests, lint, and a clean migration apply.
| Risk | Mitigation |
|---|---|
| RLS misconfiguration leaks data | Non-BYPASSRLS role; isolation tests in CI; explicit app-level filters as belt-and-suspenders |
| Postbox API differs from AWS SES in edge cases | Thin messenger abstraction; integration tests against a real Postbox staging account |
| One large tenant starves others when sending | Per-tenant River queues/priorities; Redis per-tenant rate limits |
| Quota races across worker pods | Centralize counters in Redis; enforce at campaign start, not just per-message |
| Full parity in v1 is large | Phased delivery — each phase ships; parity is the Phase 7 end state, not a Phase 1 gate |