Last Updated: 2026-06-28 Status: Active
Complete internal reference for the Vlozi newsletter system. Everything a developer needs — architecture, schema, API, billing, templates — lives here or is one link away.
1. What Is This System?
A multi-tenant email marketing engine built as a Cloudflare Worker. Tenants create templates, manage subscribers, build segments, and fire campaigns. Blog publish events auto-trigger newsletter sends via a binding configuration.
| Codebase | Location | Role |
|---|---|---|
| newsletter-service | apps/newsletter-service |
CF Worker — owns all newsletter data, send queue, scheduled campaigns |
| seller-dashboard | apps/seller-dashboard/src/modules/newsletter |
Next.js UI — template editor, subscriber management, campaign builder |
| communication-service | apps/communication |
CF Worker — email provider abstraction (Resend); handles bounce webhooks |
2. Start Here By Role
| Role | Reading Path |
|---|---|
| New to newsletter | This doc → architecture.md → database.md |
| Backend / newsletter-service work | architecture.md → database.md → api-spec.md → permissions.md |
| Adding or changing templates | templates.md — authoritative variable contract |
| Billing or credit questions | billing.md |
| Known issues / next sprint | backlog.md |
3. Doc Index
| Doc | What It Covers |
|---|---|
architecture.md |
System diagram, services, request flows, queue pipeline, blog integration |
database.md |
All 8 tables — columns, indexes, business rules, migration history |
api-spec.md |
Every route — method, path, auth, request/response shapes |
templates.md |
Template variable contract, HTML escaping, snapshot semantics, render version |
billing.md |
Credit model (1cr = 5 emails), upfront charge, idempotency, failure modes |
permissions.md |
Every permission string, what it gates, role bundles |
backlog.md |
Known bugs, gaps, and improvement scope with fix guidance |
4. Codebase Quick Map
apps/
├── newsletter-service/ CF Worker · port 8795 (local)
│ ├── src/index.ts Entry point — Hono app, queue handler, DO export
│ ├── src/scheduler.ts CampaignScheduler Durable Object (alarm-based scheduling)
│ ├── src/db/schema.ts 8 tables across 9 migrations
│ ├── src/routes/ subscribers · campaigns · templates · sections · segments
│ │ blog-binding · public · internal · internal-blog
│ ├── src/lib/campaign-send.ts Core send logic: resolve recipients → charge credits → enqueue
│ ├── src/lib/render.ts Template variable substitution ({{ subscriber.name }}, etc.)
│ ├── src/lib/hmac.ts HMAC token sign/verify for confirm & unsubscribe URLs
│ └── drizzle/ 0000–0008 SQL migrations
│
└── seller-dashboard/
└── src/modules/newsletter/ Campaign builder, template editor, subscriber dashboard5. Four Things Every Dev Must Know
1. Sends go through a Cloudflare Queue, not inline.
fireCampaign() enqueues one job per subscriber. The queue consumer calls comms-service for each delivery. This means a 50k-recipient campaign is 50k queue messages — jobs are processed at CF Workers throughput limits, not in a loop inside a single request.
2. Credits are charged upfront, before enqueue.
Math.ceil(recipients / 5) credits are debited from Core DB before a single message is queued. If the charge fails (402), no emails are sent and nothing is in the queue. If send fails after enqueue, credits are NOT automatically refunded — this is intentional (comms-service was called).
3. Every subscriber has a global suppression check.
nl_suppression_list is cross-tenant. A hard bounce on tenant A suppresses the address for all tenants. This is non-negotiable for inbox reputation. The recipient query always excludes suppressed addresses.
4. Blog integration is opt-in per tenant, fire-and-forget.
blog-service calls /internal/blog/post-published after a successful publish. If newsletter-service errors or no binding is configured (204), the publish still succeeds. Newsletter dispatch is a side effect — it cannot block or roll back publishing.