One pipeline, four stages, no seams between them.
Most cold email tools are a UI wrapped around someone else's list and someone else's sending API. outreachstack ingests its own domain data, runs its own MTA, and owns the send engine between them — so each stage feeds the next instead of stopping at an integration boundary. Where a stage is not finished, this page says which part.
The domain lake
The bulk feed we subscribe to covers ~343M domains across ~1,570 zones and publishes roughly 300k new registrations and 250k drops a day; one delta a day lands in ClickHouse. Resolution then turns raw records into signals worth filtering on: an email-provider verdict that separates a real Google Workspace tenant from a security gateway from a transactional-only sender, technology fingerprints read out of TXT tokens and SPF includes, and a site platform ranked by how strong the evidence is — a CNAME beats an IP range beats a TXT token beats an SPF include. An ICP is a structured, versioned filter over that, never a freeform prompt, always something a human can inspect and edit.
Records generated, then verified against public DNS
`GET /sending-domains/{id}/records` returns the exact record set a domain needs — MX, SPF, DKIM, DMARC and the return-path CNAME — each annotated with what breaks if it is missing or wrong, because whoever pastes them into a registrar UI should not have to already know what a return-path is. The set is delivery-mode aware: a domain sending through Google or Microsoft gets that provider's records, not ours. SPF is emitted as `ip4:` mechanisms rather than an include so it spends none of SPF's ten-lookup budget, and the evaluator behind it is a real RFC 7208 implementation that counts those lookups across the whole expansion tree. Publishing the records is still your step — a DNS integration that publishes them for you exists in the codebase but is not exposed yet, and registrar purchase is not built at all.
A scheduler that treats correctness as non-negotiable
Copy is generated per campaign from the ICP and the offer through a model chosen by a policy row with a fallback list — swapping the copywriting model is an UPDATE, not a deploy — and every model call is retained against the campaign, so a line traces back to the request that wrote it. Scheduling then lives in Postgres, not a queue: rows are claimed with SELECT ... FOR UPDATE SKIP LOCKED under per-mailbox daily caps and business-hours windows in the lead's own timezone. A reply halts every pending step for that person across every campaign before the next send fires — the halt is keyed on the human, not the campaign — enforced by a database constraint and a claim-time recheck, not just an event handler.
What reputation work runs today, and what is still a runbook
Bounces and complaints arrive from the MTA as signature-verified webhooks, are classified hard, soft or complaint, and are deduplicated on the sender's own event id. A hard bounce writes a suppression row every later send is checked against; a `5.7.1` policy rejection is recorded against the domain and deliberately does not suppress the recipient, who did nothing. New mailboxes climb a daily warmup cap, and preflight refuses to launch past a mailbox that has not started one. Blocklist checks, seed-inbox placement and DMARC aggregate reports are operator runbooks today — and the 15-minute control loop that would throttle, rotate and pause on its own is specified in those runbooks but is not built. We would rather you read that here than discover it.
An audience is a filter, not a list you bought.
Every dimension below is queryable against the lake today, and every campaign's audience traces back to the exact version of the criteria that produced it — ICP versions are immutable, so an edit writes a new version rather than overwriting the one a lead set was built from. Two dimensions are not on this list on purpose: headcount and industry are asked for while you draft an ICP and are kept on the record, but the lake has no column for them yet, so they do not narrow the query. Wiring a firmographic source in is on the roadmap; pretending it is already there would cost you an audience you thought you had filtered.
- TLD
- Country
- ASN
- Technology (TXT / SPF fingerprints)
- Site platform (CNAME / IP evidence)
- Registration age
- Email provider (MX)
- SPF and DMARC posture
The loop that closes this is the next thing we build.
Two agents are designed and not shipped. A bandit optimizer that tunes subject and body variants on positive reply rate — not opens, which are unreliable and trivially gamed. And a refinement agent that decomposes outcomes across every ICP dimension, proposing narrowing where a slice underperforms and lookalike expansion where positive replies share an attribute the ICP doesn't yet filter on, always with the evidence attached and never by mutating a live ICP. What is already built is the substrate neither agent can be retrofitted onto: ICP versions are immutable and every lead records which version produced it, every message variant carries the prompt id, prompt version and model that wrote it, and every model call is kept against its campaign. Variant selection today is a deterministic weighted round-robin — a placeholder, and the reason a preview matches what actually gets sent.
EU-hosted, and specific about what that covers.
outreachstack is hosted in the European Union. The application, Postgres, the domain lake and object storage all run on Hetzner infrastructure inside the EU, and tenant data — leads, message bodies, reply history, analytics — is not replicated outside it.
- Application and databases
- Hetzner, EU region. Postgres for operational data; a ClickHouse lake for the domain data and event history; object storage for exports, archived MIME and lead snapshots.
- The MTA is ours
- A Postal instance on our own Hetzner host with its own IPs and reverse DNS, provisioned from Terraform in this repo rather than clicked together — so the sending reputation you build is on infrastructure we control, not pooled with a SaaS sender's worst customer.
- Tenant isolation
- Row-level security in Postgres, forced on the tables and enforced by the database rather than by application code, running as a role that cannot bypass it. A query that forgets its tenant fails closed instead of returning someone else's leads.
- GDPR posture
- Per-lead lawful basis and provenance are stored as columns, not policy prose. Deleting a workspace cascades across Postgres, the analytics tables and object storage, and verifies the deletion rather than assuming it. A per-recipient erasure endpoint — the one an Article 17 request from a lead needs — is on the roadmap and is not shipped.
- The honest caveat
- Model inference is routed through OpenRouter, which may execute a request on a provider outside the EU. We would rather write that down than let you discover it. An EU-only routing policy is on the roadmap and is not shipped yet.
What it plugs into, and how far each one is wired.
Everything marked connected is reachable from the API as well as the dashboard — there is no connector that only exists behind a click. Where a provider is written but not yet reachable, it says so rather than appearing on a logo row and failing at signup.
- Google Workspace
- Connected. Bring your own mailbox over OAuth; sending and reply ingestion via the Gmail API.
- Microsoft 365
- Connected. Bring your own mailbox over OAuth; sending and reply ingestion via Microsoft Graph, including the provider's own throttling signals.
- Postal (our MTA)
- Connected. Our own mail server on our own IPs, with bounce and complaint webhooks signed and verified on the way back in.
- OpenRouter
- Connected, and deliberately model-agnostic. Model choice is a per-task policy row with a fallback list that only walks on availability failures, never a constant compiled into the product.
- SMTP / IMAP
- Written, not yet connectable. The provider is implemented — TLS modes, IMAP reply ingestion, junk-folder detection — but mailbox connection currently only accepts the two OAuth providers above. The credential endpoint it needs is on the roadmap.
- Cloudflare DNS
- Written, not yet exposed. The publisher exists, including a re-read after every write to catch a provider silently rewriting your SPF, but the records endpoint you can call today hands you the record set rather than publishing it.
- API, CLI and MCP
- A single
/api/v1surface with scoped, tenant-bound keys. Long-running work returns a job envelope an agent can poll, and anything that spends money or sends mail acceptsdry_run=true. The MCP server marks each tool live or planned, and a planned tool refuses at call time instead of posting fields the API would reject; the CLI does the same with exit code 4.