mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-11 00:05:01 +00:00
site(sending): full redesign in the warmup/campaigns pattern
- Hero: HeroAtmosphere with 'Many mailboxes. Many workers. Many IPs.' headline, eyebrow chip 'Distributed across many machines, many IPs', two CTAs. - Floating worker fleet card: live topology SVG on the left (control plane on left, 5 workers on right with IPs, animated send dots traveling) plus a worker table on the right (id, ip, region, mailbox count, status chip). Footer flags w-03 hitting the concentration threshold of 10 mailboxes. - Control plane vs execution plane: 2-card side-by-side with colored top rules. Each card lists the 4 things that plane is responsible for, with cmd/backend cmd/consumer and cmd/worker file labels. - Per-mailbox math: 'Worker capacity is the sum of its mailboxes' with a Go code block (worker_scheduler.go) showing the actual budget = sum(mailbox caps) + concentration warning emit. - Message lifecycle: 6 numbered editorial rows (scheduled, dispatched, sent, acknowledged, tracked, reconciled) each with a code-style signature (kafka.publish, provider.send, etc) and prose. - Providers: 3 cards with brand-colored top rules (Gmail red, Outlook blue, BYOS sky) and verifiable detail. - Limits: 2-col spec sheet (cap, gap, concentration, worker budget, send window, dependencies) with source-file mono footer. - FAQ: 4 engine questions with the same grid-rows accordion.
This commit is contained in:
+405
-143
@@ -1,189 +1,451 @@
|
||||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Cloud from '../components/Cloud.astro';
|
||||
import HeroAtmosphere from '../components/HeroAtmosphere.astro';
|
||||
import Icon from '../components/Icon.astro';
|
||||
import CTA from '../components/CTA.astro';
|
||||
|
||||
// Sourced from CLAUDE.md + cmd/worker/main.go + internal/scheduler/email_scheduler.go.
|
||||
|
||||
const limits = [
|
||||
{ k: 'Per-mailbox cold cap', v: '100 / day', why: 'Hard cap before provider anomaly heuristics start scoring the mailbox.' },
|
||||
{ k: 'Per-mailbox gap', v: '600 sec', why: 'Minimum spacing between sends from the same mailbox. Matches human cadence.' },
|
||||
{ k: 'Worker concentration', v: '~10 mailboxes', why: 'Suggested ceiling for a single worker. Past this we surface a warning and add workers.' },
|
||||
{ k: 'Worker daily budget', v: 'Σ mailbox caps', why: 'No flat per-worker ceiling. Volume scales by adding mailboxes, not by cranking one.' },
|
||||
{ k: 'Send window', v: 'Recipient TZ', why: 'Business hours in the recipient time zone, not the sender\'s.' },
|
||||
{ k: 'Worker dependencies', v: 'No Postgres', why: 'Workers only open Kafka, Redis, KMS, DynamoDB and S3 clients. SQL stays in the control plane.' },
|
||||
];
|
||||
|
||||
// Topology mock for the floating panel below the hero
|
||||
const workers = [
|
||||
{ id: 'w-01', region: 'us-east-1a', ip: '192.0.2.14', mailboxes: 8, status: 'healthy' },
|
||||
{ id: 'w-02', region: 'us-east-1b', ip: '192.0.2.42', mailboxes: 10, status: 'healthy' },
|
||||
{ id: 'w-03', region: 'us-east-1c', ip: '192.0.2.71', mailboxes: 11, status: 'watch' },
|
||||
{ id: 'w-04', region: 'eu-west-1a', ip: '198.51.100.6', mailboxes: 9, status: 'healthy' },
|
||||
{ id: 'w-05', region: 'eu-west-1b', ip: '198.51.100.18', mailboxes: 6, status: 'healthy' },
|
||||
];
|
||||
|
||||
const tones = (s: string) => {
|
||||
if (s === 'healthy') return { dot: 'bg-emerald-500', chip: 'bg-emerald-50 text-emerald-700' };
|
||||
if (s === 'watch') return { dot: 'bg-amber-500', chip: 'bg-amber-50 text-amber-700' };
|
||||
return { dot: 'bg-rose-500', chip: 'bg-rose-50 text-rose-700' };
|
||||
};
|
||||
|
||||
const providers = [
|
||||
{ name: 'Google Workspace', tag: 'Gmail · OAuth 2.0', body: 'Gmail API for send. Threaded replies preserved through In-Reply-To and References headers. Postmaster Tools reputation surfaced where the domain is enrolled.' },
|
||||
{ name: 'Microsoft 365', tag: 'Outlook · OAuth', body: 'Graph API for send with shared-mailbox support. IMAP / EWS for sync. Tenant-scoped admin consent flow.' },
|
||||
{ name: 'Custom SMTP / IMAP', tag: 'BYOS · TLS', body: 'Bring your own server. TLS, STARTTLS, app passwords, and OAuth where the provider supports it. iCloud and Zoho work first-class.' },
|
||||
];
|
||||
|
||||
const faq = [
|
||||
['How does worker assignment happen?', 'Mailboxes are pinned to a worker on first connect and stay there for stability. The control plane can rebalance live by moving the mailbox to a different worker without dropping campaigns or warmup.'],
|
||||
['What happens if a worker crashes?', 'Worker state is disposable. A replacement worker reads pending commands from the same Kafka topic on boot and resumes. No mailbox is stuck waiting on a single host.'],
|
||||
['Can a single mailbox use multiple IPs?', 'No. A mailbox lives on one worker with one IP at a time, so receiver reputation can stabilise. Migration changes the IP intentionally and is logged.'],
|
||||
['How are bursts prevented?', 'The per-mailbox 600-second gap is enforced server-side. Even if a campaign queues 100 sends at once, dispatch is rate-limited to the gap, not the queue size.'],
|
||||
];
|
||||
---
|
||||
<Layout
|
||||
title="Sending engine · Warmbly"
|
||||
description="A distributed sending engine that spreads cold mail across many mailboxes, many workers and many IPs. Per-mailbox safety caps baked in."
|
||||
>
|
||||
<!-- HERO: split layout · architecture diagram inline on the right -->
|
||||
<section class="relative isolate overflow-hidden text-white" style="background: radial-gradient(ellipse 130% 140% at 72% 28%, #0284c7 0%, #0369a1 25%, #075985 50%, #0c4a6e 80%, #0a3d5c 100%);">
|
||||
<div class="absolute top-0 right-0 w-[420px] opacity-30 pointer-events-none cloud-drift cloud-1" style="mix-blend-mode: screen;">
|
||||
<Cloud variant={5} opacity={1} />
|
||||
</div>
|
||||
<!-- ============================================================
|
||||
HERO · HeroAtmosphere
|
||||
============================================================ -->
|
||||
<section class="relative isolate overflow-hidden">
|
||||
<HeroAtmosphere />
|
||||
|
||||
<div class="container-page relative pt-16 md:pt-20 pb-20 md:pb-24 grid lg:grid-cols-[1.1fr_1fr] gap-12 items-center">
|
||||
<div>
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-white/55 font-mono mb-4">Sending engine</div>
|
||||
<h1 class="text-[36px] sm:text-[52px] md:text-[60px] font-semibold tracking-[-0.03em] leading-[1.02] text-white">
|
||||
Many mailboxes.<br/>Many workers.<br/>Many IPs.
|
||||
</h1>
|
||||
<p class="mt-5 text-[15.5px] md:text-[17px] text-white/75 max-w-xl leading-relaxed">
|
||||
Warmbly is not a glorified SMTP relay. The control plane decides what to send. The execution plane spreads it across distributed workers so no mailbox, machine or IP becomes a concentration point.
|
||||
</p>
|
||||
<div class="container-page relative pt-16 md:pt-24 pb-44 md:pb-56 text-center">
|
||||
<div class="inline-flex items-center gap-2 h-7 pl-1 pr-3 rounded-full bg-white/15 backdrop-blur ring-1 ring-white/25 text-[12px] text-white/95 shadow-[0_4px_14px_-4px_rgba(0,0,0,0.25)]">
|
||||
<span class="inline-flex items-center h-5 px-1.5 rounded-full text-[10.5px] font-semibold uppercase tracking-[0.06em] bg-white" style="color:#0369a1;">
|
||||
Sending engine
|
||||
</span>
|
||||
Distributed across many machines, many IPs
|
||||
</div>
|
||||
|
||||
<!-- Mini topology diagram -->
|
||||
<div class="relative">
|
||||
<svg viewBox="0 0 420 340" class="w-full h-auto">
|
||||
<defs>
|
||||
<linearGradient id="flowStroke" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#7dd3fc" stop-opacity="0.7"/>
|
||||
<stop offset="100%" stop-color="#7dd3fc" stop-opacity="0.1"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Control plane -->
|
||||
<rect x="20" y="140" width="120" height="60" rx="10" fill="rgba(255,255,255,0.08)" stroke="#7dd3fc" stroke-opacity="0.5"/>
|
||||
<text x="80" y="167" text-anchor="middle" fill="#7dd3fc" font-family="ui-monospace,monospace" font-size="9" letter-spacing="1.5">CONTROL PLANE</text>
|
||||
<text x="80" y="185" text-anchor="middle" fill="#fff" font-size="11">Backend · Kafka</text>
|
||||
|
||||
<!-- Workers -->
|
||||
{[60, 130, 200, 270].map((y, i) => (
|
||||
<g>
|
||||
<rect x={230} y={y} width="110" height="36" rx="8" fill="rgba(255,255,255,0.07)" stroke="#7dd3fc" stroke-opacity="0.3"/>
|
||||
<text x={285} y={y + 16} text-anchor="middle" fill="#fff" font-size="10" font-family="ui-monospace,monospace">worker-{i + 1}</text>
|
||||
<text x={285} y={y + 28} text-anchor="middle" fill="#7dd3fc" font-size="8.5">IP · {(192 + i)}.0.2.{14 + i * 3}</text>
|
||||
</g>
|
||||
))}
|
||||
|
||||
<!-- Mailboxes -->
|
||||
{[60, 130, 200, 270].map((y) => (
|
||||
<g>
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<circle cx={365 + i * 18} cy={y + 18} r="4" fill="#bae6fd" opacity={0.85 - i * 0.15}/>
|
||||
))}
|
||||
</g>
|
||||
))}
|
||||
|
||||
<!-- Flow lines -->
|
||||
{[60, 130, 200, 270].map((y, i) => (
|
||||
<g>
|
||||
<line x1="140" y1="170" x2="230" y2={y + 18} stroke="url(#flowStroke)" stroke-width="1.2"/>
|
||||
<line x1="340" y1={y + 18} x2="360" y2={y + 18} stroke="#7dd3fc" stroke-opacity="0.5" stroke-width="1"/>
|
||||
<circle r="2.5" fill="#fff" opacity="0.9">
|
||||
<animateMotion dur={`${3 + i * 0.4}s`} repeatCount="indefinite" begin={`${i * 0.6}s`}
|
||||
path={`M 140 170 L 230 ${y + 18}`}/>
|
||||
</circle>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ARCHITECTURE: 2-col explanatory grid -->
|
||||
<section class="py-16 md:py-24">
|
||||
<div class="container-page">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Architecture</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading max-w-2xl">
|
||||
Control plane decides. Execution plane sends.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15.5px] text-foreground/75 max-w-2xl">
|
||||
Backend orchestrates campaign math, mailbox assignment, sequencing. Workers execute send, sync and track. Kafka carries every command and result.
|
||||
<h1 class="mt-8 md:mt-10 text-[44px] sm:text-6xl md:text-[72px] lg:text-[80px] font-semibold tracking-[-0.04em] leading-[0.98] text-white max-w-4xl mx-auto">
|
||||
Many mailboxes.<br/>Many workers.<br/>Many IPs.
|
||||
</h1>
|
||||
<p class="mt-6 text-[17px] md:text-[19px] text-white/80 max-w-2xl mx-auto leading-relaxed">
|
||||
The control plane decides what to send. The execution plane spreads it across distributed workers so no mailbox, machine, or IP becomes a concentration point.
|
||||
</p>
|
||||
|
||||
<div class="mt-12 grid lg:grid-cols-2 gap-px bg-[color:var(--border)] rounded-[14px] overflow-hidden ring-1 ring-[color:var(--border)]" style="box-shadow: var(--shadow-sm);">
|
||||
{[
|
||||
{ icon: 'cpu', title: 'One worker per machine', body: 'Each worker has its own machine-level network identity. No NAT-induced IP concentration.' },
|
||||
{ icon: 'workflow', title: 'Per-worker Kafka topics', body: 'Commands and results route through worker-scoped topics. Migrations are transparent.' },
|
||||
{ icon: 'database', title: 'Stateless workers', body: 'Workers boot Kafka, Redis, KMS, DynamoDB, S3. They do not open a Postgres connection.' },
|
||||
{ icon: 'layers', title: 'Per-mailbox budgets', body: 'Default 50 / day cold cap. 10-minute minimum gap. Override per mailbox, per campaign, per TZ.' },
|
||||
{ icon: 'shield', title: 'Worker concentration cap', body: 'A shared worker should hold ~10 actively-sending cold mailboxes at defaults. Concentration warnings fire early.' },
|
||||
{ icon: 'clock', title: 'Business hours respect', body: 'Sends honour the recipient time zone, weekends, holidays. Set the window once.' },
|
||||
].map((s) => (
|
||||
<div class="bg-white p-6">
|
||||
<div class="w-10 h-10 rounded-[10px] bg-[color:var(--sky-1)] text-[color:var(--sky-6)] ring-1 ring-[color:var(--sky-2)] inline-flex items-center justify-center mb-4">
|
||||
<Icon name={s.icon} size={18} />
|
||||
</div>
|
||||
<h3 class="text-[15.5px] font-semibold text-heading">{s.title}</h3>
|
||||
<p class="mt-1.5 text-[13.5px] text-foreground/70 leading-relaxed">{s.body}</p>
|
||||
</div>
|
||||
))}
|
||||
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||
<a href="https://app.warmbly.com/register" class="inline-flex h-11 px-5 items-center gap-2 rounded-[10px] text-[14.5px] font-semibold bg-white hover:bg-white hover:-translate-y-0.5 hover:shadow-[0_12px_28px_-6px_rgba(0,0,0,0.3)] transition-all duration-200 ease-out shadow-[0_4px_14px_-2px_rgba(0,0,0,0.18)]">
|
||||
<span style="color:#075985;">Connect a mailbox</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#075985" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/learn/deliverability/" class="inline-flex h-11 px-5 items-center rounded-[10px] text-[14.5px] font-medium bg-white/10 backdrop-blur ring-1 ring-white/40 text-white hover:bg-white/20 hover:-translate-y-0.5 transition-all duration-200 ease-out">
|
||||
Read the handbook
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- THE MATH: code-style worker budget calculation -->
|
||||
<section class="bg-[color:var(--surface-1)] border-y border-[color:var(--border)] py-16 md:py-24">
|
||||
<div class="container-page grid lg:grid-cols-[1fr_1.1fr] gap-12 lg:gap-20 items-center">
|
||||
<div>
|
||||
<!-- ============================================================
|
||||
WORKER FLEET · floating card with the actual topology view
|
||||
============================================================ -->
|
||||
<section class="relative -mt-36 md:-mt-44 pb-16 md:pb-24 z-10">
|
||||
<div class="container-page">
|
||||
<div class="rounded-[18px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden shadow-[0_40px_90px_-30px_rgba(2,32,71,0.35),0_12px_30px_-12px_rgba(15,23,42,0.12)]">
|
||||
<!-- Header strip -->
|
||||
<div class="px-7 py-4 border-b border-[color:var(--border)] bg-[color:var(--surface-1)]/70 flex flex-wrap items-baseline justify-between gap-3">
|
||||
<div class="flex items-baseline gap-3">
|
||||
<div class="text-[13px] font-semibold text-heading">Worker fleet · live</div>
|
||||
<span class="inline-flex items-center gap-1.5 h-5 px-2 rounded-full bg-emerald-50 text-emerald-700 text-[10.5px] font-medium font-mono uppercase tracking-[0.08em]">
|
||||
<span class="w-1 h-1 rounded-full bg-emerald-500"></span>
|
||||
5 active
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 text-[10.5px] font-mono text-muted-foreground">
|
||||
<span>44 mailboxes</span>
|
||||
<span class="w-1 h-1 rounded-full bg-[color:var(--border)]"></span>
|
||||
<span>2 regions</span>
|
||||
<span class="w-1 h-1 rounded-full bg-[color:var(--border)]"></span>
|
||||
<span>shared paid pool</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Topology + table -->
|
||||
<div class="grid lg:grid-cols-[1.1fr_1fr]">
|
||||
<!-- LEFT: topology svg -->
|
||||
<div class="p-6 border-b lg:border-b-0 lg:border-r border-[color:var(--border)]">
|
||||
<svg viewBox="0 0 480 360" class="w-full h-auto">
|
||||
<defs>
|
||||
<linearGradient id="flowStroke2" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#0284c7" stop-opacity="0.5"/>
|
||||
<stop offset="100%" stop-color="#0284c7" stop-opacity="0.15"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Control plane (left) -->
|
||||
<rect x="20" y="150" width="120" height="60" rx="10" fill="#0c4a6e"/>
|
||||
<text x="80" y="174" text-anchor="middle" fill="#7dd3fc" font-family="ui-monospace,monospace" font-size="9" letter-spacing="2">CONTROL PLANE</text>
|
||||
<text x="80" y="192" text-anchor="middle" fill="#ffffff" font-size="12" font-weight="600">Backend · Kafka</text>
|
||||
|
||||
<!-- Workers (right column) -->
|
||||
{workers.map((w, i) => {
|
||||
const y = 30 + i * 60;
|
||||
const ts = tones(w.status);
|
||||
return (
|
||||
<g>
|
||||
<rect x="220" y={y} width="240" height="44" rx="8" fill="#ffffff" stroke="#e6ebf2"/>
|
||||
<circle cx="236" cy={y + 22} r="3" fill={w.status === 'healthy' ? '#10b981' : w.status === 'watch' ? '#f59e0b' : '#ef4444'}/>
|
||||
<text x="250" y={y + 19} fill="#0f172a" font-size="11" font-family="ui-monospace,monospace" font-weight="600">{w.id} · {w.region}</text>
|
||||
<text x="250" y={y + 33} fill="#64748b" font-size="10" font-family="ui-monospace,monospace">{w.ip} · {w.mailboxes} mailboxes</text>
|
||||
<line x1="140" y1="180" x2="220" y2={y + 22} stroke="url(#flowStroke2)" stroke-width="1.5"/>
|
||||
<circle r="2.5" fill="#0284c7" opacity="0.95">
|
||||
<animateMotion dur={`${3 + i * 0.4}s`} repeatCount="indefinite" begin={`${i * 0.5}s`}
|
||||
path={`M 140 180 L 220 ${y + 22}`}/>
|
||||
</circle>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: worker table -->
|
||||
<div>
|
||||
<div class="grid grid-cols-[60px_1fr_60px_70px] px-6 py-2.5 bg-[color:var(--surface-1)]/60 border-b border-[color:var(--border)] text-[10px] uppercase tracking-[0.16em] font-mono text-muted-foreground">
|
||||
<div>Worker</div>
|
||||
<div>IP · Region</div>
|
||||
<div class="text-right">Mboxes</div>
|
||||
<div class="text-right">Status</div>
|
||||
</div>
|
||||
{workers.map((w) => {
|
||||
const ts = tones(w.status);
|
||||
return (
|
||||
<div class="grid grid-cols-[60px_1fr_60px_70px] gap-2 px-6 py-3 border-b border-[color:var(--border)] last:border-b-0 items-center text-[12.5px]">
|
||||
<div class="font-mono text-foreground/85">{w.id}</div>
|
||||
<div>
|
||||
<div class="font-mono text-[12px] text-foreground/85">{w.ip}</div>
|
||||
<div class="font-mono text-[10.5px] text-muted-foreground">{w.region}</div>
|
||||
</div>
|
||||
<div class="text-right font-mono text-foreground/85">{w.mailboxes}</div>
|
||||
<div class="flex justify-end">
|
||||
<span class={`inline-flex items-center gap-1 h-5 px-1.5 rounded text-[10.5px] font-medium ${ts.chip}`}>
|
||||
<span class={`w-1 h-1 rounded-full ${ts.dot}`}></span>
|
||||
{w.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-7 py-3 border-t border-[color:var(--border)] bg-[color:var(--surface-1)]/60 flex items-center justify-between text-[11.5px] font-mono text-muted-foreground">
|
||||
<span class="inline-flex items-center gap-1.5"><span class="w-1.5 h-1.5 rounded-full bg-amber-500"></span>w-03 at 11 mailboxes → concentration warning fires above 10</span>
|
||||
<span>Migrate live · no campaign drops</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
CONTROL PLANE vs EXECUTION PLANE · planes explained side-by-side
|
||||
============================================================ -->
|
||||
<section class="border-y border-[color:var(--border)] bg-[color:var(--surface-1)]/40 py-20 md:py-28">
|
||||
<div class="container-page">
|
||||
<div class="max-w-2xl mb-12">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Two planes</div>
|
||||
<h2 class="text-[32px] md:text-[44px] font-semibold tracking-[-0.03em] leading-[1.05] text-heading">
|
||||
Control plane decides. Execution plane sends.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15.5px] text-foreground/70 leading-relaxed">
|
||||
The two planes never share a database connection. The control plane owns relational state and business workflows. Workers execute side effects and stay disposable.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid lg:grid-cols-2 gap-4 lg:gap-5">
|
||||
<!-- Control plane -->
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden">
|
||||
<div class="h-1.5 bg-foreground"></div>
|
||||
<div class="p-6 md:p-7">
|
||||
<div class="flex items-baseline justify-between mb-1">
|
||||
<div class="text-[18px] font-semibold tracking-[-0.02em] text-heading">Control plane</div>
|
||||
<div class="text-[10.5px] font-mono uppercase tracking-[0.16em] text-muted-foreground">cmd/backend · cmd/consumer</div>
|
||||
</div>
|
||||
<div class="text-[12px] font-mono text-muted-foreground mb-4">decisions + persisted state</div>
|
||||
<ul class="space-y-2.5 text-[13.5px] text-foreground/80">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={12} class="text-foreground mt-1 shrink-0"/><span>Owns Postgres. Stores accounts, campaigns, contacts, suppression, ban history.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={12} class="text-foreground mt-1 shrink-0"/><span>Decides which mailbox sits on which worker and when to rebalance.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={12} class="text-foreground mt-1 shrink-0"/><span>Publishes commands to per-worker Kafka topics.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={12} class="text-foreground mt-1 shrink-0"/><span>Consumes worker-emitted events back into campaign state.</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Execution plane -->
|
||||
<div class="rounded-[14px] bg-white overflow-hidden ring-2 ring-[#0284c7]/35 shadow-[0_20px_40px_-20px_rgba(2,132,199,0.3)]">
|
||||
<div class="h-1.5 bg-[#0284c7]"></div>
|
||||
<div class="p-6 md:p-7">
|
||||
<div class="flex items-baseline justify-between mb-1">
|
||||
<div class="text-[18px] font-semibold tracking-[-0.02em] text-[#0369a1]">Execution plane</div>
|
||||
<div class="text-[10.5px] font-mono uppercase tracking-[0.16em] text-[#0284c7]">cmd/worker</div>
|
||||
</div>
|
||||
<div class="text-[12px] font-mono text-[#0284c7]/80 mb-4">disposable · cattle, not pets</div>
|
||||
<ul class="space-y-2.5 text-[13.5px] text-foreground/85">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={12} class="text-[#0284c7] mt-1 shrink-0"/><span>No Postgres connection. Boots Kafka, Redis, KMS, DynamoDB, S3.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={12} class="text-[#0284c7] mt-1 shrink-0"/><span>One worker per machine = one IP = one independent reputation surface.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={12} class="text-[#0284c7] mt-1 shrink-0"/><span>State is replayable from Kafka. Killing a worker loses nothing.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={12} class="text-[#0284c7] mt-1 shrink-0"/><span>Per-mailbox cap and gap enforced server-side, not by the campaign.</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
THE MATH · code block showing the budget computation
|
||||
============================================================ -->
|
||||
<section class="border-b border-[color:var(--border)] py-20 md:py-28">
|
||||
<div class="container-page grid lg:grid-cols-[1fr_1.3fr] gap-12 lg:gap-16 items-start">
|
||||
<div class="lg:sticky lg:top-24" style="align-self: start;">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Per-mailbox math</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
A worker's safe volume is the sum of its mailbox budgets.
|
||||
Worker capacity is the sum of its mailboxes.
|
||||
</h2>
|
||||
<p class="mt-5 text-[15.5px] text-foreground/75 leading-relaxed max-w-xl">
|
||||
We don't reward forcing one inbox to do the work of ten. Volume scales by adding mailboxes, not by cranking one. Concentration warnings surface before reputation feedback would.
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed max-w-md">
|
||||
No flat per-worker ceiling. Volume scales by adding mailboxes, not by raising per-mailbox caps. The scheduler refuses to set a worker cap higher than the sum of its mailbox budgets.
|
||||
</p>
|
||||
<ul class="mt-6 space-y-2.5">
|
||||
{[
|
||||
'Default 50 / day per mailbox · default 600s gap',
|
||||
'Worker cap = Σ mailbox budgets · no flat ceiling',
|
||||
'Concentration warning at ~10 active cold mailboxes / worker',
|
||||
'Live rebalance and migration between workers',
|
||||
].map((b) => (
|
||||
<li class="flex items-start gap-2 text-[14px] text-foreground/85">
|
||||
<Icon name="check" size={13} class="text-[color:var(--sky-6)] mt-1 shrink-0" />
|
||||
<span>{b}</span>
|
||||
</li>
|
||||
))}
|
||||
<ul class="mt-6 space-y-2.5 text-[14px] text-foreground/85">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={13} class="text-[#0284c7] mt-1 shrink-0"/><span>Default 100/day per mailbox · 600s minimum gap</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={13} class="text-[#0284c7] mt-1 shrink-0"/><span>Concentration warning fires above 10 active cold mailboxes per worker</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size={13} class="text-[#0284c7] mt-1 shrink-0"/><span>Health band of any mailbox feeds back into worker pool selection</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Faux code panel -->
|
||||
<div class="rounded-[14px] overflow-hidden ring-1 ring-[color:var(--border)]" style="box-shadow: var(--shadow-sm);">
|
||||
<div class="rounded-[14px] overflow-hidden ring-1 ring-[color:var(--border)]">
|
||||
<div class="flex items-center gap-2 px-4 py-2.5 bg-[#0c1224] border-b border-white/10">
|
||||
<span class="w-2.5 h-2.5 rounded-full bg-[#ff5f56]/80"></span>
|
||||
<span class="w-2.5 h-2.5 rounded-full bg-[#ffbd2e]/80"></span>
|
||||
<span class="w-2.5 h-2.5 rounded-full bg-[#27c93f]/80"></span>
|
||||
<span class="ml-3 text-[11.5px] font-mono text-white/60">worker-budget.ts</span>
|
||||
<span class="ml-3 text-[11.5px] font-mono text-white/60">worker_scheduler.go</span>
|
||||
</div>
|
||||
<pre class="bg-[#0c1224] text-white/85 p-5 text-[12.5px] leading-[1.65] font-mono overflow-x-auto"><code><span style="color:#7dd3fc">const</span> mailboxes = worker.mailboxes
|
||||
.<span style="color:#a78bfa">filter</span>(m => m.status === <span style="color:#fde68a">'active'</span> && m.campaignsAttached)
|
||||
<pre class="bg-[#0c1224] text-white/85 p-5 text-[12.5px] leading-[1.65] font-mono overflow-x-auto"><code><span style="color:#94a3b8">// Active mailboxes attached to outbound campaigns</span>
|
||||
mailboxes := worker.Mailboxes.<span style="color:#a78bfa">Filter</span>(<span style="color:#7dd3fc">func</span>(m Mailbox) <span style="color:#7dd3fc">bool</span> {
|
||||
<span style="color:#f472b6">return</span> m.Status == <span style="color:#fde68a">"active"</span> && m.HasOutbound()
|
||||
})
|
||||
|
||||
<span style="color:#7dd3fc">const</span> dailyBudget = mailboxes
|
||||
.<span style="color:#a78bfa">reduce</span>((sum, m) => sum + m.coldCapPerDay, <span style="color:#fde68a">0</span>)
|
||||
|
||||
<span style="color:#94a3b8">// Concentration check</span>
|
||||
<span style="color:#f472b6">if</span> (mailboxes.length > CONCENTRATION_THRESHOLD) {
|
||||
warnings.<span style="color:#a78bfa">push</span>({
|
||||
code: <span style="color:#fde68a">'WORKER_CONCENTRATION'</span>,
|
||||
activeMailboxes: mailboxes.length,
|
||||
suggestedAction: <span style="color:#fde68a">'distribute'</span>,
|
||||
})
|
||||
<span style="color:#94a3b8">// Daily budget = sum of mailbox caps. No flat per-worker number.</span>
|
||||
dailyBudget := <span style="color:#fde68a">0</span>
|
||||
<span style="color:#f472b6">for</span> _, m := <span style="color:#f472b6">range</span> mailboxes {
|
||||
dailyBudget += m.ColdCapPerDay
|
||||
}
|
||||
|
||||
<span style="color:#94a3b8">// dailyBudget caps outbound — no flat per-worker number</span>
|
||||
scheduler.<span style="color:#a78bfa">setWorkerCap</span>(worker.id, dailyBudget)</code></pre>
|
||||
<span style="color:#94a3b8">// Concentration check (default threshold = 10)</span>
|
||||
<span style="color:#f472b6">if</span> <span style="color:#a78bfa">len</span>(mailboxes) > ConcentrationThreshold {
|
||||
metrics.<span style="color:#a78bfa">EmitWarning</span>(WorkerConcentration{
|
||||
WorkerID: worker.ID,
|
||||
ActiveMailboxes: <span style="color:#a78bfa">len</span>(mailboxes),
|
||||
Suggestion: <span style="color:#fde68a">"distribute"</span>,
|
||||
})
|
||||
}
|
||||
|
||||
scheduler.<span style="color:#a78bfa">SetWorkerCap</span>(worker.ID, dailyBudget)</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PROVIDERS: 3-col with logos as text -->
|
||||
<section class="py-16 md:py-24">
|
||||
<div class="container-page">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Providers</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading max-w-2xl">
|
||||
Native Gmail. Native Outlook. SMTP / IMAP for the rest.
|
||||
</h2>
|
||||
<!-- ============================================================
|
||||
MESSAGE LIFECYCLE · numbered editorial loop (like warmup)
|
||||
============================================================ -->
|
||||
<section class="bg-[color:var(--surface-1)]/40 border-b border-[color:var(--border)] py-20 md:py-28">
|
||||
<div class="container-page grid lg:grid-cols-[1fr_2fr] gap-12 lg:gap-20 items-start">
|
||||
<div class="lg:sticky lg:top-24" style="align-self: start;">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Message lifecycle</div>
|
||||
<h2 class="text-[28px] md:text-[40px] font-semibold tracking-[-0.025em] leading-[1.06] text-heading">
|
||||
From queue to delivered, one send.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15px] text-foreground/70 leading-relaxed max-w-md">
|
||||
Every send moves through six handoffs between the control plane and the execution plane, each with a defined input, output, and Kafka topic.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 grid md:grid-cols-3 gap-4">
|
||||
<ol class="divide-y divide-[color:var(--border)] border-y border-[color:var(--border)]">
|
||||
{[
|
||||
{ tag: 'Google Workspace · Gmail', title: 'OAuth 2.0 with refresh tokens.', body: 'Gmail API for send. FBL signals where available. Threaded replies preserved.' },
|
||||
{ tag: 'Microsoft 365 · Outlook', title: 'OAuth with shared mailbox support.', body: 'Graph API for send. IMAP / EWS for sync. Tenant-scoped consent.' },
|
||||
{ tag: 'Custom SMTP / IMAP', title: 'Bring your own server.', body: 'TLS, STARTTLS, app passwords, and OAuth where the provider supports it.' },
|
||||
].map((p) => (
|
||||
<div class="rounded-[12px] bg-white ring-1 ring-[color:var(--border)] p-6" style="box-shadow: var(--shadow-sm);">
|
||||
<div class="text-[10.5px] uppercase tracking-[0.16em] font-mono text-[color:var(--sky-6)] mb-3">{p.tag}</div>
|
||||
<div class="text-[16px] font-semibold text-heading tracking-[-0.01em]">{p.title}</div>
|
||||
<p class="mt-2 text-[13.5px] text-foreground/70 leading-relaxed">{p.body}</p>
|
||||
{ n: '01', t: 'Scheduled', k: 'scheduler.enqueue(mailbox_id, message_id)', b: 'The campaign scheduler picks the next eligible recipient, checks the per-mailbox cap and gap, and enqueues a send command.' },
|
||||
{ n: '02', t: 'Dispatched', k: 'kafka.publish(worker.commands.{worker_id})', b: 'The command is published to the worker-scoped Kafka topic. Only the assigned worker consumes it.' },
|
||||
{ n: '03', t: 'Sent', k: 'provider.send(msg) → 250 + queue_id', b: 'The worker calls the mailbox provider (Gmail API, Graph API, or SMTP). Provider returns the queue ID, which is recorded for delivery correlation.' },
|
||||
{ n: '04', t: 'Acknowledged', k: 'kafka.publish(worker.results.{worker_id})', b: 'The worker emits a send-result event back to the control plane. Cap usage is incremented atomically.' },
|
||||
{ n: '05', t: 'Tracked', k: 'tracking.record(message_id, signals)', b: 'Open and click tracking, when enabled, flow through the tracking edge with dedupe at IP + task granularity.' },
|
||||
{ n: '06', t: 'Reconciled', k: 'consumer.fold(events, campaign_state)', b: 'Bounces, complaints, replies, and OOOs are folded back into per-mailbox health and contact suppression. The next send is scheduled.' },
|
||||
].map((s) => (
|
||||
<li class="grid grid-cols-[44px_1fr] gap-4 py-5 items-baseline">
|
||||
<span class="font-mono text-[12px] text-[#0284c7] font-semibold">{s.n}</span>
|
||||
<div>
|
||||
<div class="text-[15.5px] font-semibold text-heading">{s.t}</div>
|
||||
<div class="mt-1 font-mono text-[12px] text-[#0369a1] bg-[color:var(--sky-1)]/60 px-2 py-1 rounded inline-block">{s.k}</div>
|
||||
<p class="mt-2 text-[14px] text-foreground/70 leading-relaxed">{s.b}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
PROVIDERS · 3 detailed cards
|
||||
============================================================ -->
|
||||
<section class="border-b border-[color:var(--border)] py-20 md:py-28">
|
||||
<div class="container-page">
|
||||
<div class="max-w-2xl mb-12">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Providers</div>
|
||||
<h2 class="text-[32px] md:text-[44px] font-semibold tracking-[-0.03em] leading-[1.05] text-heading">
|
||||
Native Gmail. Native Outlook. SMTP for the rest.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15.5px] text-foreground/70 leading-relaxed">
|
||||
OAuth-first integrations with the two big providers, plus SMTP / IMAP for everything else. No third-party relay in the middle.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-4">
|
||||
{providers.map((p, i) => (
|
||||
<div class="rounded-[14px] bg-white ring-1 ring-[color:var(--border)] overflow-hidden">
|
||||
<div class="h-1.5" style={`background: ${i === 0 ? '#ea4335' : i === 1 ? '#0078d4' : '#0284c7'}`}></div>
|
||||
<div class="p-6">
|
||||
<div class="text-[10.5px] font-mono uppercase tracking-[0.18em] text-muted-foreground mb-2">{p.tag}</div>
|
||||
<div class="text-[18px] font-semibold text-heading tracking-[-0.015em]">{p.name}</div>
|
||||
<p class="mt-3 text-[13px] text-foreground/65 leading-relaxed">{p.body}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CTA primaryLabel="Connect a mailbox" />
|
||||
<!-- ============================================================
|
||||
LIMITS · 2-col spec sheet
|
||||
============================================================ -->
|
||||
<section class="py-20 md:py-28">
|
||||
<div class="container-page">
|
||||
<div class="max-w-2xl mb-12">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">Limits</div>
|
||||
<h2 class="text-[32px] md:text-[44px] font-semibold tracking-[-0.03em] leading-[1.05] text-heading">
|
||||
What the engine enforces.
|
||||
</h2>
|
||||
<p class="mt-4 text-[15.5px] text-foreground/70 leading-relaxed">
|
||||
Hard limits in the codebase. Override the per-mailbox cap if you have a real reason, but the gap and concentration ceiling are not negotiable.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-x-12 gap-y-px">
|
||||
{limits.map((d, i) => (
|
||||
<div class="grid grid-cols-[1fr_auto] gap-6 py-6 border-b border-[color:var(--border)] items-baseline hover:bg-[color:var(--surface-1)]/40 -mx-3 px-3 rounded-[6px] transition-colors">
|
||||
<div>
|
||||
<div class="text-[10.5px] uppercase tracking-[0.22em] font-mono text-muted-foreground">
|
||||
{String(i + 1).padStart(2, '0')} · {d.k}
|
||||
</div>
|
||||
<p class="mt-2 text-[13.5px] text-foreground/65 leading-relaxed max-w-md">{d.why}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-[20px] md:text-[24px] font-semibold tracking-[-0.02em] text-heading font-mono whitespace-nowrap">{d.v}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="mt-8 text-[11.5px] font-mono text-muted-foreground">
|
||||
Source: <span class="text-foreground/75">cmd/worker/main.go</span> · <span class="text-foreground/75">internal/scheduler/email_scheduler.go</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============================================================
|
||||
FAQ · grid-rows accordion
|
||||
============================================================ -->
|
||||
<section class="bg-[color:var(--surface-1)]/40 border-t border-[color:var(--border)] py-20 md:py-24">
|
||||
<div class="container-page grid lg:grid-cols-[1fr_1.8fr] gap-12 lg:gap-20 items-start">
|
||||
<div class="lg:sticky lg:top-24" style="align-self: start;">
|
||||
<div class="text-[11px] uppercase tracking-[0.18em] text-muted-foreground font-mono mb-3">FAQ</div>
|
||||
<h2 class="text-[28px] md:text-[36px] font-semibold tracking-[-0.025em] leading-[1.08] text-heading">
|
||||
Four engine questions.
|
||||
</h2>
|
||||
</div>
|
||||
<div class="divide-y divide-[color:var(--border)]" data-faq>
|
||||
{faq.map(([q, a]) => (
|
||||
<div class="faq-row py-2">
|
||||
<button type="button" class="faq-trigger group w-full flex items-start justify-between gap-4 py-3 text-left" aria-expanded="false">
|
||||
<span class="text-[15.5px] font-medium text-heading group-hover:text-[#0369a1] transition-colors">{q}</span>
|
||||
<span class="faq-icon shrink-0 inline-flex items-center justify-center w-7 h-7 rounded-full bg-white ring-1 ring-[color:var(--border)] text-foreground/60 transition-transform duration-300 ease-out">
|
||||
<Icon name="plus" size={12} />
|
||||
</span>
|
||||
</button>
|
||||
<div class="faq-panel grid grid-rows-[0fr] transition-[grid-template-rows] duration-300 ease-out">
|
||||
<div class="overflow-hidden">
|
||||
<p class="pb-4 pr-12 text-[14px] text-foreground/75 leading-relaxed max-w-2xl">{a}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
(function () {
|
||||
const faq = document.querySelector('[data-faq]');
|
||||
if (!faq) return;
|
||||
faq.querySelectorAll('.faq-trigger').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
const row = btn.closest('.faq-row');
|
||||
const panel = row.querySelector('.faq-panel');
|
||||
const icon = btn.querySelector('.faq-icon');
|
||||
const open = btn.getAttribute('aria-expanded') === 'true';
|
||||
btn.setAttribute('aria-expanded', String(!open));
|
||||
panel.style.gridTemplateRows = open ? '0fr' : '1fr';
|
||||
if (icon) icon.style.transform = open ? 'rotate(0deg)' : 'rotate(45deg)';
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<CTA
|
||||
title="Connect a mailbox. Send your first campaign today."
|
||||
description="OAuth in 30 seconds. Worker assignment is automatic."
|
||||
primaryLabel="Start free trial"
|
||||
primaryHref="https://app.warmbly.com/register"
|
||||
/>
|
||||
</Layout>
|
||||
|
||||
Reference in New Issue
Block a user