[{"data":1,"prerenderedAt":1401},["ShallowReactive",2],{"search":3,"content-developer\u002Ffeature-flags":442,"surround-\u002Fdeveloper\u002Ffeature-flags":1396},[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":242,"body":444,"description":243,"extension":1390,"meta":1391,"navigation":1392,"path":241,"seo":1393,"stem":1394,"__hash__":1395},"content\u002F3.developer\u002F11.feature-flags.md",{"type":445,"value":446,"toc":1380},"minimark",[447,451,460,465,476,624,627,720,724,801,895,913,917,925,1036,1040,1055,1180,1184,1273,1280,1284,1311,1315,1355,1359,1376],[448,449,242],"h1",{"id":450},"feature-flags-developer-reference",[452,453,454,455,459],"p",{},"For the operator-facing overview see ",[456,457,458],"a",{"href":69},"Feature flags in the product guide",". This page is for developers adding, removing, or extending flags.",[461,462,464],"h2",{"id":463},"single-source-of-truth","Single source of truth",[452,466,467,468,475],{},"Every toggleable surface is declared in ",[469,470,471],"strong",{},[472,473,474],"code",{},"packages\u002Fshared\u002Fsrc\u002FfeatureFlags.ts",". The setup CLI, the admin UI, the Convex backend, and Nuxt middleware all import from that one file — there is no second registry to keep in sync.",[477,478,483],"pre",{"className":479,"code":480,"language":481,"meta":482,"style":482},"language-ts shiki shiki-themes github-light github-dark-dimmed","export const FEATURE_FLAGS: Record\u003CFeatureFlagKey, FeatureFlagDefinition> = {\n  postbox: {\n    key: 'postbox',\n    category: 'receiving',\n    label: 'Personal mail (Postbox)',\n    description: 'Per-user mailboxes with webmail UI, IMAP\u002FSMTP for native clients, and MX-based delivery (Gmail-equivalent).',\n    default: false,\n    dockerProfiles: ['personal-mail'],\n  },\n  \u002F\u002F … 30 more flags (31 total in FEATURE_FLAGS)\n};\n","ts","",[472,484,485,530,536,549,560,571,582,593,605,611,618],{"__ignoreMap":482},[486,487,490,494,497,501,504,508,512,515,518,521,524,527],"span",{"class":488,"line":489},"line",1,[486,491,493],{"class":492},"s7YZ4","export",[486,495,496],{"class":492}," const",[486,498,500],{"class":499},"sviXB"," FEATURE_FLAGS",[486,502,503],{"class":492},":",[486,505,507],{"class":506},"sOLd2"," Record",[486,509,511],{"class":510},"sYgZi","\u003C",[486,513,514],{"class":506},"FeatureFlagKey",[486,516,517],{"class":510},", ",[486,519,520],{"class":506},"FeatureFlagDefinition",[486,522,523],{"class":510},"> ",[486,525,526],{"class":492},"=",[486,528,529],{"class":510}," {\n",[486,531,533],{"class":488,"line":532},2,[486,534,535],{"class":510},"  postbox: {\n",[486,537,539,542,546],{"class":488,"line":538},3,[486,540,541],{"class":510},"    key: ",[486,543,545],{"class":544},"s-HuK","'postbox'",[486,547,548],{"class":510},",\n",[486,550,552,555,558],{"class":488,"line":551},4,[486,553,554],{"class":510},"    category: ",[486,556,557],{"class":544},"'receiving'",[486,559,548],{"class":510},[486,561,563,566,569],{"class":488,"line":562},5,[486,564,565],{"class":510},"    label: ",[486,567,568],{"class":544},"'Personal mail (Postbox)'",[486,570,548],{"class":510},[486,572,574,577,580],{"class":488,"line":573},6,[486,575,576],{"class":510},"    description: ",[486,578,579],{"class":544},"'Per-user mailboxes with webmail UI, IMAP\u002FSMTP for native clients, and MX-based delivery (Gmail-equivalent).'",[486,581,548],{"class":510},[486,583,585,588,591],{"class":488,"line":584},7,[486,586,587],{"class":510},"    default: ",[486,589,590],{"class":499},"false",[486,592,548],{"class":510},[486,594,596,599,602],{"class":488,"line":595},8,[486,597,598],{"class":510},"    dockerProfiles: [",[486,600,601],{"class":544},"'personal-mail'",[486,603,604],{"class":510},"],\n",[486,606,608],{"class":488,"line":607},9,[486,609,610],{"class":510},"  },\n",[486,612,614],{"class":488,"line":613},10,[486,615,617],{"class":616},"sDN9O","  \u002F\u002F … 30 more flags (31 total in FEATURE_FLAGS)\n",[486,619,621],{"class":488,"line":620},11,[486,622,623],{"class":510},"};\n",[452,625,626],{},"Each definition can declare:",[628,629,630,643],"table",{},[631,632,633],"thead",{},[634,635,636,640],"tr",{},[637,638,639],"th",{},"Field",[637,641,642],{},"Purpose",[644,645,646,657,667,677,690,700,710],"tbody",{},[634,647,648,654],{},[649,650,651],"td",{},[472,652,653],{},"category",[649,655,656],{},"Groups the flag in the admin UI (sending \u002F receiving \u002F ai \u002F integrations \u002F security \u002F deliverability \u002F hosted)",[634,658,659,664],{},[649,660,661],{},[472,662,663],{},"default",[649,665,666],{},"Initial value for fresh installs",[634,668,669,674],{},[649,670,671],{},[472,672,673],{},"requires",[649,675,676],{},"Other flag keys that must be on for this flag to be on",[634,678,679,684],{},[649,680,681],{},[472,682,683],{},"cascadesOff",[649,685,686,687,689],{},"Flags forced off when this one is turned off (also auto-derived from ",[472,688,673],{},")",[634,691,692,697],{},[649,693,694],{},[472,695,696],{},"requiredEnvVars",[649,698,699],{},"Env vars the wizard\u002FUI must collect before activation",[634,701,702,707],{},[649,703,704],{},[472,705,706],{},"dockerProfiles",[649,708,709],{},"Compose profiles activated when this flag is on",[634,711,712,717],{},[649,713,714],{},[472,715,716],{},"hostedOnly",[649,718,719],{},"Hidden from the self-host wizard (control-plane flags)",[461,721,723],{"id":722},"runtime-helpers","Runtime helpers",[477,725,727],{"className":479,"code":726,"language":481,"meta":482,"style":482},"import {\n  FEATURE_FLAGS,\n  resolveFlags,\n  isFlagEnabled,\n  applyToggle,\n  getActiveProfiles,\n  getRequiredEnvVars,\n  getFlagsByCategory,\n  FEATURE_PACKS,\n  applyPackToggle,\n  isPackEnabled,\n} from '@owlat\u002Fshared\u002FfeatureFlags';\n",[472,728,729,736,741,746,751,756,761,766,771,776,781,786],{"__ignoreMap":482},[486,730,731,734],{"class":488,"line":489},[486,732,733],{"class":492},"import",[486,735,529],{"class":510},[486,737,738],{"class":488,"line":532},[486,739,740],{"class":510},"  FEATURE_FLAGS,\n",[486,742,743],{"class":488,"line":538},[486,744,745],{"class":510},"  resolveFlags,\n",[486,747,748],{"class":488,"line":551},[486,749,750],{"class":510},"  isFlagEnabled,\n",[486,752,753],{"class":488,"line":562},[486,754,755],{"class":510},"  applyToggle,\n",[486,757,758],{"class":488,"line":573},[486,759,760],{"class":510},"  getActiveProfiles,\n",[486,762,763],{"class":488,"line":584},[486,764,765],{"class":510},"  getRequiredEnvVars,\n",[486,767,768],{"class":488,"line":595},[486,769,770],{"class":510},"  getFlagsByCategory,\n",[486,772,773],{"class":488,"line":607},[486,774,775],{"class":510},"  FEATURE_PACKS,\n",[486,777,778],{"class":488,"line":613},[486,779,780],{"class":510},"  applyPackToggle,\n",[486,782,783],{"class":488,"line":620},[486,784,785],{"class":510},"  isPackEnabled,\n",[486,787,789,792,795,798],{"class":488,"line":788},12,[486,790,791],{"class":510},"} ",[486,793,794],{"class":492},"from",[486,796,797],{"class":544}," '@owlat\u002Fshared\u002FfeatureFlags'",[486,799,800],{"class":510},";\n",[628,802,803,813],{},[631,804,805],{},[634,806,807,810],{},[637,808,809],{},"Helper",[637,811,812],{},"Use when",[644,814,815,828,841,855,865,875,885],{},[634,816,817,822],{},[649,818,819],{},[472,820,821],{},"resolveFlags(stored)",[649,823,824,825,827],{},"Reading current state — applies ",[472,826,673],{}," cascades and returns the effective state",[634,829,830,835],{},[649,831,832],{},[472,833,834],{},"isFlagEnabled(stored, key)",[649,836,837,838],{},"Convenience around ",[472,839,840],{},"resolveFlags(...)[key]",[634,842,843,848],{},[649,844,845],{},[472,846,847],{},"applyToggle(stored, key, value)",[649,849,850,851,854],{},"User clicked a switch — returns ",[472,852,853],{},"{ next, cascaded }"," so the UI can show what else changed",[634,856,857,862],{},[649,858,859],{},[472,860,861],{},"getActiveProfiles(stored)",[649,863,864],{},"Compute the Docker Compose profiles to activate",[634,866,867,872],{},[649,868,869],{},[472,870,871],{},"getRequiredEnvVars(stored)",[649,873,874],{},"What env vars to prompt for given the active flag set",[634,876,877,882],{},[649,878,879],{},[472,880,881],{},"getFlagsByCategory()",[649,883,884],{},"Render the admin UI grouped by category",[634,886,887,892],{},[649,888,889],{},[472,890,891],{},"applyPackToggle(stored, packKey, value)",[649,893,894],{},"Toggle every flag in a pack at once (cascades still apply)",[452,896,897,898,901,902,901,905,908,909,912],{},"Resolution is iterative to a fixed point with a 10-iteration safety cap — long dependency chains (",[472,899,900],{},"inbox.codeTasks"," → ",[472,903,904],{},"ai.agent",[472,906,907],{},"ai"," + ",[472,910,911],{},"inbox",") resolve in a single call.",[461,914,916],{"id":915},"storage-enforcement","Storage & enforcement",[477,918,923],{"className":919,"code":921,"language":922},[920],"language-text","                     ┌─────────────────────────────────────────────────────┐\n                     │  packages\u002Fshared\u002Fsrc\u002FfeatureFlags.ts (registry)     │\n                     │  resolveFlags, applyToggle, getActiveProfiles, …    │\n                     └────┬───────────────┬───────────────┬────────────────┘\n                          │               │               │\n              ┌───────────▼────────┐  ┌──▼─────────────────┐ ┌─▼─────────────────────┐\n              │ apps\u002Fsetup-cli     │  │ apps\u002Fapi           │ │ apps\u002Fweb              │\n              │  commands\u002Ffeature  │  │ organizations\u002F     │ │ middleware\u002F           │\n              │  commands\u002Fpack     │  │   featureFlags.ts  │ │   feature.global.ts   │\n              │  lib\u002Foverride      │  │ lib\u002FfeatureFlags.ts│ │ pages w\u002F requires-    │\n              │  lib\u002FflagState     │  │ + queries          │ │ Feature: '\u003Cflag>'     │\n              └───────┬────────────┘  └────────┬───────────┘ └───┬───────────────────┘\n                      │                        │                 │\n            writes docker-compose              │           route gate →\n            .override.yml + .owlat-            │           \u002Fdashboard?disabled=\u003Cflag>\n            flags.json                         │\n                                  source of truth:\n                                  Convex table\n                                  instanceSettings.featureFlags\n","text",[472,924,921],{"__ignoreMap":482},[926,927,928,939,949,971,1011],"ul",{},[929,930,931,934,935,938],"li",{},[469,932,933],{},"Convex"," is the source of truth at runtime. ",[472,936,937],{},"instanceSettings.featureFlags"," is a single document.",[929,940,941,944,945,948],{},[469,942,943],{},"Setup CLI"," mirrors state into ",[472,946,947],{},".owlat-flags.json"," next to the compose file so the operator can change flags before the stack is up. On boot, Convex reconciles the file with the DB.",[929,950,951,954,955,958,959,962,963,966,967,970],{},[469,952,953],{},"Docker Compose profiles"," are activated indirectly: ",[472,956,957],{},"owlat-setup"," regenerates ",[472,960,961],{},"docker-compose.override.yml"," from ",[472,964,965],{},"getActiveProfiles(...)",". The base compose file declares ",[472,968,969],{},"profiles: [\u003Cname>]"," on each gated service.",[929,972,973,976,977,980,981,984,985,988,989,992,993,996,997,999,1000,1003,1004,1003,1007,1010],{},[469,974,975],{},"Page guards",": pages declare ",[472,978,979],{},"requiresFeature"," (a single key or array — all must be on) and optionally ",[472,982,983],{},"requiresAnyFeature"," (an OR-group, at least one on) in ",[472,986,987],{},"definePageMeta",". The dedicated global middleware ",[472,990,991],{},"apps\u002Fweb\u002Fapp\u002Fmiddleware\u002Ffeature.global.ts"," reads both and redirects to ",[472,994,995],{},"\u002Fdashboard?disabled=\u003Cflag>"," when a required flag is off. ",[472,998,983],{}," exists for surfaces reachable via more than one feature — e.g. the Postbox UI under hosted ",[472,1001,1002],{},"postbox"," ",[469,1005,1006],{},"or",[472,1008,1009],{},"mail.external",".",[929,1012,1013,1016,1017,1020,1021,1024,1025,1027,1028,1031,1032,1035],{},[469,1014,1015],{},"Server-side guards",": Convex public functions in gated modules call ",[472,1018,1019],{},"await assertFeatureEnabled(ctx, '\u003Cflag>')"," (",[472,1022,1023],{},"apps\u002Fapi\u002Fconvex\u002Flib\u002FfeatureFlags.ts",") early, so a stale client can't bypass UI gates. The helper reads ",[472,1026,937],{},", resolves dependencies via the shared ",[472,1029,1030],{},"resolveFlags",", and throws a ",[472,1033,1034],{},"forbidden"," error when off.",[461,1037,1039],{"id":1038},"feature-packs","Feature packs",[452,1041,1042,1043,1046,1047,1050,1051,1054],{},"Packs are pure UI sugar — they don't introduce new state, they read and write the same ",[472,1044,1045],{},"FeatureFlagState"," via ",[472,1048,1049],{},"applyPackToggle",", which calls ",[472,1052,1053],{},"applyToggle"," per flag so cascades behave correctly.",[477,1056,1058],{"className":479,"code":1057,"language":481,"meta":482,"style":482},"export const FEATURE_PACKS: Record\u003CFeaturePackKey, FeaturePack> = {\n  marketing:   { flags: ['campaigns', 'automations', 'transactional'], \u002F* … *\u002F },\n  emailClient: { flags: ['inbox', 'chat', 'postbox'],                  \u002F* … *\u002F },\n  ai:          { flags: ['ai', 'ai.agent', 'ai.autonomy', 'ai.knowledge', 'ai.assistant', 'ai.visualizations'] },\n};\n",[472,1059,1060,1089,1116,1140,1176],{"__ignoreMap":482},[486,1061,1062,1064,1066,1069,1071,1073,1075,1078,1080,1083,1085,1087],{"class":488,"line":489},[486,1063,493],{"class":492},[486,1065,496],{"class":492},[486,1067,1068],{"class":499}," FEATURE_PACKS",[486,1070,503],{"class":492},[486,1072,507],{"class":506},[486,1074,511],{"class":510},[486,1076,1077],{"class":506},"FeaturePackKey",[486,1079,517],{"class":510},[486,1081,1082],{"class":506},"FeaturePack",[486,1084,523],{"class":510},[486,1086,526],{"class":492},[486,1088,529],{"class":510},[486,1090,1091,1094,1097,1099,1102,1104,1107,1110,1113],{"class":488,"line":532},[486,1092,1093],{"class":510},"  marketing:   { flags: [",[486,1095,1096],{"class":544},"'campaigns'",[486,1098,517],{"class":510},[486,1100,1101],{"class":544},"'automations'",[486,1103,517],{"class":510},[486,1105,1106],{"class":544},"'transactional'",[486,1108,1109],{"class":510},"], ",[486,1111,1112],{"class":616},"\u002F* … *\u002F",[486,1114,1115],{"class":510}," },\n",[486,1117,1118,1121,1124,1126,1129,1131,1133,1136,1138],{"class":488,"line":538},[486,1119,1120],{"class":510},"  emailClient: { flags: [",[486,1122,1123],{"class":544},"'inbox'",[486,1125,517],{"class":510},[486,1127,1128],{"class":544},"'chat'",[486,1130,517],{"class":510},[486,1132,545],{"class":544},[486,1134,1135],{"class":510},"],                  ",[486,1137,1112],{"class":616},[486,1139,1115],{"class":510},[486,1141,1142,1145,1148,1150,1153,1155,1158,1160,1163,1165,1168,1170,1173],{"class":488,"line":551},[486,1143,1144],{"class":510},"  ai:          { flags: [",[486,1146,1147],{"class":544},"'ai'",[486,1149,517],{"class":510},[486,1151,1152],{"class":544},"'ai.agent'",[486,1154,517],{"class":510},[486,1156,1157],{"class":544},"'ai.autonomy'",[486,1159,517],{"class":510},[486,1161,1162],{"class":544},"'ai.knowledge'",[486,1164,517],{"class":510},[486,1166,1167],{"class":544},"'ai.assistant'",[486,1169,517],{"class":510},[486,1171,1172],{"class":544},"'ai.visualizations'",[486,1174,1175],{"class":510},"] },\n",[486,1177,1178],{"class":488,"line":562},[486,1179,623],{"class":510},[461,1181,1183],{"id":1182},"adding-a-new-flag","Adding a new flag",[1185,1186,1187,1203,1212,1228,1236,1249,1264],"ol",{},[929,1188,1189,1192,1193,1195,1196,1198,1199,1202],{},[469,1190,1191],{},"Declare it"," in ",[472,1194,474],{}," — add to the ",[472,1197,514],{}," union and the ",[472,1200,1201],{},"FEATURE_FLAGS"," map.",[929,1204,1205,1208,1209,1211],{},[469,1206,1207],{},"Decide dependencies",". Use ",[472,1210,673],{}," for hard prerequisites; the resolver and the cascade-off helper handle the rest automatically.",[929,1213,1214,1217,1218,1220,1221,1224,1225,1010],{},[469,1215,1216],{},"(Optional) Declare a Docker profile"," if the flag needs an extra service. Add ",[472,1219,969],{}," to the corresponding service in ",[472,1222,1223],{},"infra\u002Ftemplates\u002Fdocker-compose.vps.yml"," and the root ",[472,1226,1227],{},"docker-compose.yml",[929,1229,1230,1192,1233,1235],{},[469,1231,1232],{},"(Optional) Declare env vars",[472,1234,696],{},". The wizard will prompt for them; the admin UI will block toggling on until they're set.",[929,1237,1238,1241,1242,1245,1246,1248],{},[469,1239,1240],{},"Gate the UI",". Add ",[472,1243,1244],{},"requiresFeature: '\u003Ckey>'"," to the ",[472,1247,987],{}," of every page that belongs to the feature, and conditionally render sidebar entries.",[929,1250,1251,1254,1255,962,1258,1260,1261,1263],{},[469,1252,1253],{},"Gate the server",". At the top of every Convex public function behind the flag (after the auth check), call ",[472,1256,1257],{},"await assertFeatureEnabled(ctx, '\u003Ckey>')",[472,1259,1023],{}," — it reads the stored flags, resolves dependencies, and throws a ",[472,1262,1034],{}," error on its own.",[929,1265,1266,1269,1270,1272],{},[469,1267,1268],{},"Update docs",": add the flag to the table on ",[456,1271,458],{"href":69}," and (if user-visible) reference it from the README feature table.",[452,1274,1275,1276,1279],{},"If you introduce a new category, also add the category to the ",[472,1277,1278],{},"FeatureCategory"," union and a label in the admin UI's category-sorter.",[461,1281,1283],{"id":1282},"removing-a-flag","Removing a flag",[1185,1285,1286,1293,1296,1308],{},[929,1287,1288,1289,1292],{},"Set its ",[472,1290,1291],{},"default: false"," for one release to give operators time to migrate.",[929,1294,1295],{},"Drop the page-meta gates and conditional renders, leaving the underlying code path either always-on (if you're keeping it) or fully deleted (if you're retiring the feature).",[929,1297,1298,1299,1301,1302,1304,1305,1307],{},"Remove the entry from ",[472,1300,1201],{}," and the ",[472,1303,514],{}," union. The resolver gracefully ignores unknown stored keys, so removal is safe — old ",[472,1306,947],{}," files won't break.",[929,1309,1310],{},"Drop the docker profile from the compose files.",[461,1312,1314],{"id":1313},"hosted-only-flags","Hosted-only flags",[452,1316,1317,517,1320,1323,1324,1327,1328,1331,1332,1335,1336,1338,1339,1342,1343,1346,1347,1350,1351,1354],{},[472,1318,1319],{},"billing.stripe",[472,1321,1322],{},"multiTenancy",", and ",[472,1325,1326],{},"tier.autoProvision"," are marked ",[472,1329,1330],{},"hostedOnly: true",". ",[472,1333,1334],{},"getDefaultFlags()"," and ",[472,1337,881],{}," skip them directly unless ",[472,1340,1341],{},"{ hosted: true }"," is passed, so the self-host setup CLI and admin UI never see them. ",[472,1344,1345],{},"resolveFlags()"," doesn't filter them itself — it just merges over the (filtered) default baseline, so a hostedOnly key explicitly present in ",[472,1348,1349],{},"stored"," would still resolve through. They're set externally by the hosted control plane (in a separate private repo) and read by ",[472,1352,1353],{},"apps\u002Fapi"," for billing UI visibility.",[461,1356,1358],{"id":1357},"testing","Testing",[452,1360,1361,1362,1365,1366,1020,1369,517,1372,1375],{},"Unit tests for the resolver live in ",[472,1363,1364],{},"packages\u002Fshared\u002Fsrc\u002F__tests__\u002FfeatureFlags.test.ts",". When adding a flag with non-trivial dependencies, add a case there exercising the cascade. The setup CLI's own tests live in ",[472,1367,1368],{},"apps\u002Fsetup-cli\u002Fsrc\u002Flib\u002F__tests__\u002F",[472,1370,1371],{},"passwordHash.test.ts",[472,1373,1374],{},"convexDeploy.test.ts",").",[1377,1378,1379],"style",{},"html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .sOLd2, html code.shiki .sOLd2{--shiki-default:#6F42C1;--shiki-dark:#F69D50}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .s-HuK, html code.shiki .s-HuK{--shiki-default:#032F62;--shiki-dark:#96D0FF}html pre.shiki code .sDN9O, html code.shiki .sDN9O{--shiki-default:#6A737D;--shiki-dark:#768390}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);}",{"title":482,"searchDepth":532,"depth":532,"links":1381},[1382,1383,1384,1385,1386,1387,1388,1389],{"id":463,"depth":532,"text":464},{"id":722,"depth":532,"text":723},{"id":915,"depth":532,"text":916},{"id":1038,"depth":532,"text":1039},{"id":1182,"depth":532,"text":1183},{"id":1282,"depth":532,"text":1283},{"id":1313,"depth":532,"text":1314},{"id":1357,"depth":532,"text":1358},"md",{},true,{"title":242,"description":243},"3.developer\u002F11.feature-flags","b27OnBn0TF5QOOni-wAW3QveRAVVNLBx5BAslBRk-bg",[1397,1399],{"title":238,"path":237,"stem":1398,"children":-1},"3.developer\u002F10.mta-system",{"title":246,"path":245,"stem":1400,"children":-1},"3.developer\u002F12.how-email-works",1782846427565]