The problem
Sending one email is trivial. Sending two hundred thousand is a rate-limiting, reputation and idempotency problem where every mistake is irreversible - you cannot un-send, and a burst that trips a provider’s threshold damages a sending reputation that takes weeks to rebuild.
ECamp is a multi-tenant platform for that, and it doubles as my Master’s thesis project on agentic campaign management.
Provider-agnostic by construction
Every organisation configures its own sending path - AWS SES or plain SMTP - and those credentials are encrypted at rest, per organisation. The application never talks to a provider directly; it talks to a sender interface. Swapping SES for SMTP is a configuration change, and a provider outage is a failover rather than an incident.
Rate limiting in three layers
A single global limiter is not enough, because the constraints are genuinely different at different scopes:
- Provider quota - the hard ceiling SES gives you, which you must never cross.
- Per-organisation throughput - so one tenant’s blast cannot consume the shared budget.
- Per-recipient-domain pacing - because large mailbox providers throttle bursts from a single sender, and the polite rate is well below the provider ceiling.
All three are enforced with Redis counters that Celery workers consult before claiming a batch. A worker that cannot claim capacity yields rather than sleeps, so it stays available for a campaign that does have budget.
Delivery you can stop
Campaigns are executed in batches, and each batch is a Celery task that records per-recipient state before it hands anything to the provider. That state is what makes pause and resume real: pausing sets a flag that batches check on entry, and resuming picks up from recorded state instead of restarting the campaign.
Every send is keyed on a campaign/recipient pair. A retried task that already sent will not send again - with two hundred thousand recipients, “at-least-once plus idempotent” is the only combination that ends well.
Feedback loop
SES webhooks stream bounces, opens, clicks and complaints back into the platform. Hard bounces suppress an address immediately; complaints suppress and flag the source list. Delivery status pushes to the dashboard over WebSocket, so a campaign in flight is something you watch rather than refresh.
Automation sits on top: immediate, delayed and scheduled triggers driven by Celery Beat, so a signup can start a sequence without a human in the loop.
The agentic layer
This is the thesis part. Contact management is driven by natural language and voice through a Gemini-powered agent - “add everyone who opened the March campaign to a new segment and tag them warm.”
The design decision that makes it safe: the agent never writes to the database. It proposes structured operations against a declared schema. The application layer validates them, applies the same permission checks a human request would face, and executes. A misunderstood instruction becomes a rejected proposal, not a corrupted contact list.
That boundary is the whole difference between an agent that is useful in production and a demo you would never point at real data.
What it demonstrates
Careful backend work under a genuinely unforgiving constraint - irreversible side effects at volume - with an AI layer that is treated as a proposer, not an authority.