[{"data":1,"prerenderedAt":1905},["ShallowReactive",2],{"search":3,"content-developer\u002Fproviders":442,"surround-\u002Fdeveloper\u002Fproviders":1900},[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":258,"body":444,"description":259,"extension":1895,"meta":1896,"navigation":778,"path":257,"seo":1897,"stem":1898,"__hash__":1899},"content\u002F3.developer\u002F15.providers.md",{"type":445,"value":446,"toc":1882},"minimark",[447,456,459,482,521,526,533,543,546,559,573,580,587,792,802,853,856,944,972,988,992,1022,1036,1050,1055,1064,1200,1211,1215,1225,1451,1506,1510,1519,1549,1582,1586,1635,1652,1676,1680,1697,1705,1709,1741,1772,1776,1878],[448,449,450,451,455],"p",{},"Wherever Owlat talks to an external system — an LLM, an email delivery backend, a notification transport, a vector store, an analytics sink — it goes through a ",[452,453,454],"strong",{},"provider abstraction",". Most are factories that read a single env var to pick an implementation, cache it per-process, and expose a small, stable interface to the rest of the codebase.",[448,457,458],{},"The point: self-hosters swap providers without code changes, and we can add new ones without touching every call site.",[448,460,461,462,465,466,469,470,474,475,477,478,481],{},"Two pluggable provider abstractions ship: the ",[452,463,464],{},"LLM provider"," and the ",[452,467,468],{},"send (email) provider",", both fully consumed by the backend (agent steps, knowledge extraction, translation, every send path). The LLM provider reads one env var (",[471,472,473],"code",{},"LLM_PROVIDER",") and caches the resolved client per-process. The ",[452,476,468],{}," is the exception: it uses a static registry keyed by provider ",[471,479,480],{},"kind"," (passed in per call) instead of reading one env var and caching the resolved instance. That difference is called out below.",[483,484,487],"callout",{"title":485,"type":486},"No notification, vector-store, or analytics provider abstraction","info",[448,488,489,490,493,494,497,498,501,502,505,506,509,510,513,514,513,517,520],{},"Earlier drafts of this page described ",[452,491,492],{},"notification",", ",[452,495,496],{},"vector-store",", and ",[452,499,500],{},"analytics"," provider factories. Those were unused speculative seams and have been ",[452,503,504],{},"removed",". The runtime handles those concerns directly instead: notifications are client-managed, knowledge retrieval queries Convex's built-in vector index, and product analytics flow through ",[471,507,508],{},"lib\u002Fposthog.ts",". There are no ",[471,511,512],{},"NOTIFICATION_PROVIDER"," \u002F ",[471,515,516],{},"VECTOR_STORE",[471,518,519],{},"ANALYTICS_PROVIDER"," env vars.",[522,523,525],"h2",{"id":524},"factory-layout","Factory layout",[448,527,528,529,532],{},"The two shipping abstractions live under ",[471,530,531],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002F",":",[534,535,540],"pre",{"className":536,"code":538,"language":539},[537],"language-text","lib\u002F\n├── llmProvider.ts          ← LLM_PROVIDER — single module\n│                              exports getLLMProvider(task) \u002F getEmbeddingModel() \u002F getLLMConfig()\n├── sendProviders\u002F          ← registry keyed by SendProviderKind (not one env var)\n│   ├── types.ts            ← SendProviderModule\u003CK>, EmailSendAttempt, EmailErrorCode\n│   ├── index.ts            ← SEND_PROVIDERS registry + providerFor(kind)\n│   ├── dispatch.ts         ← sendProviderDispatch() — owns the retry loop\n│   ├── routing.ts          ← resolveRoute() — per-org route selection\n│   ├── health.ts           ← providerHealth recording + reads\n│   ├── mta\u002Findex.ts        ← built-in MTA adapter\n│   ├── ses\u002Findex.ts        ← Amazon SES adapter\n│   ├── resend\u002Findex.ts     ← Resend adapter\n│   └── strategies\u002F         ← single \u002F priority_failover \u002F workload_split\n└── emailProviders\u002F         ← identity & domain verification only (NOT the send factory)\n    ├── domainVerification.ts\n    ├── mtaIdentity.ts\n    └── sesIdentity.ts\n","text",[471,541,538],{"__ignoreMap":542},"",[522,544,464],{"id":545},"llm-provider",[448,547,548,551,552,554,555,558],{},[452,549,550],{},"Env var:"," ",[471,553,473],{}," (default: ",[471,556,557],{},"openai",")",[448,560,561,551,564,566,567,493,570],{},[452,562,563],{},"Supported values:",[471,565,557],{}," (default), ",[471,568,569],{},"openrouter",[471,571,572],{},"ollama",[448,574,575,576,579],{},"All three speak the OpenAI Chat Completions shape (so anything OpenAI-compatible plugs in — a self-hosted vLLM or LM Studio, for example). Every client is built with ",[471,577,578],{},"createOpenAI"," from the Vercel AI SDK.",[448,581,582,583,586],{},"The whole abstraction is a single module, ",[471,584,585],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FllmProvider.ts",", exposing standalone functions:",[534,588,592],{"className":589,"code":590,"language":591,"meta":542,"style":542},"language-ts shiki shiki-themes github-light github-dark-dimmed","\u002F\u002F apps\u002Fapi\u002Fconvex\u002Flib\u002FllmProvider.ts\nexport function getLLMProvider(task: LLMTask): LanguageModel;   \u002F\u002F returns AI SDK LanguageModel\nexport function getEmbeddingModel(): EmbeddingModel;\nexport function getLLMConfig(): {\n  provider: string;\n  modelFast: string;\n  modelCapable: string;\n  embeddingModel: string;\n  baseURL: string | undefined;\n  hasApiKey: boolean;        \u002F\u002F snapshot — never the key itself — safe to log\n};\n\n\u002F\u002F task tiers — classify\u002Fextract\u002Fguard\u002Fsummarize → fast model\n\u002F\u002F              draft\u002Fplan                       → capable model\n","ts",[471,593,594,603,644,665,682,696,708,720,732,750,767,773,780,786],{"__ignoreMap":542},[595,596,599],"span",{"class":597,"line":598},"line",1,[595,600,602],{"class":601},"sDN9O","\u002F\u002F apps\u002Fapi\u002Fconvex\u002Flib\u002FllmProvider.ts\n",[595,604,606,610,613,617,621,625,627,631,633,635,638,641],{"class":597,"line":605},2,[595,607,609],{"class":608},"s7YZ4","export",[595,611,612],{"class":608}," function",[595,614,616],{"class":615},"sPO5f"," getLLMProvider",[595,618,620],{"class":619},"sYgZi","(",[595,622,624],{"class":623},"stnAF","task",[595,626,532],{"class":608},[595,628,630],{"class":629},"sOLd2"," LLMTask",[595,632,558],{"class":619},[595,634,532],{"class":608},[595,636,637],{"class":629}," LanguageModel",[595,639,640],{"class":619},";   ",[595,642,643],{"class":601},"\u002F\u002F returns AI SDK LanguageModel\n",[595,645,647,649,651,654,657,659,662],{"class":597,"line":646},3,[595,648,609],{"class":608},[595,650,612],{"class":608},[595,652,653],{"class":615}," getEmbeddingModel",[595,655,656],{"class":619},"()",[595,658,532],{"class":608},[595,660,661],{"class":629}," EmbeddingModel",[595,663,664],{"class":619},";\n",[595,666,668,670,672,675,677,679],{"class":597,"line":667},4,[595,669,609],{"class":608},[595,671,612],{"class":608},[595,673,674],{"class":615}," getLLMConfig",[595,676,656],{"class":619},[595,678,532],{"class":608},[595,680,681],{"class":619}," {\n",[595,683,685,688,690,694],{"class":597,"line":684},5,[595,686,687],{"class":623},"  provider",[595,689,532],{"class":608},[595,691,693],{"class":692},"sviXB"," string",[595,695,664],{"class":619},[595,697,699,702,704,706],{"class":597,"line":698},6,[595,700,701],{"class":623},"  modelFast",[595,703,532],{"class":608},[595,705,693],{"class":692},[595,707,664],{"class":619},[595,709,711,714,716,718],{"class":597,"line":710},7,[595,712,713],{"class":623},"  modelCapable",[595,715,532],{"class":608},[595,717,693],{"class":692},[595,719,664],{"class":619},[595,721,723,726,728,730],{"class":597,"line":722},8,[595,724,725],{"class":623},"  embeddingModel",[595,727,532],{"class":608},[595,729,693],{"class":692},[595,731,664],{"class":619},[595,733,735,738,740,742,745,748],{"class":597,"line":734},9,[595,736,737],{"class":623},"  baseURL",[595,739,532],{"class":608},[595,741,693],{"class":692},[595,743,744],{"class":608}," |",[595,746,747],{"class":692}," undefined",[595,749,664],{"class":619},[595,751,753,756,758,761,764],{"class":597,"line":752},10,[595,754,755],{"class":623},"  hasApiKey",[595,757,532],{"class":608},[595,759,760],{"class":692}," boolean",[595,762,763],{"class":619},";        ",[595,765,766],{"class":601},"\u002F\u002F snapshot — never the key itself — safe to log\n",[595,768,770],{"class":597,"line":769},11,[595,771,772],{"class":619},"};\n",[595,774,776],{"class":597,"line":775},12,[595,777,779],{"emptyLinePlaceholder":778},true,"\n",[595,781,783],{"class":597,"line":782},13,[595,784,785],{"class":601},"\u002F\u002F task tiers — classify\u002Fextract\u002Fguard\u002Fsummarize → fast model\n",[595,787,789],{"class":597,"line":788},14,[595,790,791],{"class":601},"\u002F\u002F              draft\u002Fplan                       → capable model\n",[448,793,794,795,798,799,801],{},"To run Claude models, point the OpenAI client at an OpenAI-compatible endpoint — there is no native Anthropic provider, and ",[471,796,797],{},"anthropic"," is not a recognized ",[471,800,473],{}," value:",[534,803,807],{"className":804,"code":805,"language":806,"meta":542,"style":542},"language-sh shiki shiki-themes github-light github-dark-dimmed","LLM_PROVIDER=openai\nLLM_BASE_URL=https:\u002F\u002Fapi.anthropic.com\u002Fv1\u002F   # or an OpenAI-compat shim\nLLM_MODEL_CAPABLE=claude-sonnet-4-6\nLLM_MODEL_FAST=claude-haiku-4-5-20251001\n","sh",[471,808,809,820,833,843],{"__ignoreMap":542},[595,810,811,813,816],{"class":597,"line":598},[595,812,473],{"class":619},[595,814,815],{"class":608},"=",[595,817,819],{"class":818},"s-HuK","openai\n",[595,821,822,825,827,830],{"class":597,"line":605},[595,823,824],{"class":619},"LLM_BASE_URL",[595,826,815],{"class":608},[595,828,829],{"class":818},"https:\u002F\u002Fapi.anthropic.com\u002Fv1\u002F",[595,831,832],{"class":601},"   # or an OpenAI-compat shim\n",[595,834,835,838,840],{"class":597,"line":646},[595,836,837],{"class":619},"LLM_MODEL_CAPABLE",[595,839,815],{"class":608},[595,841,842],{"class":818},"claude-sonnet-4-6\n",[595,844,845,848,850],{"class":597,"line":667},[595,846,847],{"class":619},"LLM_MODEL_FAST",[595,849,815],{"class":608},[595,851,852],{"class":818},"claude-haiku-4-5-20251001\n",[448,854,855],{},"Required env vars depend on which provider is selected:",[857,858,859,875],"table",{},[860,861,862],"thead",{},[863,864,865,869,872],"tr",{},[866,867,868],"th",{},"Provider",[866,870,871],{},"Required env",[866,873,874],{},"Optional",[876,877,878,905,926],"tbody",{},[863,879,880,885,894],{},[881,882,883],"td",{},[471,884,557],{},[881,886,887,890,891,558],{},[471,888,889],{},"LLM_API_KEY"," (or ",[471,892,893],{},"OPENAI_API_KEY",[881,895,896,493,898,493,900,493,903],{},[471,897,847],{},[471,899,837],{},[471,901,902],{},"LLM_EMBEDDING_MODEL",[471,904,824],{},[863,906,907,911,918],{},[881,908,909],{},[471,910,569],{},[881,912,913,890,915,558],{},[471,914,889],{},[471,916,917],{},"OPENROUTER_API_KEY",[881,919,920,493,922,493,924],{},[471,921,847],{},[471,923,837],{},[471,925,824],{},[863,927,928,932,935],{},[881,929,930],{},[471,931,572],{},[881,933,934],{},"—",[881,936,937,939,940,943],{},[471,938,824],{}," (default ",[471,941,942],{},"http:\u002F\u002Follama:11434\u002Fv1","), model overrides",[448,945,946,947,493,949,951,952,954,955,958,959,961,962,964,965,967,968,971],{},"All OpenAI-compatible providers accept any of ",[471,948,889],{},[471,950,917],{},", or ",[471,953,893],{}," (first set wins — see ",[471,956,957],{},"resolveApiKey()"," in ",[471,960,585],{},"). The ",[471,963,572],{}," default base URL is the Docker service hostname ",[471,966,942],{},", resolved in ",[471,969,970],{},"resolveBaseURL()"," in the same file.",[448,973,974,975,978,979,981,982,984,985,987],{},"The ",[471,976,977],{},"ai"," feature flag requires ",[471,980,473],{}," and ",[471,983,889],{}," to be set. The admin UI blocks toggling ",[471,986,977],{}," on until they exist.",[522,989,991],{"id":990},"send-email-provider","Send (email) provider",[448,993,994,995,998,999,1002,1003,1005,1006,1009,1010,1013,1014,1017,1018,1021],{},"The send-side abstraction lives under ",[471,996,997],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FsendProviders\u002F"," (per ADR-0020). Unlike the other factories it is ",[452,1000,1001],{},"not"," selected by reading one env var into a cached singleton — instead it is a static registry keyed by provider ",[471,1004,480],{},", and the kind is resolved per send (from the org's route config, falling back to the ",[471,1007,1008],{},"EMAIL_PROVIDER"," env var, then to ",[452,1011,1012],{},"unconfigured"," when neither names a provider). Resolution is ",[452,1015,1016],{},"fail-closed",": there is no implicit MTA default, so a send that reaches dispatch without a configured provider is refused rather than dispatched to a phantom MTA. The send entry points gate on an ",[471,1019,1020],{},"isDeliveryConfigured"," capability check first (so a campaign or transactional send is rejected before any row is written), making this routing fallback defence-in-depth.",[448,1023,1024,551,1027,493,1030,493,1033],{},[452,1025,1026],{},"Supported kinds:",[471,1028,1029],{},"mta",[471,1031,1032],{},"resend",[471,1034,1035],{},"ses",[448,1037,1038,1040,1041,1044,1045,981,1047,1049],{},[471,1039,1029],{}," is the built-in custom sender that ships with self-host (",[471,1042,1043],{},"apps\u002Fmta","). ",[471,1046,1032],{},[471,1048,1035],{}," are alternatives — useful if you don't want to run an MTA, or for transactional traffic specifically while keeping campaigns on the MTA.",[1051,1052,1054],"h3",{"id":1053},"registry-lookup","Registry & lookup",[448,1056,1057,1060,1061,532],{},[471,1058,1059],{},"sendProviders\u002Findex.ts"," exports the registry and the lookup helpers — there is no ",[471,1062,1063],{},"getEmailProvider()",[534,1065,1067],{"className":589,"code":1066,"language":591,"meta":542,"style":542},"export const SEND_PROVIDERS = {\n  mta: mtaSendProvider,\n  ses: sesSendProvider,\n  resend: resendSendProvider,\n} as const;\n\nexport function providerFor\u003CK extends SendProviderKind>(kind: K): SendProviderModule\u003CK>;\nexport function isSendProviderKind(kind: string | undefined | null): kind is SendProviderKind;\n",[471,1068,1069,1084,1089,1094,1099,1111,1115,1160],{"__ignoreMap":542},[595,1070,1071,1073,1076,1079,1082],{"class":597,"line":598},[595,1072,609],{"class":608},[595,1074,1075],{"class":608}," const",[595,1077,1078],{"class":692}," SEND_PROVIDERS",[595,1080,1081],{"class":608}," =",[595,1083,681],{"class":619},[595,1085,1086],{"class":597,"line":605},[595,1087,1088],{"class":619},"  mta: mtaSendProvider,\n",[595,1090,1091],{"class":597,"line":646},[595,1092,1093],{"class":619},"  ses: sesSendProvider,\n",[595,1095,1096],{"class":597,"line":667},[595,1097,1098],{"class":619},"  resend: resendSendProvider,\n",[595,1100,1101,1104,1107,1109],{"class":597,"line":684},[595,1102,1103],{"class":619},"} ",[595,1105,1106],{"class":608},"as",[595,1108,1075],{"class":608},[595,1110,664],{"class":619},[595,1112,1113],{"class":597,"line":698},[595,1114,779],{"emptyLinePlaceholder":778},[595,1116,1117,1119,1121,1124,1127,1130,1133,1136,1139,1141,1143,1146,1148,1150,1153,1155,1157],{"class":597,"line":710},[595,1118,609],{"class":608},[595,1120,612],{"class":608},[595,1122,1123],{"class":615}," providerFor",[595,1125,1126],{"class":619},"\u003C",[595,1128,1129],{"class":629},"K",[595,1131,1132],{"class":608}," extends",[595,1134,1135],{"class":629}," SendProviderKind",[595,1137,1138],{"class":619},">(",[595,1140,480],{"class":623},[595,1142,532],{"class":608},[595,1144,1145],{"class":629}," K",[595,1147,558],{"class":619},[595,1149,532],{"class":608},[595,1151,1152],{"class":629}," SendProviderModule",[595,1154,1126],{"class":619},[595,1156,1129],{"class":629},[595,1158,1159],{"class":619},">;\n",[595,1161,1162,1164,1166,1169,1171,1173,1175,1177,1179,1181,1183,1186,1188,1190,1193,1196,1198],{"class":597,"line":722},[595,1163,609],{"class":608},[595,1165,612],{"class":608},[595,1167,1168],{"class":615}," isSendProviderKind",[595,1170,620],{"class":619},[595,1172,480],{"class":623},[595,1174,532],{"class":608},[595,1176,693],{"class":692},[595,1178,744],{"class":608},[595,1180,747],{"class":692},[595,1182,744],{"class":608},[595,1184,1185],{"class":692}," null",[595,1187,558],{"class":619},[595,1189,532],{"class":608},[595,1191,1192],{"class":623}," kind",[595,1194,1195],{"class":608}," is",[595,1197,1135],{"class":629},[595,1199,664],{"class":619},[448,1201,1202,1203,1206,1207,1210],{},"A compile-time ",[471,1204,1205],{},"satisfies","-style mapped-type check pins each registry value to ",[471,1208,1209],{},"SendProviderModule\u003CthatKind>",", so a missing method fails the build.",[1051,1212,1214],{"id":1213},"adapter-interface","Adapter interface",[448,1216,1217,1218,1221,1222,532],{},"Each adapter implements ",[471,1219,1220],{},"SendProviderModule\u003CK>"," from ",[471,1223,1224],{},"sendProviders\u002Ftypes.ts",[534,1226,1228],{"className":589,"code":1227,"language":591,"meta":542,"style":542},"export interface SendProviderModule\u003CK extends SendProviderKind> {\n  readonly kind: K;\n  readonly retryDelays: readonly number[];                 \u002F\u002F backoff schedule; dispatch owns the loop\n  sendEmail(params: EmailSendParams, extras?: ExtrasFor\u003CK>): Promise\u003CEmailSendAttempt>;\n  categorizeError(message: string, httpStatus?: number): EmailErrorCode;\n}\n\n\u002F\u002F Single-attempt result — no internal retry:\nexport type EmailSendAttempt =\n  | { success: true; id: string }\n  | { success: false; errorMessage: string; errorCode: EmailErrorCode };\n",[471,1229,1230,1250,1263,1284,1329,1361,1366,1370,1375,1388,1417],{"__ignoreMap":542},[595,1231,1232,1234,1237,1239,1241,1243,1245,1247],{"class":597,"line":598},[595,1233,609],{"class":608},[595,1235,1236],{"class":608}," interface",[595,1238,1152],{"class":629},[595,1240,1126],{"class":619},[595,1242,1129],{"class":629},[595,1244,1132],{"class":608},[595,1246,1135],{"class":629},[595,1248,1249],{"class":619},"> {\n",[595,1251,1252,1255,1257,1259,1261],{"class":597,"line":605},[595,1253,1254],{"class":608},"  readonly",[595,1256,1192],{"class":623},[595,1258,532],{"class":608},[595,1260,1145],{"class":629},[595,1262,664],{"class":619},[595,1264,1265,1267,1270,1272,1275,1278,1281],{"class":597,"line":646},[595,1266,1254],{"class":608},[595,1268,1269],{"class":623}," retryDelays",[595,1271,532],{"class":608},[595,1273,1274],{"class":608}," readonly",[595,1276,1277],{"class":692}," number",[595,1279,1280],{"class":619},"[];                 ",[595,1282,1283],{"class":601},"\u002F\u002F backoff schedule; dispatch owns the loop\n",[595,1285,1286,1289,1291,1294,1296,1299,1301,1304,1307,1310,1312,1314,1317,1319,1322,1324,1327],{"class":597,"line":667},[595,1287,1288],{"class":615},"  sendEmail",[595,1290,620],{"class":619},[595,1292,1293],{"class":623},"params",[595,1295,532],{"class":608},[595,1297,1298],{"class":629}," EmailSendParams",[595,1300,493],{"class":619},[595,1302,1303],{"class":623},"extras",[595,1305,1306],{"class":608},"?:",[595,1308,1309],{"class":629}," ExtrasFor",[595,1311,1126],{"class":619},[595,1313,1129],{"class":629},[595,1315,1316],{"class":619},">)",[595,1318,532],{"class":608},[595,1320,1321],{"class":629}," Promise",[595,1323,1126],{"class":619},[595,1325,1326],{"class":629},"EmailSendAttempt",[595,1328,1159],{"class":619},[595,1330,1331,1334,1336,1339,1341,1343,1345,1348,1350,1352,1354,1356,1359],{"class":597,"line":684},[595,1332,1333],{"class":615},"  categorizeError",[595,1335,620],{"class":619},[595,1337,1338],{"class":623},"message",[595,1340,532],{"class":608},[595,1342,693],{"class":692},[595,1344,493],{"class":619},[595,1346,1347],{"class":623},"httpStatus",[595,1349,1306],{"class":608},[595,1351,1277],{"class":692},[595,1353,558],{"class":619},[595,1355,532],{"class":608},[595,1357,1358],{"class":629}," EmailErrorCode",[595,1360,664],{"class":619},[595,1362,1363],{"class":597,"line":698},[595,1364,1365],{"class":619},"}\n",[595,1367,1368],{"class":597,"line":710},[595,1369,779],{"emptyLinePlaceholder":778},[595,1371,1372],{"class":597,"line":722},[595,1373,1374],{"class":601},"\u002F\u002F Single-attempt result — no internal retry:\n",[595,1376,1377,1379,1382,1385],{"class":597,"line":734},[595,1378,609],{"class":608},[595,1380,1381],{"class":608}," type",[595,1383,1384],{"class":629}," EmailSendAttempt",[595,1386,1387],{"class":608}," =\n",[595,1389,1390,1393,1396,1399,1401,1404,1407,1410,1412,1414],{"class":597,"line":752},[595,1391,1392],{"class":608},"  |",[595,1394,1395],{"class":619}," { ",[595,1397,1398],{"class":623},"success",[595,1400,532],{"class":608},[595,1402,1403],{"class":692}," true",[595,1405,1406],{"class":619},"; ",[595,1408,1409],{"class":623},"id",[595,1411,532],{"class":608},[595,1413,693],{"class":692},[595,1415,1416],{"class":619}," }\n",[595,1418,1419,1421,1423,1425,1427,1430,1432,1435,1437,1439,1441,1444,1446,1448],{"class":597,"line":769},[595,1420,1392],{"class":608},[595,1422,1395],{"class":619},[595,1424,1398],{"class":623},[595,1426,532],{"class":608},[595,1428,1429],{"class":692}," false",[595,1431,1406],{"class":619},[595,1433,1434],{"class":623},"errorMessage",[595,1436,532],{"class":608},[595,1438,693],{"class":692},[595,1440,1406],{"class":619},[595,1442,1443],{"class":623},"errorCode",[595,1445,532],{"class":608},[595,1447,1358],{"class":629},[595,1449,1450],{"class":619}," };\n",[448,1452,1453,1456,1457,1459,1460,1463,1464,1467,1468,1467,1471,1474,1475,1478,1479,493,1482,493,1485,493,1488,493,1491,493,1494,493,1497,1500,1501,981,1503,1505],{},[471,1454,1455],{},"sendEmail"," performs exactly one attempt and never retries internally. ",[471,1458,1303],{}," is a typed per-provider second arg (",[471,1461,1462],{},"MtaExtras"," carries ",[471,1465,1466],{},"ipPool","\u002F",[471,1469,1470],{},"engagementScore",[471,1472,1473],{},"dkimDomain","; SES and Resend take none). ",[471,1476,1477],{},"EmailErrorCode"," is an enum (",[471,1480,1481],{},"RATE_LIMIT",[471,1483,1484],{},"SERVER_ERROR",[471,1486,1487],{},"INVALID_RECIPIENT",[471,1489,1490],{},"INVALID_SENDER",[471,1492,1493],{},"AUTH_FAILED",[471,1495,1496],{},"CONTENT_REJECTED",[471,1498,1499],{},"UNKNOWN","); only ",[471,1502,1481],{},[471,1504,1484],{}," are retryable.",[1051,1507,1509],{"id":1508},"dispatch","Dispatch",[448,1511,1512,1513,958,1516,532],{},"The single public entry point is ",[471,1514,1515],{},"sendProviderDispatch()",[471,1517,1518],{},"sendProviders\u002Fdispatch.ts",[534,1520,1522],{"className":589,"code":1521,"language":591,"meta":542,"style":542},"sendProviderDispatch(ctx, kind, params, extras?): Promise\u003CDispatchResult>\n",[471,1523,1524],{"__ignoreMap":542},[595,1525,1526,1529,1532,1535,1537,1539,1541,1543,1546],{"class":597,"line":598},[595,1527,1528],{"class":615},"sendProviderDispatch",[595,1530,1531],{"class":619},"(ctx, kind, params, extras",[595,1533,1534],{"class":608},"?",[595,1536,558],{"class":619},[595,1538,532],{"class":608},[595,1540,1321],{"class":692},[595,1542,1126],{"class":608},[595,1544,1545],{"class":619},"DispatchResult",[595,1547,1548],{"class":608},">\n",[448,1550,1551,1552,1555,1556,1559,1560,1563,1564,1566,1567,1569,1570,1573,1574,1577,1578,1581],{},"It owns the retry loop (driven by the module's ",[471,1553,1554],{},"retryDelays"," + ",[471,1557,1558],{},"categorizeError","), and after every terminal outcome — success or exhausted retries — it records provider health by scheduling ",[471,1561,1562],{},"internal.lib.sendProviders.health.recordSendResult",". ",[471,1565,1545],{}," carries the final ",[471,1568,1326],{},", the ",[471,1571,1572],{},"providerType"," used, total ",[471,1575,1576],{},"latencyMs",", and the number of ",[471,1579,1580],{},"attempts",".",[1051,1583,1585],{"id":1584},"routing-health","Routing & health",[448,1587,1588,958,1591,1594,1595,1597,1598,1601,1602,1605,1606,493,1609,951,1612,1615,1616,1618,1619,1622,1623,1625,1626,1628,1629,551,1632,1634],{},[471,1589,1590],{},"resolveRoute()",[471,1592,1593],{},"sendProviders\u002Frouting.ts"," picks the provider ",[471,1596,480],{}," for a send from an org's ",[471,1599,1600],{},"providerRoutes"," config, dispatching to a strategy module under ",[471,1603,1604],{},"sendProviders\u002Fstrategies\u002F"," — ",[471,1607,1608],{},"single",[471,1610,1611],{},"priority_failover",[471,1613,1614],{},"workload_split",". It falls back to the ",[471,1617,1008],{}," env var, then returns ",[471,1620,1621],{},"null"," (",[452,1624,1012],{},") — never a phantom ",[471,1627,1029],{}," — when there is no config, no enabled providers, the strategy returns null, ",[452,1630,1631],{},"and",[471,1633,1008],{}," names no provider.",[448,1636,1637,1638,1641,1642,1644,1645,1647,1648,1651],{},"Admins configure these routes per message type (transactional, campaigns, automations) under ",[452,1639,1640],{},"Settings → Technical → Provider Routing"," in the dashboard — pick the strategy, order the providers, set workload-split weights, and optionally pin an IP pool. With no route configured, every send uses the ",[471,1643,1008],{}," value; with neither a route nor ",[471,1646,1008],{},", the send is refused (no delivery provider configured). See ",[1649,1650,158],"a",{"href":157}," for which deployment shapes need a provider.",[448,1653,1654,1657,1658,1467,1661,1467,1664,1667,1668,1671,1672,1675],{},[471,1655,1656],{},"sendProviders\u002Fhealth.ts"," records rolling success\u002Ffailure counts, average latency, and a ",[471,1659,1660],{},"healthy",[471,1662,1663],{},"degraded",[471,1665,1666],{},"down"," status per provider into the ",[471,1669,1670],{},"providerHealth"," table. Dispatch is the only writer; ",[471,1673,1674],{},"resolveRoute"," is the only reader of the all-providers snapshot.",[1051,1677,1679],{"id":1678},"identity-domain-verification","Identity & domain verification",[448,1681,1682,1683,1686,1687,493,1690,493,1693,1696],{},"The older ",[471,1684,1685],{},"lib\u002FemailProviders\u002F"," directory still exists, but it now holds only identity and domain-verification helpers — ",[471,1688,1689],{},"domainVerification.ts",[471,1691,1692],{},"mtaIdentity.ts",[471,1694,1695],{},"sesIdentity.ts"," — not the send factory.",[448,1698,1699,1700,1704],{},"See ",[1649,1701,1703],{"href":1702},"\u002Fdeveloper\u002Fenvironment-variables#email-sending","Environment Variables → Email Sending"," for the per-provider env vars.",[522,1706,1708],{"id":1707},"adding-a-new-provider","Adding a new provider",[448,1710,1711,1714,1715,1718,1719,1721,1722,1724,1725,1727,1728,1731,1732,958,1735,1738,1739,1581],{},[452,1712,1713],{},"LLM:"," because every supported client speaks the OpenAI shape, most \"new providers\" need no code at all — set ",[471,1716,1717],{},"LLM_PROVIDER=openai"," and point ",[471,1720,824],{}," at the OpenAI-compatible endpoint. For a genuinely non-OpenAI-compatible backend you would edit the single ",[471,1723,585],{}," module directly (e.g. add a base-URL case in ",[471,1726,970],{}," and, if it needs a different SDK client, branch in ",[471,1729,1730],{},"getClient()","). If the provider needs new env vars, declare them in ",[471,1733,1734],{},"FEATURE_FLAGS[\u003Cflag>].requiredEnvVars",[471,1736,1737],{},"packages\u002Fshared\u002Fsrc\u002FfeatureFlags.ts"," so the wizard prompts for them, and document them in ",[1649,1740,333],{"href":332},[448,1742,1743,1746,1747,1750,1751,1754,1755,958,1758,1760,1761,1764,1765,1767,1768,1771],{},[452,1744,1745],{},"Send (email):"," to add a fourth sender, create a subfolder ",[471,1748,1749],{},"lib\u002FsendProviders\u002F\u003Ckind>\u002Findex.ts"," implementing ",[471,1752,1753],{},"SendProviderModule\u003C'\u003Ckind>'>",", add the literal to ",[471,1756,1757],{},"SendProviderKind",[471,1759,1224],{},", and add one entry to the ",[471,1762,1763],{},"SEND_PROVIDERS"," registry in ",[471,1766,1059],{},". There is no ",[471,1769,1770],{},"switch"," — the compile-time mapped-type check enforces the shape, so a missing method fails the build. No call sites change.",[522,1773,1775],{"id":1774},"design-notes","Design notes",[1777,1778,1779,1807,1827,1855],"ul",{},[1780,1781,1782,1785,1786,958,1789,1792,1793,1796,1797,1800,1801,1803,1804,1806],"li",{},[452,1783,1784],{},"Caching",": the LLM client is cached at module level (",[471,1787,1788],{},"cachedClient",[471,1790,1791],{},"lib\u002FllmProvider.ts",") so each process resolves it once. There is no exported ",[471,1794,1795],{},"clear*Cache()"," — tests reset the module state with ",[471,1798,1799],{},"vi.resetModules()",". The send provider has no cache at all: it uses the static ",[471,1802,1763],{}," registry with the ",[471,1805,480],{}," passed per call.",[1780,1808,1809,1812,1813,1816,1817,1820,1821,1823,1824,1826],{},[452,1810,1811],{},"Throwing on unknown",": ",[471,1814,1815],{},"providerFor"," throws a descriptive ",[471,1818,1819],{},"Unknown send provider: \u003Ckind>"," error, so a typo fails loudly instead of silently falling back. (An unrecognized ",[471,1822,473],{}," value — e.g. ",[471,1825,797],{}," — silently falls through to the default OpenAI client with no base URL.)",[1780,1828,1829,1812,1835,1837,1838,1841,1842,1845,1846,493,1848,493,1850,497,1852,1854],{},[452,1830,1831,1832],{},"No secrets in ",[471,1833,1834],{},"getLLMConfig()",[471,1836,1834],{}," returns a snapshot of resolved settings with ",[471,1839,1840],{},"hasApiKey: boolean"," rather than the key itself, safe to log. (The send provider has no config getter — ",[471,1843,1844],{},"SendProviderModule"," exposes only ",[471,1847,480],{},[471,1849,1554],{},[471,1851,1455],{},[471,1853,1558],{},".)",[1780,1856,1857,1812,1860,1863,1864,1866,1867,1870,1871,1406,1874,1877],{},[452,1858,1859],{},"Embeddings",[471,1861,1862],{},"getEmbeddingModel()"," always resolves a single OpenAI-compatible embedding model (",[471,1865,902],{},", default ",[471,1868,1869],{},"text-embedding-3-small",") and throws only on a known dimension mismatch against ",[471,1872,1873],{},"EMBEDDING_DIMENSIONS",[471,1875,1876],{},"assertEmbeddingDimension"," enforces the same at write time. There is no separate embedding-provider env var.",[1879,1880,1881],"style",{},"html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}",{"title":542,"searchDepth":605,"depth":605,"links":1883},[1884,1885,1886,1893,1894],{"id":524,"depth":605,"text":525},{"id":545,"depth":605,"text":464},{"id":990,"depth":605,"text":991,"children":1887},[1888,1889,1890,1891,1892],{"id":1053,"depth":646,"text":1054},{"id":1213,"depth":646,"text":1214},{"id":1508,"depth":646,"text":1509},{"id":1584,"depth":646,"text":1585},{"id":1678,"depth":646,"text":1679},{"id":1707,"depth":605,"text":1708},{"id":1774,"depth":605,"text":1775},"md",{},{"title":258,"description":259},"3.developer\u002F15.providers","JDXLZkKvaf1zeUwlY2usnQdesMcFpicEHOcdnntgbpo",[1901,1903],{"title":254,"path":253,"stem":1902,"children":-1},"3.developer\u002F14.postbox-architecture",{"title":262,"path":261,"stem":1904,"children":-1},"3.developer\u002F16.campaign-internals",1782846427962]