
Mailgun's documentation is explicit about domain-level reputation thresholds — bounce rate above 5%, complaint rate above 0.05%, domain suspended — and silent on one question every multi-tenant operator needs answered first: if a tenant's subaccount is suspended, does the master account survive? Documentation implies per-domain isolation. It does not guarantee it. That unanswered sentence shaped the primary architecture of Sendora: per-tenant sending domains as the reputation isolator, because no vendor would commit to the layer the platform needed to trust. Sendora is a multi-tenant email marketing platform built for the buyer who wants to run email for paying tenants as their own branded SaaS — lists, campaigns, drip sequences, subscription forms, and click-primary reporting, delivered as deployable source.
Starting point
The functional target was Mailchimp's campaign loop: contact list into segment, campaign editor to HTML, bulk send, per-campaign report. Not the CRM, not the landing-page builder, not SMS. The assumption was that those bolted-on surfaces explain why long-time Mailchimp users describe the interface as denser without becoming more useful — and that stripping them out would let the core loop breathe.
Tone reference came from a different source. Buttondown proves that restraint and editorial clarity sell in email software, even without full list-and-campaign capability. The target was more capable than Buttondown, same honesty register.
The design direction was chosen from four explored candidates, all built around the product's comparison-led identity. The argument the product makes — this billing axis, these metrics, against these alternatives — is naturally a side-by-side. The winning direction leaned into that: the comparison table as the landing's primary structural gesture, density as the argument's own form.
The product is multi-tenant from the data model up. The buyer deploys it, sets their own pricing, and runs it as a branded SaaS. Each tenant gets their own organization, contact base, sending domain, and plan limits. The platform operator gets a cross-organization console with full visibility and control over every tenant entity.
Reading the market
The incumbents price multi-tenant capability as enterprise. Adding sub-accounts, isolated reputation tracking, and per-account suppression lists reaches into their highest tier — effectively out of reach for a buyer building a product for dozens of SMB tenants rather than managing a single brand. One major ESP's verified sub-account capability starts at a price point that makes it a structural non-option for anything below agency scale.
End users of the major platforms describe a related but separate frustration: per-contact billing charges for addresses that cannot legally be mailed — the unsubscribed, the bounced, the inactive — while the actually addressable list stays flat.
The buyer's real wedge is upstream of both. It is the billing axis itself. Whether to charge tenants per send, per contact, or on a flat tier is normally a decision the incumbent makes and hands down. A buyer who owns the platform owns that decision. The wholesale cost of the sending infrastructure becomes an operational variable they manage. And the metrics their tenants see can be honest by default — which incumbents running volume-driven models have structural reasons not to prioritize.
The honest structural comparison is listmonk, an open-source single-tenant platform that covers the same feature set under AGPL. The SaaS layer — multi-tenant isolation, tenant billing, cross-organization monitoring, the platform operator's console — is the actual gap the product fills.
The decisions that shaped it
Reputation isolation by domain, not by vendor structure. The vendor guarantee question was resolved by de-scoping the vendor conversation: the master-account blast radius is recorded as a documented risk, not an action item, with the reasoning that no ESP publishes an explicit guarantee on it. Every protective layer that does not depend on that guarantee ships in v1. Each tenant gets a dedicated sending domain; entry tenants share a platform subdomain at zero configuration overhead. The domain carries the reputation, so a tenant sending from their own verified domain who burns their list damages that domain's standing first — while tenants still on the shared entry subdomain share its fate, which is exactly why the platform pushes every tenant toward verifying their own domain, and why the master-account blast radius stays recorded as a documented risk rather than a solved one. The platform adds its own circuit breakers above whatever the ESP provides: complaint rate triggers a warning at 0.1%, auto-pauses the organization at 0.3%, and blocks at 1%, all computed over a rolling 24-hour window. Quota is enforced before the queue, not inside the batch consumer.
Sending is a queue, not an HTTP request. A 50,000-recipient send physically exceeds the serverless execution window. At 500 recipients per ESP batch call, the dispatch chain runs to roughly 250 seconds — well past the function limit. The campaign send path writes durable queue rows first, publishes each batch to an external job queue, and returns immediately. The consumer is the only caller of the ESP batch API. Suppressed recipients are recorded as skipped rather than dropped, so the queue preserves an accurate audience snapshot at send time. An ESP outage does not lose the queue — the rows persist until the consumer succeeds.
Per-recipient click tracking moved out of the email body. A batch API call sends one HTML document to 500 people. A per-recipient click token cannot be baked into that body — the document is shared. The solution was recipient variables: the render pipeline emits a placeholder per tracked link, the dispatcher mints one stateless HMAC token per contact-URL pair, and the ESP substitutes the variable per recipient before delivery. No token table, which would otherwise require one database row per recipient per link per campaign.
Click-primary metrics, and the reason is accuracy, not deliverability. Apple Mail accounts for roughly 49% of email opens by market share (Litmus, January 2025), and Apple Mail Privacy Protection pre-fetches messages through rotating proxy servers regardless of whether the recipient viewed them. Open rates inflated by roughly 18 percentage points after MPP rolled out (Omeda study). No official Apple proxy UA list exists to filter machine opens reliably. The product reports clicks by default. When open tracking is enabled per organization, the report labels opens as not reliably measurable and applies timing-based heuristics to downweight machine signals. The rationale is signal accuracy and recipient privacy — explicitly not deliverability. A tracking pixel has negligible impact on inbox placement, and claiming otherwise would undercut the data-honest positioning the product is built on.
The ESP is configuration, not code. API endpoints, versions, and rate limits live in a platform settings table rather than in source. This was a deliberate decision against using the vendor's own SDK, which carries its own endpoint table and would become a second invisible source of truth for anything the admin console should own. The send path uses a direct fetch client that honours the admin-editable base URL verbatim. The adapter is swappable without a code change — the sending vendor is an operational decision, not an architectural commitment.
Suppression at two scopes for two different facts. Hard bounces and spam complaints suppress platform-wide: an address that bounced or complained is bad for every tenant, not just the one who mailed it. Unsubscribes suppress per organization: a contact's withdrawal of consent belongs to the specific tenant they opted into. Both layers are checked on every send path — at enqueue and inside the batch consumer — and neither can be feature-gated.
What fought back
The suppression-check function turned out to be reachable without authentication. A privilege grant on the campaign-metrics function was written to restrict access to the service role. It removed nothing: a standing default-privilege configuration in the database had already made the function callable by roles that include unauthenticated callers. Live verification confirmed an unauthenticated HTTP call returned a 200. The exploit chain was real: forged events into the table the deliverability reader monitors would trigger the complaint-rate ladder and auto-pause a competing tenant's sending — one tenant pausing another's sending through a forged metric event. The fix revoked the default grant explicitly. Re-verification at the access-control layer confirmed service-role-only privilege. Re-verification at runtime confirmed unauthenticated and authenticated non-service calls return permission-denied errors while service-role calls continue to work. The companion finding — the suppression-check function itself had an equivalent exposure and could be used to read cross-tenant suppression lists — was closed in the same pass.
The webhook receiver had to fork. The baseline webhook handler verifies a signature over a timestamp, event ID, and body transmitted in request headers. The ESP's webhooks HMAC a timestamp and token that appear inside the JSON body. Verification had to be delegated to the provider adapter. Every other property of the baseline receiver was preserved: the body size cap, fail-closed behaviour on a missing signing secret, and routing-layer replay protection.
QStash latency was not measured, and that is the honest answer. The job queue credentials were blank during testing by design — the environment could not exercise real dispatch latency, and fabricating a p50 or p95 figure would retire a real operational risk on false evidence. What was verified instead: the queue row is durable when dispatch fails, the API response describes the deferral accurately, and the job callback endpoint refuses unsigned requests with a 503. An unsigned callback would allow any caller to drive the batch send path. "The key isn't set yet" is not grounds for accepting unauthenticated sends.
Pausing a sequence was a one-way door. A bug in the enrollment runner meant that pausing a sequence left every active enrollment in a state the runner would never re-select. Re-activating the sequence did not recover them. The fix changed the runner to include paused enrollments in its selection and un-park them on re-activation. Disabling a sequence now exits all live enrollments with a recorded exit reason. The admin confirmation copy was rewritten to reflect the actual semantics: pause resumes from the same step, disable is permanent. Before the fix, neither was true.
What shipped
A complete multi-tenant email marketing platform with the full campaign loop intact: contacts and segments with CSV import, custom fields, and natural-key deduplication; a block-based campaign editor with preview and test sends; automated drip and welcome sequences; subscription forms with double opt-in; per-campaign metrics with click-primary reporting; unsubscribe and suppression handling; reusable email templates; and guided BYO sending-domain setup with SPF and DKIM verification.
Compliance surfaces are not feature-gated. Suppression checks run on every send path regardless of tenant plan. Double opt-in and unsubscribe handling are always enforced. Unsubscribed and bounced contacts never count toward send quotas.
The platform operator console mirrors every entity across organizations — campaigns, contacts, sequences, forms, sending domains, deliverability state — with full control. A sweep of all 53 routes at launch found every protected page redirecting unauthenticated callers correctly and every API endpoint clean of server errors across 65 probes.
The tenant-facing pricing tiers the platform ships with are per-send volume, seeded as defaults in the billing database and adjustable from the admin console without touching source. The billing axis — whether to charge tenants per send, per contact, or by some other measure — is the operator's decision to make, not a constraint inherited from upstream.
