[{"data":1,"prerenderedAt":1787},["ShallowReactive",2],{"search":3,"content-developer\u002Fdeliverability-infrastructure":442,"surround-\u002Fdeveloper\u002Fdeliverability-infrastructure":1782},[4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80,84,88,92,96,100,104,108,112,116,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,204,208,212,216,220,224,228,232,236,240,244,248,252,256,260,264,268,272,276,280,284,288,292,296,300,304,308,312,316,320,323,327,331,335,339,343,347,351,355,359,363,367,371,375,379,383,387,391,395,399,403,407,411,415,419,423,426,430,434,438],{"path":5,"title":6,"description":7},"\u002Fguide","Guide","Product guides for Owlat — a modular, self-hosted email platform. Learn how to send campaigns, run a personal mailbox, manage a team inbox, and more.",{"path":9,"title":10,"description":11},"\u002Fguide\u002Fgetting-started","Welcome to Owlat","Set up your Owlat workspace and send your first email — from deploying the stack to verifying a domain, building your audience, and launching a campaign.",{"path":13,"title":14,"description":15},"\u002Fguide\u002Fcontact-properties","Contact Properties","Custom fields that extend built-in contact data with your own values for segmentation.",{"path":17,"title":18,"description":19},"\u002Fguide\u002Ftopics","Topics","Topics are explicit audience groups you manage by hand — ideal for opt-in subscribers, imported cohorts, and organized contact buckets you target with campaigns.",{"path":21,"title":22,"description":23},"\u002Fguide\u002Fsegments","Segments","Build dynamic, rule-based contact groups from properties, email activity, and topic membership, re-evaluated from current data each time they're used.",{"path":25,"title":26,"description":27},"\u002Fguide\u002Fforms","Forms","Form Endpoints collect new contacts from your website or landing pages by exposing a public endpoint that accepts submissions and feeds them into a topic.",{"path":29,"title":30,"description":31},"\u002Fguide\u002Fcampaigns","Campaigns & Reporting","Build and send marketing campaigns to a topic or segment with the five-step wizard, optional A\u002FB testing, and full delivery reporting.",{"path":33,"title":34,"description":35},"\u002Fguide\u002Fab-testing","A\u002FB Testing","Compare two variants of a campaign on a test group, then automatically or manually send the winning version to the rest of your audience.",{"path":37,"title":38,"description":39},"\u002Fguide\u002Fautomations","Automations","Send emails automatically based on triggers, delays, and conditions — build welcome series, trial flows, and follow-ups once and let Owlat run them.",{"path":41,"title":42,"description":43},"\u002Fguide\u002Ftransactional","Transactional Emails","One-to-one emails your application triggers in response to a user action — password resets, order confirmations, welcome emails, and similar notifications.",{"path":45,"title":46,"description":47},"\u002Fguide\u002Fcreate-campaign","Create a Campaign","Walk through Owlat's five-step campaign wizard: Basics, Audience, Content, A\u002FB Test, and Review & Send.",{"path":49,"title":50,"description":51},"\u002Fguide\u002Fsend-campaign","Send & Monitor a Campaign","How to send your campaign and track its performance with real-time metrics.",{"path":53,"title":54,"description":55},"\u002Fguide\u002Fquick-start","Quick Start","The fastest path from a blank Owlat workspace to a live email campaign, from your first template through sending and reviewing results.",{"path":57,"title":58,"description":59},"\u002Fguide\u002Ftransactional-setup","Transactional Email Setup","Set up and send transactional emails like password resets and order confirmations via the Owlat API and SDKs.",{"path":61,"title":62,"description":63},"\u002Fguide\u002Fdeliverability","Deliverability","Verify sending domains, manage your blocklist, monitor sending reputation, and stay compliant so your emails reach the inbox.",{"path":65,"title":66,"description":67},"\u002Fguide\u002Fapi-keys-webhooks","API Keys & Webhooks","Create API keys for programmatic access and set up outbound webhooks to receive real-time notifications for email and contact events.",{"path":69,"title":70,"description":71},"\u002Fguide\u002Ffeature-flags","Feature flags","Owlat is modular — every feature listed in this guide can be turned on or off. This page is the user-facing overview of how to do it.",{"path":73,"title":74,"description":75},"\u002Fguide\u002Fteam-permissions","Team & Permissions","Use role-based access to control what each member of your organization can do, with Owner, Admin, and Editor roles.",{"path":77,"title":78,"description":79},"\u002Fguide\u002Faudit-logs","Audit Logs","A chronological record of significant actions in your Owlat organization, so you can see who did what and when.",{"path":81,"title":82,"description":83},"\u002Fguide\u002Fshare-links","Share Links","Create temporary preview links to share email designs with stakeholders who don't have dashboard access.",{"path":85,"title":86,"description":87},"\u002Fguide\u002Fpostbox","Postbox — Personal Email","Per-user mailboxes with a webmail interface and native IMAP\u002FSMTP support. Run your own Gmail-equivalent personal mailbox on your Owlat instance.",{"path":89,"title":90,"description":91},"\u002Fguide\u002Fmigrate-from-google","Migrate from Google","Import your full Gmail history into Owlat over IMAP, and let your AI assistant learn from every imported conversation.",{"path":93,"title":94,"description":95},"\u002Fguide\u002Fteam-inbox","Team Inbox","Triage inbound email as a team: read AI-classified threads, approve, edit or reject agent drafts, work the review queue, and manage quarantine.",{"path":97,"title":98,"description":99},"\u002Fguide\u002Femail-editor","Email Editor","A block-based visual editor for building responsive emails that render consistently across desktop, mobile, Outlook, Gmail, and Apple Mail.",{"path":101,"title":102,"description":103},"\u002Fguide\u002Fai-agent","AI Agent & Autonomy","Configure the AI agent that classifies and drafts replies to inbound mail: auto-reply settings, the health dashboard, circuit breakers, autonomy rules, and the knowledge backfill.",{"path":105,"title":106,"description":107},"\u002Fguide\u002Fknowledge-graph","Knowledge Graph","Browse, search, and manage Owlat's typed organizational knowledge — the 7 entry types, source attribution, confidence decay, relations, and how entries are extracted from mail.",{"path":109,"title":110,"description":111},"\u002Fguide\u002Ffiles","Files","Upload, browse, search, tag, and version documents in the file library.",{"path":113,"title":114,"description":115},"\u002Fguide\u002Fchat","Team Chat","Use Owlat's built-in team chat: public and private channels, direct messages, mentions, attachments, and channels linked to an inbox conversation.",{"path":117,"title":118,"description":119},"\u002Fguide\u002Fcode-tasks","Code Tasks","Queue coding-agent tasks, watch them move from queued through review, and run the code-worker sidecar that opens the pull requests.",{"path":121,"title":122,"description":123},"\u002Fguide\u002Faudience-data","Audience Data: Identities, Relationships & Timeline","Unify a contact across email, phone, and messaging channels, merge duplicates, map relationships, and read the cross-channel interaction timeline.",{"path":125,"title":126,"description":127},"\u002Fguide\u002Fimporting-contacts","Importing & Exporting Contacts","Bring contacts into Owlat from a CSV or from Mailchimp and Stripe, export them back out, and run bulk operations on your audience.",{"path":129,"title":130,"description":131},"\u002Fguide\u002Faccount","Your Account & Data","Export your data as JSON or CSV, request account deletion with a 30-day grace period, and use the onboarding checklist and the public preference center.",{"path":133,"title":134,"description":135},"\u002Fguide\u002Fchannels","Communication Channels","Configure SMS, WhatsApp, and generic-webhook channels, monitor channel health, and understand which channels are fully live today.",{"path":137,"title":138,"description":139},"\u002Fguide\u002Fdesktop-app","Desktop App","Install the Owlat desktop app, connect one or more workspaces, switch between them, and use native notifications, tray badges, shortcuts, and deep links.",{"path":141,"title":142,"description":143},"\u002Fguide\u002Femail-templates","Email Templates","Reusable email designs that define the structure, content, and personalization of every campaign and transactional message you send in Owlat.",{"path":145,"title":146,"description":147},"\u002Fguide\u002Fai-assistant","AI Assistant","Owlat's multi-turn, streaming, tool-calling AI assistant — a private chat surface that can search your workspace and draft copy, plus @assistant replies inside team chat.",{"path":149,"title":150,"description":151},"\u002Fguide\u002Fsecurity-scanning","Sending Security & Scanning","Owlat's security scanning: a content check for spam and phishing, an attachment scan for malware, and a Google Safe Browsing URL check. Suspicious content goes to a review queue.",{"path":153,"title":154,"description":155},"\u002Fguide\u002Fsystem-updates","System & Updates","The owner-only System & Updates screen: your current Owlat version, container health, LLM spend, and the in-app one-click updater with history.",{"path":157,"title":158,"description":159},"\u002Fguide\u002Foperating-modes","Operating Modes","The different ways to run Owlat at a company — read external mailboxes over IMAP, send transactional or marketing email through a delivery provider, host your own mail server, or run a team inbox with AI — and the rules that keep each combination coherent.",{"path":161,"title":162,"description":163},"\u002Fguide\u002Fsaved-blocks","Saved Blocks","Create reusable, linked content blocks you can drop into any email — edit one and every email that uses it updates automatically.",{"path":165,"title":166,"description":167},"\u002Fguide\u002Fmedia-library","Media Library","Manage, organize, search, and reuse images and files across your emails from one centralized hub.",{"path":169,"title":170,"description":171},"\u002Fguide\u002Femail-theme","Email Theme","Set your organization's default colors, font, and email width so every new template starts from a consistent baseline.",{"path":173,"title":174,"description":175},"\u002Fguide\u002Ftranslations","Translations","Send one email in multiple languages: add per-language translations to a single template and Owlat picks the right version for each recipient.",{"path":177,"title":178,"description":179},"\u002Fguide\u002Fcontacts","Contacts","How to add, view, organize, and manage contacts in Owlat, including sources, the contact detail tabs, and subscription compliance.",{"path":181,"title":182,"description":183},"\u002Fapi","API Overview","Owlat exposes authenticated API endpoints under your Convex site URL.",{"path":185,"title":186,"description":187},"\u002Fapi\u002Fwebhooks","Webhooks","Owlat supports both outbound customer webhooks and inbound provider webhooks.",{"path":189,"title":190,"description":191},"\u002Fapi\u002Fpublic-endpoints","Public Endpoints","These routes are public-facing and usually accessed from email links or embedded forms.",{"path":193,"title":194,"description":195},"\u002Fapi\u002Fwebhook-payloads","Webhook Payloads","The authoritative wire contract for outbound webhooks: envelope, signature headers, per-event data shapes, and payload versioning.",{"path":197,"title":198,"description":199},"\u002Fapi\u002Finbound-channels","Inbound Channel Webhooks","Provider webhook reference for inbound SMS, WhatsApp, and generic-channel messages, plus the MTA mailbox and credential callbacks.",{"path":201,"title":202,"description":203},"\u002Fapi\u002Fauthentication","Authentication","Secure API access with organization-scoped API keys.",{"path":205,"title":206,"description":207},"\u002Fapi\u002Fsdk","TypeScript SDK","Typed client for the Owlat API, usable from Node.js, Bun, Deno, or any server-side JavaScript runtime.",{"path":209,"title":210,"description":211},"\u002Fapi\u002Fsdk-java","Java SDK","The official `owlat-sdk` package provides a typed client for interacting with the Owlat API from any JVM application. Requires Java 11+.",{"path":213,"title":214,"description":215},"\u002Fapi\u002Fcontacts","Contacts API","Manage contacts for your organization.",{"path":217,"title":218,"description":219},"\u002Fapi\u002Ftopics","Topics API","Manage topic membership through authenticated endpoints.",{"path":221,"title":222,"description":223},"\u002Fapi\u002Fevents","Events API","Send contact events to drive segmentation and automation triggers.",{"path":225,"title":226,"description":227},"\u002Fapi\u002Ftransactional","Transactional API","Send published transactional templates to a recipient.",{"path":229,"title":230,"description":231},"\u002Fapi\u002Fforms","Forms API","Capture subscribers through public form endpoints.",{"path":233,"title":234,"description":235},"\u002Fdeveloper","Developer Guide","Technical architecture, feature-flag model, and provider abstractions used by Owlat.",{"path":237,"title":238,"description":239},"\u002Fdeveloper\u002Fmta-system","MTA System","Owlat's custom Mail Transfer Agent for direct SMTP delivery with intelligent rate limiting, bounce processing, and IP warming.",{"path":241,"title":242,"description":243},"\u002Fdeveloper\u002Ffeature-flags","Feature flags — developer reference","How the Owlat feature flag system works: single source of truth, dependency resolution, docker profile mapping, and how to add a new flag.",{"path":245,"title":246,"description":247},"\u002Fdeveloper\u002Fhow-email-works","How Email Works","A technical deep-dive into how email actually works — from SMTP and DNS to authentication, deliverability, and the differences between marketing and private email.",{"path":249,"title":250,"description":251},"\u002Fdeveloper\u002Femail-security","Email Security","Content scanning, attachment validation, URL reputation checking, and malware detection for outbound emails.",{"path":253,"title":254,"description":255},"\u002Fdeveloper\u002Fpostbox-architecture","Postbox Architecture","How the Postbox personal-mail feature is wired — schema, IMAP server, app-password auth, outbound relay, inbound delivery, and external mailboxes.",{"path":257,"title":258,"description":259},"\u002Fdeveloper\u002Fproviders","Providers","Pluggable provider abstractions for LLM, email sending, notifications, vector stores, and analytics, selected per-deployment so self-hosters can swap implementations without code changes.",{"path":261,"title":262,"description":263},"\u002Fdeveloper\u002Fcampaign-internals","Campaign Internals","How the campaign backend works: two status machines, send pre-flight, the send orchestrator, emailSends records, and the priority workpools.",{"path":265,"title":266,"description":267},"\u002Fdeveloper\u002Faudience-internals","Audience Internals","Backend reference for contact resolution, the double opt-in lifecycle, topic subscription, the conditions registry, and segment evaluation.",{"path":269,"title":270,"description":271},"\u002Fdeveloper\u002Fautomation-internals","Automation Internals","How the automation run engine works: the step walker, the lifecycle state machine, trigger fanout, the three step types, and the resilience cron.",{"path":273,"title":274,"description":275},"\u002Fdeveloper\u002Fdeliverability-infrastructure","Deliverability Infrastructure","The Convex-side deliverability backend: provider routing, health-aware failover, sending reputation with auto-enforcement, IP warming cache, the blocklist, and the content-scan gate.",{"path":277,"title":278,"description":279},"\u002Fdeveloper\u002Farchitecture","Architecture Overview","Owlat follows a modern serverless architecture with real-time capabilities.",{"path":281,"title":282,"description":283},"\u002Fdeveloper\u002Fplatform-operations","Platform Operations","Operator reference for abuse status and the sending gate, the platform-admin roster, content review, org deletion, in-app self-update, dev endpoints, crons, and migrations.",{"path":285,"title":286,"description":287},"\u002Fdeveloper\u002Fscopes","Scopes","What each app and package in the Owlat monorepo is responsible for.",{"path":289,"title":290,"description":291},"\u002Fdeveloper\u002Fself-hosting","Self-Hosting","Deploy Owlat on your own infrastructure with Docker Compose. Complete guide from first boot to production.",{"path":293,"title":294,"description":295},"\u002Fdeveloper\u002Fself-hosting-config","Self-Hosting Configuration","Complete reference for Docker environment variables, Convex backend variables, service topology, and volume persistence.",{"path":297,"title":298,"description":299},"\u002Fdeveloper\u002Fself-hosting-dns-email","DNS & Email Setup","Configure DNS records, DKIM signing, SPF, DMARC, and bounce handling for reliable email delivery.",{"path":301,"title":302,"description":303},"\u002Fdeveloper\u002Fself-hosting-production","Production Deployment","Secure your self-hosted Owlat instance with TLS, firewall rules, backups, and monitoring.",{"path":305,"title":306,"description":307},"\u002Fdeveloper\u002Fself-hosting-maintenance","Maintenance & Updates","Keep your self-hosted Owlat instance up to date, manage backups, scale performance, and troubleshoot common issues.",{"path":309,"title":310,"description":311},"\u002Fdeveloper\u002Fself-hosting-desktop","Desktop Installer","Install Owlat on a bare Linux VPS straight from the desktop app over SSH — no terminal — with a live, animated provisioning timeline.",{"path":313,"title":314,"description":315},"\u002Fdeveloper\u002Fsetup-cli","Setup CLI & Installer","Operator reference for the Owlat self-host tooling: the install.sh one-liner, the owlat-setup CLI, the convex-deploy flow, and admin bootstrap.",{"path":317,"title":318,"description":319},"\u002Fdeveloper\u002Fconvex","Convex Backend","Owlat uses Convex as its serverless backend, providing real-time subscriptions, ACID transactions, and TypeScript-first development.",{"path":321,"title":202,"description":322},"\u002Fdeveloper\u002Fauthentication","Owlat uses BetterAuth with the Convex adapter for authentication and organization (team) management.",{"path":324,"title":325,"description":326},"\u002Fdeveloper\u002Femail-system","Email System","Owlat's email system consists of a visual editor, template management, and multi-provider sending infrastructure.",{"path":328,"title":329,"description":330},"\u002Fdeveloper\u002Femail-renderer","Email Renderer","The @owlat\u002Femail-renderer package converts editor JSON blocks into production-ready HTML emails with cross-client compatibility, CSS inlining, dark mode, and Outlook VML fallbacks.",{"path":332,"title":333,"description":334},"\u002Fdeveloper\u002Fenvironment-variables","Environment Variables","Reference for every environment variable Owlat reads across the Convex backend, web app, MTA, IMAP server, and mail-sync worker.",{"path":336,"title":337,"description":338},"\u002Fdeveloper\u002Fcomponents","Component Library","Reference for the reusable, auto-imported Vue UI components shipped in the packages\u002Fui layer.",{"path":340,"title":341,"description":342},"\u002Fdeveloper\u002Fdecisions","Architectural Decision Records","The architectural decision records for the Owlat project, each capturing the context, the decision, and the trade-offs involved.",{"path":344,"title":345,"description":346},"\u002Fdeveloper\u002Fdecisions\u002F009-model-routing","ADR-009: Task-Based Model Routing","Why Owlat supports per-task LLM model selection instead of using a single model for all pipeline steps.",{"path":348,"title":349,"description":350},"\u002Fdeveloper\u002Fdecisions\u002F010-listing-engine","ADR-010: Listing Engine","Why Owlat replaced four incompatible list-query contracts with one generic listing engine driven by per-entity descriptors.",{"path":352,"title":353,"description":354},"\u002Fdeveloper\u002Fdecisions\u002F001-custom-email-renderer","ADR-001: Custom Email Renderer Over MJML","Why Owlat built a custom table-based HTML email renderer instead of using MJML, gaining full control over VML, dark mode, and per-client rendering.",{"path":356,"title":357,"description":358},"\u002Fdeveloper\u002Fdecisions\u002F002-convex-backend","ADR-002: Convex as Backend","Why Owlat chose Convex over PostgreSQL and Firebase for real-time reactivity, co-located TypeScript logic, and zero-config scaling.",{"path":360,"title":361,"description":362},"\u002Fdeveloper\u002Fdecisions\u002F003-notion-like-builder","ADR-003: Notion-like Email Builder","Why Owlat replaced the traditional 3-panel email editor with a Notion-like single-column canvas for inline WYSIWYG editing.",{"path":364,"title":365,"description":366},"\u002Fdeveloper\u002Fdecisions\u002F004-monorepo-bun-workspaces","ADR-004: Monorepo with Bun Workspaces","Why Owlat uses a monorepo with Bun workspaces and Turborepo for fast installs, atomic cross-package changes, and cached CI.",{"path":368,"title":369,"description":370},"\u002Fdeveloper\u002Fdecisions\u002F005-custom-mta","ADR-005: Custom MTA","Why Owlat built a custom Mail Transfer Agent instead of relying solely on third-party email providers.",{"path":372,"title":373,"description":374},"\u002Fdeveloper\u002Fdecisions\u002F006-self-hosted-convex","ADR-006: Self-Hosted Convex","Why Owlat uses the open-source Convex backend for self-hosting instead of migrating to a different database.",{"path":376,"title":377,"description":378},"\u002Fdeveloper\u002Fdecisions\u002F007-pluggable-llm","ADR-007: Pluggable LLM Provider","Why Owlat uses the Vercel AI SDK with a provider abstraction layer instead of hardcoding a single LLM vendor.",{"path":380,"title":381,"description":382},"\u002Fdeveloper\u002Fdecisions\u002F008-process-architecture","ADR-008: Agent Process Architecture","Why Owlat processes inbound messages with a self-scheduling step walker plus a lifecycle coordinator instead of one sequential function.",{"path":384,"title":385,"description":386},"\u002Fexamples","Examples","Copy-pasteable integration patterns for common Owlat use cases.",{"path":388,"title":389,"description":390},"\u002Fexamples\u002Fwelcome-email","Welcome Email","Send a personalized welcome email when a new user signs up.",{"path":392,"title":393,"description":394},"\u002Fexamples\u002Fbilling-email","Billing Email","Send a billing receipt with an invoice PDF attached after a successful payment.",{"path":396,"title":397,"description":398},"\u002Fexamples\u002Fevent-automation","Event Automation","Trigger automations with custom events for trial lifecycle, feature adoption, and more.",{"path":400,"title":401,"description":402},"\u002Fexamples\u002Fcontact-sync","Contact Sync","Sync contacts from your database to Owlat using upsert patterns and bulk operations.",{"path":404,"title":405,"description":406},"\u002Fexamples\u002Fwebhook-handler","Webhook Handler","Handle Owlat delivery webhooks with signature verification and event routing.",{"path":408,"title":409,"description":410},"\u002Fexamples\u002Fmultilingual-email","Multilingual Email","Send emails in the recipient's preferred language using template translations.",{"path":412,"title":413,"description":414},"\u002Fvision","Vision","Where Owlat is heading — from email platform to unified communication intelligence powered by AI agents.",{"path":416,"title":417,"description":418},"\u002Fvision\u002Fself-hosting","Self-Hosting Architecture","How Owlat runs as a fully self-hosted stack using Docker Compose — open-source Convex backend, custom MTA, and a pluggable LLM provider.",{"path":420,"title":421,"description":422},"\u002Fvision\u002Fagent-pipeline","Agent Pipeline","Technical architecture for the inbound email agent pipeline — step modules, the walker, security scanning, threading, and human review.",{"path":424,"title":106,"description":425},"\u002Fvision\u002Fknowledge-graph","Technical architecture for Owlat's typed knowledge storage — how organizational knowledge is stored, searched, decayed, and maintained.",{"path":427,"title":428,"description":429},"\u002Fvision\u002Fmulti-channel","Multi-Channel & CRM","Technical architecture for channel adapters, unified messaging, contact identity unification, and the CRM hub.",{"path":431,"title":432,"description":433},"\u002Fvision\u002Ffile-system","Semantic File System","Technical architecture for Owlat's semantic file storage — version tracking with provenance today, plus the planned embedding-based retrieval and auto-tagging layer.",{"path":435,"title":436,"description":437},"\u002Fvision\u002Fdesktop-app","Desktop App & Advanced Agents","Architecture of the Owlat desktop shell, visualization agent, adaptive dashboard, agent health, graduated autonomy, and coding agents.",{"path":439,"title":440,"description":441},"\u002Fvision\u002Froadmap","Roadmap","What's planned next for Owlat — the documented-but-unbuilt pieces still being wired, and the enhancements on our radar.",{"id":443,"title":274,"body":444,"description":275,"extension":1776,"meta":1777,"navigation":1778,"path":273,"seo":1779,"stem":1780,"__hash__":1781},"content\u002F3.developer\u002F19.deliverability-infrastructure.md",{"type":445,"value":446,"toc":1752},"minimark",[447,451,468,473,488,496,605,638,643,667,726,757,796,800,822,867,871,883,930,950,954,985,989,1007,1083,1087,1104,1166,1170,1203,1217,1233,1237,1273,1289,1296,1314,1323,1327,1337,1381,1388,1432,1443,1447,1451,1454,1522,1526,1545,1613,1634,1638,1674,1737,1741,1745,1748],[448,449,450],"p",{},"This page maps the deliverability machinery that lives in the Convex backend: how a send picks a provider, how provider failures feed failover, how delivery events accumulate into a reputation score that can auto-warn or auto-suspend the deployment, the cached view of IP-warming state, the address blocklist, and the daily send counters plus the pre-send content-scan thresholds.",[448,452,453,454,458,459,462,463,467],{},"The ",[455,456,457],"em",{},"sending-side"," intelligence — per-ISP throttling, the actual IP warming schedule, circuit breakers, and DNSBL monitoring — lives in the MTA, not Convex. See ",[460,461,238],"a",{"href":237}," for that. This page covers everything Convex owns; the two meet at the MTA's ",[464,465,466],"code",{},"\u002Fip-reputation"," endpoint and its delivery webhooks.",[469,470,472],"h2",{"id":471},"per-org-provider-routing-and-strategies","Per-org provider routing and strategies",[448,474,475,476,479,480,483,484,487],{},"A deployment can route each message type to a different email provider, or split a single type across several. Routes are stored in the ",[464,477,478],{},"providerRoutes"," table (",[464,481,482],{},"apps\u002Fapi\u002Fconvex\u002Fschema\u002Fdelivery.ts",") and managed through ",[464,485,486],{},"apps\u002Fapi\u002Fconvex\u002FproviderRoutes.ts",".",[448,489,490,491,495],{},"Each route row keys on ",[492,493,494],"strong",{},"one message type"," and carries a strategy, an ordered provider list, and an optional IP-pool override:",[497,498,499,515],"table",{},[500,501,502],"thead",{},[503,504,505,509,512],"tr",{},[506,507,508],"th",{},"Field",[506,510,511],{},"Type",[506,513,514],{},"Meaning",[516,517,518,541,562,592],"tbody",{},[503,519,520,526,538],{},[521,522,523],"td",{},[464,524,525],{},"messageType",[521,527,528,531,532,531,535],{},[464,529,530],{},"campaign"," | ",[464,533,534],{},"transactional",[464,536,537],{},"automation",[521,539,540],{},"One route per message type",[503,542,543,548,559],{},[521,544,545],{},[464,546,547],{},"strategy",[521,549,550,531,553,531,556],{},[464,551,552],{},"single",[464,554,555],{},"priority_failover",[464,557,558],{},"workload_split",[521,560,561],{},"Selection algorithm",[503,563,564,569,575],{},[521,565,566],{},[464,567,568],{},"providers",[521,570,571,572],{},"array of ",[464,573,574],{},"{ providerType, weight?, isEnabled }",[521,576,577,578,581,582,585,586,585,589],{},"Ordered candidate set; ",[464,579,580],{},"providerType"," is ",[464,583,584],{},"mta"," \u002F ",[464,587,588],{},"ses",[464,590,591],{},"resend",[503,593,594,599,602],{},[521,595,596],{},[464,597,598],{},"ipPool",[521,600,601],{},"string (optional)",[521,603,604],{},"Override the MTA IP pool for sends on this route",[448,606,607,610,611,614,615,618,619,622,623,626,627,585,630,633,634,637],{},[464,608,609],{},"setRoute"," upserts a route and ",[464,612,613],{},"removeRoute"," deletes one (reverting that message type to the global default). Both require the ",[464,616,617],{},"organization:manage"," permission. The public reader is ",[464,620,621],{},"listRoutes"," (an ",[464,624,625],{},"authedQuery","); send paths resolve routes through ",[464,628,629],{},"resolveSendRoute",[464,631,632],{},"resolveSendRouteFromDb"," (",[464,635,636],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FsendProviders\u002Froute.ts",").",[639,640,642],"h3",{"id":641},"the-three-strategies","The three strategies",[448,644,645,646,633,649,652,653,655,656,659,660,662,663,666],{},"Selection is a pure function. The thin dispatcher ",[464,647,648],{},"resolveRoute",[464,650,651],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FsendProviders\u002Frouting.ts",") looks up a strategy module by ",[464,654,547],{}," and calls its ",[464,657,658],{},"select()"," with the enabled providers, the route's ",[464,661,598],{},", and the current provider-health snapshot. Each strategy lives in its own folder under ",[464,664,665],{},"lib\u002FsendProviders\u002Fstrategies\u002F",":",[497,668,669,679],{},[500,670,671],{},[503,672,673,676],{},[506,674,675],{},"Strategy",[506,677,678],{},"Behaviour",[516,680,681,690,707],{},[503,682,683,687],{},[521,684,685],{},[464,686,552],{},[521,688,689],{},"Always use the first enabled provider. Ignores health.",[503,691,692,696],{},[521,693,694],{},[464,695,555],{},[521,697,698,699,702,703,706],{},"Walk enabled providers in order; pick the first that is ",[492,700,701],{},"not"," ",[464,704,705],{},"down",". Falls back to the first enabled provider if all are down or no health data exists.",[503,708,709,713],{},[521,710,711],{},[464,712,558],{},[521,714,715,716,718,719,722,723,725],{},"Weighted-random pick across enabled providers, excluding any that are ",[464,717,705],{},". Weights default to ",[464,720,721],{},"100"," (uniform). If every provider is ",[464,724,705],{},", it still picks one rather than blocking the send.",[448,727,728,729,731,732,735,736,739,740,742,743,746,747,633,750,585,753,756],{},"When there is no route, no enabled provider, or the strategy returns nothing, ",[464,730,648],{}," falls through to the ",[464,733,734],{},"EMAIL_PROVIDER"," env var, and otherwise returns ",[464,737,738],{},"null"," (unconfigured). Resolution is fail-closed — there is no implicit ",[464,741,584],{}," default, so an unconfigured deployment never silently dispatches to a phantom MTA. The returned ",[464,744,745],{},"ResolvedRoute"," records its ",[464,748,749],{},"source",[464,751,752],{},"org_config",[464,754,755],{},"env_fallback",") for observability.",[758,759,762],"callout",{"title":760,"type":761},"IP pool plumbing","info",[448,763,764,765,767,768,633,771,774,775,777,778,781,782,784,785,788,789,792,793,795],{},"The route's ",[464,766,598],{}," is threaded to the MTA adapter via ",[464,769,770],{},"MtaExtras",[464,772,773],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FsendProviders\u002Fmta\u002Findex.ts","); when unset, the adapter defaults to the ",[464,776,534],{}," pool. Transactional, test, and one-off sends pass it directly. The per-recipient campaign workpool worker (",[464,779,780],{},"apps\u002Fapi\u002Fconvex\u002Fdelivery\u002Fworker.ts",") passes ",[464,783,770],{}," carrying only a ",[464,786,787],{},"messageId"," idempotency key (",[464,790,791],{},"worker.ts:441","), with no ",[464,794,598],{},", so campaign-level pool routing is selected at orchestration time rather than re-derived per message.",[469,797,799],{"id":798},"send-dispatch-and-provider-health-aware-failover","Send dispatch and provider health-aware failover",[448,801,802,803,633,806,809,810,813,814,817,818,821],{},"Every send producer funnels through one helper, ",[464,804,805],{},"sendProviderDispatch",[464,807,808],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FsendProviders\u002Fdispatch.ts","), described by ADR-0020 (",[464,811,812],{},"docs\u002Fadr\u002F0020-send-provider-adapter-modules.md","). Six producers route through it: the workpool worker, the campaign orchestrator's test send, the post-send resend, the automation email step, the transactional HTTP send, and the system\u002Fauth mail sender (",[464,815,816],{},"sendSystemEmail"," in ",[464,819,820],{},"systemMail.ts","). The dispatcher does three things uniformly:",[823,824,825,843,857],"ol",{},[826,827,828,831,832,835,836,839,840,487],"li",{},[492,829,830],{},"Retry loop"," driven by each provider module's ",[464,833,834],{},"retryDelays"," and ",[464,837,838],{},"categorizeError",". Each attempt calls the module's single-attempt ",[464,841,842],{},"sendEmail",[826,844,845,848,849,852,853,856],{},[492,846,847],{},"Health recording"," — after every terminal outcome (success or exhausted retries) it schedules ",[464,850,851],{},"recordSendResult"," on the ",[492,854,855],{},"Send provider health"," module, so even bypass callers (test sends, automation steps) record health.",[826,858,859,862,863,866],{},[492,860,861],{},"Error categorization"," — the result carries a typed ",[464,864,865],{},"EmailErrorCode",", not a raw string.",[639,868,870],{"id":869},"provider-health","Provider health",[448,872,873,633,875,878,879,882],{},[464,874,851],{},[464,876,877],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FsendProviders\u002Fhealth.ts",") maintains one ",[464,880,881],{},"providerHealth"," row per provider kind, using exponentially-decayed rolling success\u002Ffailure counts, an EMA latency, and a consecutive-failure counter. Status is derived from those:",[497,884,885,895],{},[500,886,887],{},[503,888,889,892],{},[506,890,891],{},"Status",[506,893,894],{},"Condition",[516,896,897,907,917],{},[503,898,899,904],{},[521,900,901],{},[464,902,903],{},"healthy",[521,905,906],{},"success rate ≥ 90%",[503,908,909,914],{},[521,910,911],{},[464,912,913],{},"degraded",[521,915,916],{},"success rate ≥ 50% and \u003C 90%",[503,918,919,923],{},[521,920,921],{},[464,922,705],{},[521,924,925,926,929],{},"success rate \u003C 50%, ",[492,927,928],{},"or"," ≥ 5 consecutive failures",[448,931,453,932,934,935,633,937,939,940,942,943,835,945,585,947,949],{},[464,933,881],{}," rows are collected by ",[464,936,632],{},[464,938,636],{},") and passed to the pure ",[464,941,648],{}," before each dispatch, closing the loop: a provider that starts failing flips to ",[464,944,705],{},[464,946,555],{},[464,948,558],{}," route around it automatically.",[469,951,953],{"id":952},"sending-reputation-org-per-domain-derived-risk-auto-enforcement","Sending reputation (org + per-domain, derived risk, auto-enforcement)",[448,955,956,957,960,961,964,965,968,969,972,973,976,977,980,981,984],{},"Delivery outcomes accumulate into the ",[464,958,959],{},"sendingReputation"," table, owned exclusively by the ",[492,962,963],{},"Sending reputation"," module (",[464,966,967],{},"apps\u002Fapi\u002Fconvex\u002Fanalytics\u002FsendingReputation.ts","), per ADR-0042 (",[464,970,971],{},"docs\u002Fadr\u002F0042-sending-reputation-module.md","). It is a scope-discriminated table: ",[464,974,975],{},"scope: 'org'"," rows track the whole deployment; ",[464,978,979],{},"scope: 'domain'"," rows track one sending domain. Bounce rate, complaint rate, and risk level are ",[492,982,983],{},"never stored"," — they are derived on read.",[639,986,988],{"id":987},"how-events-arrive","How events arrive",[448,990,991,992,995,996,999,1000,1003,1004,1006],{},"The Send lifecycle (",[464,993,994],{},"apps\u002Fapi\u002Fconvex\u002Fdelivery\u002FsendLifecycle.ts",") emits a ",[464,997,998],{},"reputation_update"," effect on each delivery transition, which schedules ",[464,1001,1002],{},"recordEvent"," with an event type and (when known) the sending domain. ",[464,1005,1002],{}," is the single writer: it bumps today's org day-bucket always, and the domain day-bucket when a domain is present.",[497,1008,1009,1019],{},[500,1010,1011],{},[503,1012,1013,1016],{},[506,1014,1015],{},"Event type",[506,1017,1018],{},"Counters bumped",[516,1020,1021,1033,1045,1057,1071],{},[503,1022,1023,1028],{},[521,1024,1025],{},[464,1026,1027],{},"send",[521,1029,1030],{},[464,1031,1032],{},"totalSent",[503,1034,1035,1040],{},[521,1036,1037],{},[464,1038,1039],{},"deliver",[521,1041,1042],{},[464,1043,1044],{},"totalDelivered",[503,1046,1047,1052],{},[521,1048,1049],{},[464,1050,1051],{},"bounce",[521,1053,1054],{},[464,1055,1056],{},"totalBounced",[503,1058,1059,1064],{},[521,1060,1061],{},[464,1062,1063],{},"hard_bounce",[521,1065,1066,835,1068],{},[464,1067,1056],{},[464,1069,1070],{},"totalHardBounced",[503,1072,1073,1078],{},[521,1074,1075],{},[464,1076,1077],{},"complaint",[521,1079,1080],{},[464,1081,1082],{},"totalComplaints",[639,1084,1086],{"id":1085},"derived-risk","Derived risk",[448,1088,1089,1092,1093,1096,1097,1100,1101,666],{},[464,1090,1091],{},"summarize"," (and ",[464,1094,1095],{},"summarizeDomains"," for the per-domain view) is the ",[492,1098,1099],{},"only"," place the rolling 30-day window is summed; it is reader-typed so the writer, the session-auth queries, the platform-admin queries, and the control-plane reporter all derive the identical number. Risk is computed from industry-standard thresholds (Gmail\u002FYahoo reject above a 0.3% complaint rate). Senders below the minimum sample size are always ",[464,1102,1103],{},"low",[497,1105,1106,1116],{},[500,1107,1108],{},[503,1109,1110,1113],{},[506,1111,1112],{},"Risk",[506,1114,1115],{},"Trigger (with ≥ 100 sends in window)",[516,1117,1118,1127,1140,1153],{},[503,1119,1120,1124],{},[521,1121,1122],{},[464,1123,1103],{},[521,1125,1126],{},"below the medium thresholds",[503,1128,1129,1134],{},[521,1130,1131],{},[464,1132,1133],{},"medium",[521,1135,1136,1137,1139],{},"complaint rate ≥ 0.1% ",[492,1138,928],{}," bounce rate ≥ 2%",[503,1141,1142,1147],{},[521,1143,1144],{},[464,1145,1146],{},"high",[521,1148,1149,1150,1152],{},"complaint rate ≥ 0.2% ",[492,1151,928],{}," bounce rate ≥ 5%",[503,1154,1155,1160],{},[521,1156,1157],{},[464,1158,1159],{},"critical",[521,1161,1162,1163,1165],{},"complaint rate ≥ 0.3% ",[492,1164,928],{}," bounce rate ≥ 10%",[639,1167,1169],{"id":1168},"auto-enforcement","Auto-enforcement",[448,1171,1172,1173,1175,1176,1179,1180,1182,1183,1185,1186,1189,1190,1192,1193,1196,1197,1192,1199,1202],{},"Auto-enforcement no longer runs inside ",[464,1174,1002],{}," (which now only bumps the sharded counters). It runs hourly via the ",[464,1177,1178],{},"evaluateAutoEnforce"," cron, which summarizes the org window once and — at ",[464,1181,1146],{}," or ",[464,1184,1159],{}," — schedules ",[464,1187,1188],{},"autoEnforceReputation",", picking a target Abuse status (",[464,1191,1146],{}," → ",[464,1194,1195],{},"warned",", ",[464,1198,1159],{},[464,1200,1201],{},"suspended",") and delegating the transition to the Abuse status module (ADR-0011), which dedupes idempotently and refuses severity downgrades. Domain buckets feed the per-domain dashboard only — Abuse status is a deployment-level state.",[448,1204,1205,1208,1209,1212,1213,1216],{},[464,1206,1207],{},"recalculateAll"," is a ",[492,1210,1211],{},"cleanup-only"," hourly cron (wired in ",[464,1214,1215],{},"apps\u002Fapi\u002Fconvex\u002Fcrons.ts","); it ages out day-buckets older than 60 days across both scopes. Risk no longer needs periodic recalculation because it is derived on read.",[758,1218,1220],{"title":1219,"type":761},"Where the dashboards live",[448,1221,1222,1223,633,1226,1196,1229,1232],{},"Session-scoped reads are in ",[464,1224,1225],{},"apps\u002Fapi\u002Fconvex\u002Fanalytics\u002FreputationQueries.ts",[464,1227,1228],{},"getSendingOverview",[464,1230,1231],{},"getDomainReputations","). The deployment-wide platform-admin reputation surface (roster, abuse status, content-review) is a backend\u002FAPI surface in this OSS repo, not a bundled product dashboard — the rich control-plane UI was extracted to a separate private repo.",[469,1234,1236],{"id":1235},"ip-warming-state-convex-cached-and-send-estimates","IP warming state (Convex-cached) and send estimates",[448,1238,1239,1240,1243,1244,633,1247,1250,1251,1254,1255,1258,1259,1261,1262,1265,1266,585,1269,1272],{},"The MTA owns the real warming schedule and per-IP state in Redis. Convex keeps a ",[492,1241,1242],{},"cached, reactive"," copy so queries can subscribe to it without hitting the MTA on every read. ",[464,1245,1246],{},"syncWarmingState",[464,1248,1249],{},"apps\u002Fapi\u002Fconvex\u002Fdelivery\u002FwarmingSync.ts",") runs every 5 minutes (cron in ",[464,1252,1253],{},"crons.ts","), fetches ",[464,1256,1257],{},"GET \u002Fip-reputation"," from the MTA, filters to the ",[464,1260,530],{}," pool (transactional IPs have no warming limits), aggregates the per-IP rows, and upserts the singleton ",[464,1263,1264],{},"warmingState"," row. If ",[464,1267,1268],{},"MTA_INTERNAL_URL",[464,1270,1271],{},"MTA_API_KEY"," are unset, it silently skips.",[448,1274,1275,1276,633,1279,585,1282,585,1285,1288],{},"The cached row carries an overall ",[464,1277,1278],{},"phase",[464,1280,1281],{},"ramp",[464,1283,1284],{},"plateau",[464,1286,1287],{},"graduated","), the summed daily cap, today's send count, an IP count, and a per-IP breakdown (phase, warming day, daily cap, sent today, bounce\u002Fdeferral rate, pool, active flag).",[448,1290,1291,1292,1295],{},"Two client-facing queries read it (",[464,1293,1294],{},"reputationQueries.ts","):",[1297,1298,1299,1306],"ul",{},[826,1300,1301,1305],{},[492,1302,1303],{},[464,1304,1228],{}," — combines warming state, daily send volume, the rolling 30-day org reputation summary, and the current abuse status into one card.",[826,1307,1308,1313],{},[492,1309,1310],{},[464,1311,1312],{},"getCampaignSendEstimate"," — given a recipient count, estimates how many days a campaign will take based on remaining daily capacity, projecting forward conservatively (~1.5× cap growth per day) when IPs are still warming. Fully-warmed deployments report a single-day estimate.",[758,1315,1318],{"title":1316,"type":1317},"Estimate is a projection","warning",[448,1319,1320,1322],{},[464,1321,1312],{}," is a UI-facing projection, not a scheduler. The actual pacing is enforced by the MTA's warming throttle at delivery time; this query only sets recipient expectations.",[469,1324,1326],{"id":1325},"suppression-list-and-blocklist","Suppression list and blocklist",[448,1328,453,1329,1332,1333,1336],{},[464,1330,1331],{},"blockedEmails"," table is the address-level suppression list — the last line of defense for sender reputation. It is managed in ",[464,1334,1335],{},"apps\u002Fapi\u002Fconvex\u002FblockedEmails.ts",". Each row stores a normalized (lowercased, trimmed) address and a reason:",[497,1338,1339,1349],{},[500,1340,1341],{},[503,1342,1343,1346],{},[506,1344,1345],{},"Reason",[506,1347,1348],{},"Source",[516,1350,1351,1361,1371],{},[503,1352,1353,1358],{},[521,1354,1355],{},[464,1356,1357],{},"bounced",[521,1359,1360],{},"Hard bounce — the address doesn't exist",[503,1362,1363,1368],{},[521,1364,1365],{},[464,1366,1367],{},"complained",[521,1369,1370],{},"Recipient marked the email as spam",[503,1372,1373,1378],{},[521,1374,1375],{},[464,1376,1377],{},"manual",[521,1379,1380],{},"Operator added it by hand",[448,1382,1383,1384,1387],{},"Blocked addresses are excluded from sends as part of the campaign audience eligibility predicate (soft-delete + email-present + suppression + DOI-if-topic). Auto-blocking happens through ",[464,1385,1386],{},"addFromEvent",", the internal writer the bounce\u002Fcomplaint handlers call; it is idempotent (re-blocking an existing address returns the existing record). Operator-facing surface:",[1297,1389,1390,1406,1422],{},[826,1391,1392,585,1395,585,1398,1401,1402,1405],{},[464,1393,1394],{},"add",[464,1396,1397],{},"bulkAdd",[464,1399,1400],{},"remove"," — require the ",[464,1403,1404],{},"contacts:manage"," permission.",[826,1407,1408,1411,1412,1196,1415,1196,1418,1421],{},[464,1409,1410],{},"listByTeam"," (optionally filtered by reason), ",[464,1413,1414],{},"get",[464,1416,1417],{},"getByEmail",[464,1419,1420],{},"getCountsByReason"," — reads.",[826,1423,1424,1427,1428,1431],{},[464,1425,1426],{},"isBlocked"," (session) and ",[464,1429,1430],{},"isBlockedInternal"," (used by other Convex functions, no access check) — point lookups.",[448,1433,1434,1435,1438,1439,1442],{},"All lookups go through the ",[464,1436,1437],{},"by_email"," index on the normalized address; ",[464,1440,1441],{},"by_reason"," backs the filtered list and counts.",[469,1444,1446],{"id":1445},"daily-send-stats-and-content-scan-gate-thresholds","Daily send stats and content-scan gate thresholds",[639,1448,1450],{"id":1449},"daily-counters","Daily counters",[448,1452,1453],{},"Two separate daily counters exist, for different purposes:",[1297,1455,1456,1490],{},[826,1457,1458,1463,1464,633,1467,1470,1471,1474,1475,817,1478,1481,1482,1485,1486,1489],{},[492,1459,1460],{},[464,1461,1462],{},"instanceSettings.dailySendCount"," — a single running counter for the current UTC day, bumped via ",[464,1465,1466],{},"nextDailySendCount",[464,1468,1469],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FsendingLimits.ts","), which folds the increment into the ",[464,1472,1473],{},"instanceSettings"," patch (the bulk-send writer is ",[464,1476,1477],{},"incrementDailySendCountInternal",[464,1479,1480],{},"campaigns\u002FsendQueries.ts","). It is ",[492,1483,1484],{},"display-only","; tier-based limits were removed and pacing is the MTA's job. ",[464,1487,1488],{},"getDailySendVolume"," resets it lazily on the first read of a new UTC day.",[826,1491,1492,1497,1498,585,1501,585,1504,585,1507,1510,1511,1514,1515,633,1518,1521],{},[492,1493,1494],{},[464,1495,1496],{},"sendDailyStats"," — one row per UTC day with ",[464,1499,1500],{},"sent",[464,1502,1503],{},"delivered",[464,1505,1506],{},"opened",[464,1508,1509],{},"clicked"," counters, written by the Send lifecycle's ",[464,1512,1513],{},"daily_stats_bump"," effect through ",[464,1516,1517],{},"bumpSendDailyStat",[464,1519,1520],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FsendDailyStats.ts","). The dashboard summary card reads the last 30 rows of this table instead of scanning every send.",[639,1523,1525],{"id":1524},"the-content-scan-gate","The content-scan gate",[448,1527,1528,1529,1532,1533,1536,1537,1540,1541,1544],{},"Before a campaign fans out, the orchestrator (",[464,1530,1531],{},"apps\u002Fapi\u002Fconvex\u002Fcampaigns\u002Fsend.ts",") runs the address through the email scanner. It combines the local content score (",[464,1534,1535],{},"scanContent"," from ",[464,1538,1539],{},"@owlat\u002Femail-scanner",") with an optional Google Safe Browsing URL-reputation pass (only when ",[464,1542,1543],{},"GOOGLE_SAFE_BROWSING_API_KEY"," is set; URL-check failures never block a send). The combined 0–100 score maps to three levels:",[497,1546,1547,1560],{},[500,1548,1549],{},[503,1550,1551,1554,1557],{},[506,1552,1553],{},"Score",[506,1555,1556],{},"Level",[506,1558,1559],{},"Outcome",[516,1561,1562,1583,1600],{},[503,1563,1564,1567,1572],{},[521,1565,1566],{},"≥ 40",[521,1568,1569],{},[464,1570,1571],{},"blocked",[521,1573,1574,1575,1578,1579,1582],{},"Campaign reverts to ",[464,1576,1577],{},"draft"," with a ",[464,1580,1581],{},"contentBlockReason","; send aborts",[503,1584,1585,1588,1593],{},[521,1586,1587],{},"15–39",[521,1589,1590],{},[464,1591,1592],{},"suspicious",[521,1594,1595,1596,1599],{},"Campaign transitions to ",[464,1597,1598],{},"pending_review"," for platform-admin review",[503,1601,1602,1605,1610],{},[521,1603,1604],{},"\u003C 15",[521,1606,1607],{},[464,1608,1609],{},"clean",[521,1611,1612],{},"Send proceeds",[448,1614,1615,1616,1619,1620,1623,1624,1627,1628,1631,1632,487],{},"Non-clean results are persisted to ",[464,1617,1618],{},"contentScanResults"," as an audit trail (keyed by ",[464,1621,1622],{},"resourceType"," + ",[464,1625,1626],{},"resourceId","). The same scanner backs attachment and media-upload validation elsewhere; URL verdicts are cached in ",[464,1629,1630],{},"urlReputationCache"," (24h for clean, 1h for flagged). For the security-scanning internals, see ",[460,1633,250],{"href":249},[469,1635,1637],{"id":1636},"custom-tracking-domains","Custom tracking domains",[448,1639,1640,1641,835,1644,1647,1648,1651,1652,1655,1656,1659,1660,1663,1664,1667,1668,633,1671,1295],{},"Open\u002Fclick links point at the deployment's own tracking host — the ",[464,1642,1643],{},"\u002Ft\u002Fo",[464,1645,1646],{},"\u002Ft\u002Fc"," HTTP actions, served at ",[464,1649,1650],{},"CONVEX_SITE_URL",". A deployment can ",[492,1653,1654],{},"register and DNS-verify"," a branded subdomain so that, eventually, tracked links carry its own domain rather than a shared host. Registration, admin-gated DNS verification, and the Settings surface are in place; the send-time link rewrite is ",[492,1657,1658],{},"not yet wired"," (see the second bullet). The ",[464,1661,1662],{},"trackingDomains"," table is managed in ",[464,1665,1666],{},"apps\u002Fapi\u002Fconvex\u002Fdomains\u002FtrackingDomains.ts"," and surfaced under ",[492,1669,1670],{},"Settings → Domains",[464,1672,1673],{},"TrackingDomainsSection.vue",[1297,1675,1676,1700],{},[826,1677,1678,1681,1682,1685,1686,1688,1689,1692,1693,1696,1697,487],{},[464,1679,1680],{},"addTrackingDomain"," records the subdomain with a ",[464,1683,1684],{},"cnameTarget"," derived from ",[464,1687,1650],{},"'s hostname — the host that actually serves the tracking handlers, never an external SaaS host. ",[464,1690,1691],{},"verifyTrackingDomain"," schedules a DNS-over-HTTPS (Cloudflare) CNAME check (",[464,1694,1695],{},"verifyTrackingDomainDns",") that flips the row to verified only when the CNAME resolves to that target. All three mutations require ",[464,1698,1699],{},"requireAdminContext",[826,1701,1702,1703,1706,1707,1710,1711,1714,1715,1718,1719,1722,1723,1726,1727,1729,1730,1733,1734,1736],{},"The internal ",[464,1704,1705],{},"getActiveTrackingDomain"," query (",[464,1708,1709],{},"trackingDomains.ts",") exposes the first verified row to the send pipeline, ",[492,1712,1713],{},"but no producer consumes it yet",". ",[464,1716,1717],{},"delivery\u002Fworker.ts"," derives ",[464,1720,1721],{},"trackingBaseUrl"," as ",[464,1724,1725],{},"envelopeInput.trackingBaseUrl ?? convexSiteUrl",", and nothing on the campaign send path populates ",[464,1728,1721],{}," from a tracking domain (",[464,1731,1732],{},"apps\u002Fapi\u002Fconvex\u002Fcampaigns\u002F"," has zero references to it). So tracked links still default to ",[464,1735,1650],{}," regardless of any verified domain — the branding half of this feature is registered and verified but not yet rendered into links.",[469,1738,1740],{"id":1739},"related","Related",[1742,1743],"link-card",{"description":1744,"title":238,"to":237},"Sending-side delivery: per-ISP throttling, the IP warming schedule, circuit breakers, DNSBL monitoring.",[1742,1746],{"description":1747,"title":258,"to":257},"The pluggable provider abstractions (LLM, email, notifications) and how to add a new send provider.",[1742,1749],{"description":1750,"title":1751,"to":61},"The user-facing view of reputation, warming, and inbox placement.","Deliverability (Guide)",{"title":1753,"searchDepth":1754,"depth":1754,"links":1755},"",2,[1756,1760,1763,1768,1769,1770,1774,1775],{"id":471,"depth":1754,"text":472,"children":1757},[1758],{"id":641,"depth":1759,"text":642},3,{"id":798,"depth":1754,"text":799,"children":1761},[1762],{"id":869,"depth":1759,"text":870},{"id":952,"depth":1754,"text":953,"children":1764},[1765,1766,1767],{"id":987,"depth":1759,"text":988},{"id":1085,"depth":1759,"text":1086},{"id":1168,"depth":1759,"text":1169},{"id":1235,"depth":1754,"text":1236},{"id":1325,"depth":1754,"text":1326},{"id":1445,"depth":1754,"text":1446,"children":1771},[1772,1773],{"id":1449,"depth":1759,"text":1450},{"id":1524,"depth":1759,"text":1525},{"id":1636,"depth":1754,"text":1637},{"id":1739,"depth":1754,"text":1740},"md",{},true,{"title":274,"description":275},"3.developer\u002F19.deliverability-infrastructure","oZS__AOSWw42_vjW3WZcWRvnIPe1PC20aDJIMaiXUyU",[1783,1785],{"title":270,"path":269,"stem":1784,"children":-1},"3.developer\u002F18.automation-internals",{"title":278,"path":277,"stem":1786,"children":-1},"3.developer\u002F2.architecture",1782846428157]