[{"data":1,"prerenderedAt":1557},["ShallowReactive",2],{"content-vision\u002Fself-hosting":3,"surround-\u002Fvision\u002Fself-hosting":1549},{"id":4,"title":5,"body":6,"description":1542,"extension":1543,"meta":1544,"navigation":349,"path":1545,"seo":1546,"stem":1547,"__hash__":1548},"content\u002F5.vision\u002F1.self-hosting.md","Self-Hosting Architecture",{"type":7,"value":8,"toc":1524},"minimark",[9,13,17,25,30,33,36,157,162,171,215,222,226,704,708,712,793,797,805,891,895,977,981,989,1065,1072,1076,1079,1083,1086,1121,1166,1170,1173,1206,1274,1278,1281,1367,1373,1377,1520],[10,11,5],"h1",{"id":12},"self-hosting-architecture",[14,15,16],"p",{},"Owlat is designed to run entirely on your own infrastructure. Every component is open-source, and the entire stack deploys via a single Docker Compose file.",[18,19,22],"callout",{"title":20,"type":21},"Why self-hosting matters","info",[14,23,24],{},"Data sovereignty, compliance requirements, air-gapped environments, or simply wanting full control. Owlat does not require any cloud services to function — not even for AI features, if you run a local model.",[26,27,29],"h2",{"id":28},"the-stack","The Stack",[31,32],"arch-self-hosting",{},[14,34,35],{},"The self-hosted stack consists of five required services and one optional service:",[37,38,39,55],"table",{},[40,41,42],"thead",{},[43,44,45,49,52],"tr",{},[46,47,48],"th",{},"Service",[46,50,51],{},"Role",[46,53,54],{},"Image",[56,57,58,76,92,107,122,137],"tbody",{},[43,59,60,67,70],{},[61,62,63],"td",{},[64,65,66],"strong",{},"Convex Backend",[61,68,69],{},"Database, real-time subscriptions, file storage, vector search, serverless functions",[61,71,72],{},[73,74,75],"code",{},"ghcr.io\u002Fget-convex\u002Fconvex-backend",[43,77,78,83,86],{},[61,79,80],{},[64,81,82],{},"Web",[61,84,85],{},"Nuxt application (dashboard, email builder, settings)",[61,87,88,89],{},"Built from ",[73,90,91],{},"apps\u002Fweb\u002FDockerfile",[43,93,94,99,102],{},[61,95,96],{},[64,97,98],{},"MTA",[61,100,101],{},"Custom mail transfer agent — SMTP delivery, bounce processing, IP warming",[61,103,88,104],{},[73,105,106],{},"apps\u002Fmta\u002FDockerfile",[43,108,109,114,117],{},[61,110,111],{},[64,112,113],{},"Redis",[61,115,116],{},"Job queue for MTA, distributed coordination, rate limiting state",[61,118,119],{},[73,120,121],{},"redis:7-alpine",[43,123,124,129,132],{},[61,125,126],{},[64,127,128],{},"ClamAV",[61,130,131],{},"Antivirus scanning for email attachments",[61,133,134],{},[73,135,136],{},"clamav\u002Fclamav:1.3",[43,138,139,149,152],{},[61,140,141,144,145],{},[64,142,143],{},"Ollama"," ",[146,147,148],"em",{},"(optional)",[61,150,151],{},"Self-hosted LLM for the Agent Pipeline, knowledge extraction, and semantic search",[61,153,154],{},[73,155,156],{},"ollama\u002Follama",[158,159,161],"h3",{"id":160},"what-convex-provides-natively","What Convex provides natively",[14,163,164,165,170],{},"The open-source Convex backend (",[166,167,169],"a",{"href":168},"\u002Fdeveloper\u002Fdecisions\u002F006-self-hosted-convex","ADR-006",") bundles capabilities that would otherwise require separate services:",[172,173,174,181,187,197,203,209],"ul",{},[175,176,177,180],"li",{},[64,178,179],{},"Database"," — document-oriented with indexes, full-text search, and ACID transactions",[175,182,183,186],{},[64,184,185],{},"Vector search"," — native vector indexes for embedding-based retrieval (Knowledge Graph, semantic file search)",[175,188,189,192,193,196],{},[64,190,191],{},"File storage"," — binary file uploads and downloads via ",[73,194,195],{},"ctx.storage"," (media library, attachments, semantic files)",[175,198,199,202],{},[64,200,201],{},"Real-time subscriptions"," — reactive queries that push updates to the UI over WebSocket",[175,204,205,208],{},[64,206,207],{},"Scheduled functions"," — cron jobs and one-off scheduled tasks (campaign sending, usage metering, knowledge decay)",[175,210,211,214],{},[64,212,213],{},"HTTP actions"," — webhook endpoints, API routes, tracking pixels",[14,216,217,218,221],{},"This means ",[64,219,220],{},"no PostgreSQL, no MinIO, no separate vector database",". The Convex backend is the single stateful service.",[26,223,225],{"id":224},"docker-compose","Docker Compose",[227,228,233],"pre",{"className":229,"code":230,"language":231,"meta":232,"style":232},"language-yaml shiki shiki-themes github-light github-dark-dimmed","services:\n  convex:\n    image: ghcr.io\u002Fget-convex\u002Fconvex-backend\n    ports:\n      - \"3210:3210\"   # Backend API\n      - \"3211:3211\"   # HTTP actions\n      - \"6791:6791\"   # Dashboard\n    volumes:\n      - convex-data:\u002Fdata\n    environment:\n      - INSTANCE_SECRET=${INSTANCE_SECRET}\n\n  web:\n    build: .\u002Fapps\u002Fweb\n    ports:\n      - \"3000:3000\"\n    environment:\n      - CONVEX_SELF_HOSTED_URL=http:\u002F\u002Fconvex:3210\n      - CONVEX_SELF_HOSTED_ADMIN_KEY=${CONVEX_ADMIN_KEY}\n\n  mta:\n    build: .\u002Fapps\u002Fmta\n    ports:\n      - \"25:25\"       # Inbound SMTP (bounce processing)\n      - \"3100:3100\"   # HTTP API\n    environment:\n      - REDIS_URL=redis:\u002F\u002Fredis:6379\n      - CONVEX_SITE_URL=http:\u002F\u002Fconvex:3211\n      - MTA_API_KEY=${MTA_API_KEY}\n      - MTA_WEBHOOK_SECRET=${MTA_WEBHOOK_SECRET}\n    depends_on:\n      - redis\n      - convex\n\n  redis:\n    image: redis:7-alpine\n    volumes:\n      - redis-data:\u002Fdata\n\n  clamav:\n    image: clamav\u002Fclamav:1.3\n    volumes:\n      - clamav-data:\u002Fvar\u002Flib\u002Fclamav\n\n  # Optional: self-hosted LLM\n  ollama:\n    image: ollama\u002Follama\n    volumes:\n      - ollama-data:\u002Froot\u002F.ollama\n    profiles:\n      - ai\n\nvolumes:\n  convex-data:\n  redis-data:\n  clamav-data:\n  ollama-data:\n","yaml","",[73,234,235,248,256,269,277,290,301,312,320,328,336,344,351,359,370,377,385,392,400,408,413,421,431,438,449,460,467,475,483,491,499,507,515,523,528,536,546,553,561,566,574,584,591,599,604,610,618,628,635,643,651,659,664,672,680,688,696],{"__ignoreMap":232},[236,237,240,244],"span",{"class":238,"line":239},"line",1,[236,241,243],{"class":242},"sIAta","services",[236,245,247],{"class":246},"sYgZi",":\n",[236,249,251,254],{"class":238,"line":250},2,[236,252,253],{"class":242},"  convex",[236,255,247],{"class":246},[236,257,259,262,265],{"class":238,"line":258},3,[236,260,261],{"class":242},"    image",[236,263,264],{"class":246},": ",[236,266,268],{"class":267},"s-HuK","ghcr.io\u002Fget-convex\u002Fconvex-backend\n",[236,270,272,275],{"class":238,"line":271},4,[236,273,274],{"class":242},"    ports",[236,276,247],{"class":246},[236,278,280,283,286],{"class":238,"line":279},5,[236,281,282],{"class":246},"      - ",[236,284,285],{"class":267},"\"3210:3210\"",[236,287,289],{"class":288},"sDN9O","   # Backend API\n",[236,291,293,295,298],{"class":238,"line":292},6,[236,294,282],{"class":246},[236,296,297],{"class":267},"\"3211:3211\"",[236,299,300],{"class":288},"   # HTTP actions\n",[236,302,304,306,309],{"class":238,"line":303},7,[236,305,282],{"class":246},[236,307,308],{"class":267},"\"6791:6791\"",[236,310,311],{"class":288},"   # Dashboard\n",[236,313,315,318],{"class":238,"line":314},8,[236,316,317],{"class":242},"    volumes",[236,319,247],{"class":246},[236,321,323,325],{"class":238,"line":322},9,[236,324,282],{"class":246},[236,326,327],{"class":267},"convex-data:\u002Fdata\n",[236,329,331,334],{"class":238,"line":330},10,[236,332,333],{"class":242},"    environment",[236,335,247],{"class":246},[236,337,339,341],{"class":238,"line":338},11,[236,340,282],{"class":246},[236,342,343],{"class":267},"INSTANCE_SECRET=${INSTANCE_SECRET}\n",[236,345,347],{"class":238,"line":346},12,[236,348,350],{"emptyLinePlaceholder":349},true,"\n",[236,352,354,357],{"class":238,"line":353},13,[236,355,356],{"class":242},"  web",[236,358,247],{"class":246},[236,360,362,365,367],{"class":238,"line":361},14,[236,363,364],{"class":242},"    build",[236,366,264],{"class":246},[236,368,369],{"class":267},".\u002Fapps\u002Fweb\n",[236,371,373,375],{"class":238,"line":372},15,[236,374,274],{"class":242},[236,376,247],{"class":246},[236,378,380,382],{"class":238,"line":379},16,[236,381,282],{"class":246},[236,383,384],{"class":267},"\"3000:3000\"\n",[236,386,388,390],{"class":238,"line":387},17,[236,389,333],{"class":242},[236,391,247],{"class":246},[236,393,395,397],{"class":238,"line":394},18,[236,396,282],{"class":246},[236,398,399],{"class":267},"CONVEX_SELF_HOSTED_URL=http:\u002F\u002Fconvex:3210\n",[236,401,403,405],{"class":238,"line":402},19,[236,404,282],{"class":246},[236,406,407],{"class":267},"CONVEX_SELF_HOSTED_ADMIN_KEY=${CONVEX_ADMIN_KEY}\n",[236,409,411],{"class":238,"line":410},20,[236,412,350],{"emptyLinePlaceholder":349},[236,414,416,419],{"class":238,"line":415},21,[236,417,418],{"class":242},"  mta",[236,420,247],{"class":246},[236,422,424,426,428],{"class":238,"line":423},22,[236,425,364],{"class":242},[236,427,264],{"class":246},[236,429,430],{"class":267},".\u002Fapps\u002Fmta\n",[236,432,434,436],{"class":238,"line":433},23,[236,435,274],{"class":242},[236,437,247],{"class":246},[236,439,441,443,446],{"class":238,"line":440},24,[236,442,282],{"class":246},[236,444,445],{"class":267},"\"25:25\"",[236,447,448],{"class":288},"       # Inbound SMTP (bounce processing)\n",[236,450,452,454,457],{"class":238,"line":451},25,[236,453,282],{"class":246},[236,455,456],{"class":267},"\"3100:3100\"",[236,458,459],{"class":288},"   # HTTP API\n",[236,461,463,465],{"class":238,"line":462},26,[236,464,333],{"class":242},[236,466,247],{"class":246},[236,468,470,472],{"class":238,"line":469},27,[236,471,282],{"class":246},[236,473,474],{"class":267},"REDIS_URL=redis:\u002F\u002Fredis:6379\n",[236,476,478,480],{"class":238,"line":477},28,[236,479,282],{"class":246},[236,481,482],{"class":267},"CONVEX_SITE_URL=http:\u002F\u002Fconvex:3211\n",[236,484,486,488],{"class":238,"line":485},29,[236,487,282],{"class":246},[236,489,490],{"class":267},"MTA_API_KEY=${MTA_API_KEY}\n",[236,492,494,496],{"class":238,"line":493},30,[236,495,282],{"class":246},[236,497,498],{"class":267},"MTA_WEBHOOK_SECRET=${MTA_WEBHOOK_SECRET}\n",[236,500,502,505],{"class":238,"line":501},31,[236,503,504],{"class":242},"    depends_on",[236,506,247],{"class":246},[236,508,510,512],{"class":238,"line":509},32,[236,511,282],{"class":246},[236,513,514],{"class":267},"redis\n",[236,516,518,520],{"class":238,"line":517},33,[236,519,282],{"class":246},[236,521,522],{"class":267},"convex\n",[236,524,526],{"class":238,"line":525},34,[236,527,350],{"emptyLinePlaceholder":349},[236,529,531,534],{"class":238,"line":530},35,[236,532,533],{"class":242},"  redis",[236,535,247],{"class":246},[236,537,539,541,543],{"class":238,"line":538},36,[236,540,261],{"class":242},[236,542,264],{"class":246},[236,544,545],{"class":267},"redis:7-alpine\n",[236,547,549,551],{"class":238,"line":548},37,[236,550,317],{"class":242},[236,552,247],{"class":246},[236,554,556,558],{"class":238,"line":555},38,[236,557,282],{"class":246},[236,559,560],{"class":267},"redis-data:\u002Fdata\n",[236,562,564],{"class":238,"line":563},39,[236,565,350],{"emptyLinePlaceholder":349},[236,567,569,572],{"class":238,"line":568},40,[236,570,571],{"class":242},"  clamav",[236,573,247],{"class":246},[236,575,577,579,581],{"class":238,"line":576},41,[236,578,261],{"class":242},[236,580,264],{"class":246},[236,582,583],{"class":267},"clamav\u002Fclamav:1.3\n",[236,585,587,589],{"class":238,"line":586},42,[236,588,317],{"class":242},[236,590,247],{"class":246},[236,592,594,596],{"class":238,"line":593},43,[236,595,282],{"class":246},[236,597,598],{"class":267},"clamav-data:\u002Fvar\u002Flib\u002Fclamav\n",[236,600,602],{"class":238,"line":601},44,[236,603,350],{"emptyLinePlaceholder":349},[236,605,607],{"class":238,"line":606},45,[236,608,609],{"class":288},"  # Optional: self-hosted LLM\n",[236,611,613,616],{"class":238,"line":612},46,[236,614,615],{"class":242},"  ollama",[236,617,247],{"class":246},[236,619,621,623,625],{"class":238,"line":620},47,[236,622,261],{"class":242},[236,624,264],{"class":246},[236,626,627],{"class":267},"ollama\u002Follama\n",[236,629,631,633],{"class":238,"line":630},48,[236,632,317],{"class":242},[236,634,247],{"class":246},[236,636,638,640],{"class":238,"line":637},49,[236,639,282],{"class":246},[236,641,642],{"class":267},"ollama-data:\u002Froot\u002F.ollama\n",[236,644,646,649],{"class":238,"line":645},50,[236,647,648],{"class":242},"    profiles",[236,650,247],{"class":246},[236,652,654,656],{"class":238,"line":653},51,[236,655,282],{"class":246},[236,657,658],{"class":267},"ai\n",[236,660,662],{"class":238,"line":661},52,[236,663,350],{"emptyLinePlaceholder":349},[236,665,667,670],{"class":238,"line":666},53,[236,668,669],{"class":242},"volumes",[236,671,247],{"class":246},[236,673,675,678],{"class":238,"line":674},54,[236,676,677],{"class":242},"  convex-data",[236,679,247],{"class":246},[236,681,683,686],{"class":238,"line":682},55,[236,684,685],{"class":242},"  redis-data",[236,687,247],{"class":246},[236,689,691,694],{"class":238,"line":690},56,[236,692,693],{"class":242},"  clamav-data",[236,695,247],{"class":246},[236,697,699,702],{"class":238,"line":698},57,[236,700,701],{"class":242},"  ollama-data",[236,703,247],{"class":246},[26,705,707],{"id":706},"environment-variables","Environment Variables",[158,709,711],{"id":710},"required","Required",[37,713,714,724],{},[40,715,716],{},[43,717,718,721],{},[46,719,720],{},"Variable",[46,722,723],{},"Description",[56,725,726,736,750,760,770,780],{},[43,727,728,733],{},[61,729,730],{},[73,731,732],{},"INSTANCE_SECRET",[61,734,735],{},"Convex backend instance secret (generate on first run)",[43,737,738,743],{},[61,739,740],{},[73,741,742],{},"CONVEX_ADMIN_KEY",[61,744,745,746,749],{},"Admin key for Convex (generated via ",[73,747,748],{},"generate_admin_key.sh",")",[43,751,752,757],{},[61,753,754],{},[73,755,756],{},"MTA_API_KEY",[61,758,759],{},"Shared secret between Convex and MTA for send requests",[43,761,762,767],{},[61,763,764],{},[73,765,766],{},"MTA_WEBHOOK_SECRET",[61,768,769],{},"HMAC secret for MTA → Convex webhook authentication",[43,771,772,777],{},[61,773,774],{},[73,775,776],{},"BETTER_AUTH_SECRET",[61,778,779],{},"Authentication secret for BetterAuth sessions",[43,781,782,787],{},[61,783,784],{},[73,785,786],{},"SITE_URL",[61,788,789,790,749],{},"Public URL of the web application (e.g., ",[73,791,792],{},"https:\u002F\u002Fowlat.example.com",[158,794,796],{"id":795},"llm-configuration-for-agent-pipeline","LLM Configuration (for Agent Pipeline)",[14,798,799,800,804],{},"See ",[166,801,803],{"href":802},"\u002Fdeveloper\u002Fdecisions\u002F007-pluggable-llm","ADR-007: Pluggable LLM Provider"," for details.",[37,806,807,818],{},[40,808,809],{},[43,810,811,813,815],{},[46,812,720],{},[46,814,723],{},[46,816,817],{},"Default",[56,819,820,847,863,876],{},[43,821,822,827,843],{},[61,823,824],{},[73,825,826],{},"LLM_PROVIDER",[61,828,829,830,833,834,833,837,833,840],{},"Provider type: ",[73,831,832],{},"openai",", ",[73,835,836],{},"anthropic",[73,838,839],{},"ollama",[73,841,842],{},"custom",[61,844,845],{},[73,846,832],{},[43,848,849,854,860],{},[61,850,851],{},[73,852,853],{},"LLM_BASE_URL",[61,855,856,857,749],{},"API endpoint (for Ollama: ",[73,858,859],{},"http:\u002F\u002Follama:11434\u002Fv1",[61,861,862],{},"Provider default",[43,864,865,870,873],{},[61,866,867],{},[73,868,869],{},"LLM_API_KEY",[61,871,872],{},"API key (not needed for Ollama)",[61,874,875],{},"—",[43,877,878,883,886],{},[61,879,880],{},[73,881,882],{},"LLM_MODEL",[61,884,885],{},"Model identifier",[61,887,888],{},[73,889,890],{},"gpt-4o",[158,892,894],{"id":893},"optional","Optional",[37,896,897,907],{},[40,898,899],{},[43,900,901,903,905],{},[46,902,720],{},[46,904,723],{},[46,906,817],{},[56,908,909,924,947,962],{},[43,910,911,916,919],{},[61,912,913],{},[73,914,915],{},"BILLING_ENABLED",[61,917,918],{},"Enable Stripe billing integration",[61,920,921],{},[73,922,923],{},"false",[43,925,926,931,943],{},[61,927,928],{},[73,929,930],{},"EMAIL_PROVIDER",[61,932,933,934,833,937,833,940],{},"Email provider: ",[73,935,936],{},"mta",[73,938,939],{},"ses",[73,941,942],{},"resend",[61,944,945],{},[73,946,936],{},[43,948,949,954,957],{},[61,950,951],{},[73,952,953],{},"CLAMAV_HOST",[61,955,956],{},"ClamAV hostname",[61,958,959],{},[73,960,961],{},"clamav",[43,963,964,969,972],{},[61,965,966],{},[73,967,968],{},"CLAMAV_PORT",[61,970,971],{},"ClamAV port",[61,973,974],{},[73,975,976],{},"3310",[26,978,980],{"id":979},"how-the-stack-evolves","How the stack evolves",[14,982,983,984,988],{},"The Docker Compose file grows with each phase of the ",[166,985,987],{"href":986},"\u002Fvision","roadmap",":",[37,990,991,1004],{},[40,992,993],{},[43,994,995,998,1001],{},[46,996,997],{},"Phase",[46,999,1000],{},"Services Added",[46,1002,1003],{},"Purpose",[56,1005,1006,1020,1036,1049],{},[43,1007,1008,1014,1017],{},[61,1009,1010,1013],{},[64,1011,1012],{},"Now"," (Email Platform)",[61,1015,1016],{},"convex, web, mta, redis, clamav",[61,1018,1019],{},"Full email marketing platform",[43,1021,1022,1028,1033],{},[61,1023,1024,1027],{},[64,1025,1026],{},"Next"," (Inbound & Agents)",[61,1029,1030,1031],{},"ollama ",[146,1032,148],{},[61,1034,1035],{},"Agent pipeline, inbound email processing, verification queue",[43,1037,1038,1044,1046],{},[61,1039,1040,1043],{},[64,1041,1042],{},"Then"," (Communication Intelligence)",[61,1045,875],{},[61,1047,1048],{},"Knowledge graph, multi-channel, CRM, file system — all run within Convex",[43,1050,1051,1057,1062],{},[61,1052,1053,1056],{},[64,1054,1055],{},"Later"," (Complete Vision)",[61,1058,1059,1060],{},"code-worker ",[146,1061,148],{},[61,1063,1064],{},"Coding agent sidecar for feature request → prototype pipeline",[14,1066,1067,1068,1071],{},"The architecture is designed so that ",[64,1069,1070],{},"adding AI capabilities does not add required infrastructure",". The agent pipeline, knowledge graph, and semantic search all run as Convex functions. Ollama is optional — cloud users can use OpenAI or Anthropic API keys instead.",[26,1073,1075],{"id":1074},"security-isolation","Security & isolation",[14,1077,1078],{},"Self-hosting means running AI agents on your own infrastructure — which requires the same defense-in-depth approach applied to the email pipeline. The security model covers credential isolation, process sandboxing, and environment hygiene.",[158,1080,1082],{"id":1081},"credential-isolation","Credential isolation",[14,1084,1085],{},"Agent pipeline functions never see raw API keys or secrets directly. All sensitive configuration flows through the Convex backend's environment variable system:",[172,1087,1088,1101,1111],{},[175,1089,1090,1093,1094,1096,1097,1100],{},[64,1091,1092],{},"LLM credentials"," — ",[73,1095,869],{}," is read only by ",[73,1098,1099],{},"getLLMProvider()"," inside Convex actions. The key never appears in agent context, LLM prompts, or log output.",[175,1102,1103,1106,1107,1110],{},[64,1104,1105],{},"Channel provider credentials"," — SMS\u002FWhatsApp API keys stored encrypted in ",[73,1108,1109],{},"channelConfigs",", decrypted only at the moment of the API call.",[175,1112,1113,1116,1117,1120],{},[64,1114,1115],{},"Ollama on internal network"," — self-hosted LLM runs on the Docker internal network (",[73,1118,1119],{},"ollama:11434","), with no API key required and no external exposure. The Ollama port is not published to the host by default.",[227,1122,1124],{"className":229,"code":1123,"language":231,"meta":232,"style":232},"# Ollama is only reachable from other Docker services\nollama:\n  image: ollama\u002Follama\n  # No 'ports:' mapping — only accessible via Docker network\n  networks:\n    - internal\n",[73,1125,1126,1131,1137,1146,1151,1158],{"__ignoreMap":232},[236,1127,1128],{"class":238,"line":239},[236,1129,1130],{"class":288},"# Ollama is only reachable from other Docker services\n",[236,1132,1133,1135],{"class":238,"line":250},[236,1134,839],{"class":242},[236,1136,247],{"class":246},[236,1138,1139,1142,1144],{"class":238,"line":258},[236,1140,1141],{"class":242},"  image",[236,1143,264],{"class":246},[236,1145,627],{"class":267},[236,1147,1148],{"class":238,"line":271},[236,1149,1150],{"class":288},"  # No 'ports:' mapping — only accessible via Docker network\n",[236,1152,1153,1156],{"class":238,"line":279},[236,1154,1155],{"class":242},"  networks",[236,1157,247],{"class":246},[236,1159,1160,1163],{"class":238,"line":292},[236,1161,1162],{"class":246},"    - ",[236,1164,1165],{"class":267},"internal\n",[158,1167,1169],{"id":1168},"process-sandboxing","Process sandboxing",[14,1171,1172],{},"Agent-generated content runs in isolated execution environments:",[172,1174,1175,1190,1200],{},[175,1176,1177,1180,1181,1184,1185,1189],{},[64,1178,1179],{},"Visualization agent output"," — rendered in ",[73,1182,1183],{},"\u003Ciframe sandbox=\"allow-scripts\">"," with no access to the parent DOM, Convex client, cookies, or navigation. See ",[166,1186,1188],{"href":1187},"\u002Fvision\u002Fdesktop-app#visualization-agent","Visualization Agent",".",[175,1191,1192,1195,1196,1199],{},[64,1193,1194],{},"Coding agent sidecar"," — the ",[73,1197,1198],{},"code-worker"," Docker container runs with restricted filesystem access (mounted workspace volume only), no access to the Convex backend's data volume, and no network access to internal services beyond the Convex API endpoint.",[175,1201,1202,1205],{},[64,1203,1204],{},"Network isolation"," — agent containers communicate only with the Convex backend API. They cannot reach Redis, ClamAV, or the MTA directly.",[227,1207,1209],{"className":229,"code":1208,"language":231,"meta":232,"style":232},"code-worker:\n  build: .\u002Fapps\u002Fcode-worker\n  volumes:\n    - workspace:\u002Fworkspace  # Isolated workspace, not convex-data\n  networks:\n    - agent-net              # Restricted network: only Convex API access\n  security_opt:\n    - no-new-privileges:true\n",[73,1210,1211,1217,1227,1234,1244,1250,1260,1267],{"__ignoreMap":232},[236,1212,1213,1215],{"class":238,"line":239},[236,1214,1198],{"class":242},[236,1216,247],{"class":246},[236,1218,1219,1222,1224],{"class":238,"line":250},[236,1220,1221],{"class":242},"  build",[236,1223,264],{"class":246},[236,1225,1226],{"class":267},".\u002Fapps\u002Fcode-worker\n",[236,1228,1229,1232],{"class":238,"line":258},[236,1230,1231],{"class":242},"  volumes",[236,1233,247],{"class":246},[236,1235,1236,1238,1241],{"class":238,"line":271},[236,1237,1162],{"class":246},[236,1239,1240],{"class":267},"workspace:\u002Fworkspace",[236,1242,1243],{"class":288},"  # Isolated workspace, not convex-data\n",[236,1245,1246,1248],{"class":238,"line":279},[236,1247,1155],{"class":242},[236,1249,247],{"class":246},[236,1251,1252,1254,1257],{"class":238,"line":292},[236,1253,1162],{"class":246},[236,1255,1256],{"class":267},"agent-net",[236,1258,1259],{"class":288},"              # Restricted network: only Convex API access\n",[236,1261,1262,1265],{"class":238,"line":303},[236,1263,1264],{"class":242},"  security_opt",[236,1266,247],{"class":246},[236,1268,1269,1271],{"class":238,"line":314},[236,1270,1162],{"class":246},[236,1272,1273],{"class":267},"no-new-privileges:true\n",[158,1275,1277],{"id":1276},"environment-variable-hygiene","Environment variable hygiene",[14,1279,1280],{},"Sensitive variables follow strict scoping rules:",[37,1282,1283,1296],{},[40,1284,1285],{},[43,1286,1287,1290,1293],{},[46,1288,1289],{},"Variable Scope",[46,1291,1292],{},"Who can read",[46,1294,1295],{},"Example",[56,1297,1298,1316,1332,1347],{},[43,1299,1300,1305,1308],{},[61,1301,1302],{},[64,1303,1304],{},"Convex backend",[61,1306,1307],{},"Convex functions only",[61,1309,1310,833,1312,833,1314],{},[73,1311,732],{},[73,1313,869],{},[73,1315,776],{},[43,1317,1318,1322,1325],{},[61,1319,1320],{},[64,1321,98],{},[61,1323,1324],{},"MTA process only",[61,1326,1327,833,1329,1331],{},[73,1328,756],{},[73,1330,766],{},", DKIM keys",[43,1333,1334,1338,1341],{},[61,1335,1336],{},[64,1337,82],{},[61,1339,1340],{},"Browser-safe only",[61,1342,1343,1346],{},[73,1344,1345],{},"CONVEX_SELF_HOSTED_URL"," (no secrets)",[43,1348,1349,1354,1357],{},[61,1350,1351],{},[64,1352,1353],{},"Agent containers",[61,1355,1356],{},"Task-specific only",[61,1358,1359,1362,1363,1366],{},[73,1360,1361],{},"CONVEX_URL"," (API endpoint), ",[73,1364,1365],{},"LLM_*"," (if agent needs direct LLM access)",[14,1368,1369,1370,1372],{},"Convex environment variables are never passed to agent-generated code. The ",[73,1371,1198],{}," sidecar receives only the Convex client URL and LLM configuration — it authenticates to Convex via a scoped API token, not the admin key.",[26,1374,1376],{"id":1375},"getting-started","Getting started",[227,1378,1382],{"className":1379,"code":1380,"language":1381,"meta":232,"style":232},"language-bash shiki shiki-themes github-light github-dark-dimmed","# 1. Clone the repository\ngit clone https:\u002F\u002Fgithub.com\u002Fowlat\u002Fowlat.git\ncd owlat\n\n# 2. Start the stack\ndocker compose up -d\n\n# 3. Generate admin credentials\ndocker compose exec convex .\u002Fgenerate_admin_key.sh\n\n# 4. Deploy the Convex functions\necho 'CONVEX_SELF_HOSTED_URL=http:\u002F\u002Flocalhost:3210' > .env.local\necho 'CONVEX_SELF_HOSTED_ADMIN_KEY=\u003Cyour-key>' >> .env.local\nnpx convex deploy\n\n# 5. Open the dashboard\nopen http:\u002F\u002Flocalhost:3000\n","bash",[73,1383,1384,1389,1401,1410,1414,1419,1433,1437,1442,1457,1461,1466,1481,1493,1503,1507,1512],{"__ignoreMap":232},[236,1385,1386],{"class":238,"line":239},[236,1387,1388],{"class":288},"# 1. Clone the repository\n",[236,1390,1391,1395,1398],{"class":238,"line":250},[236,1392,1394],{"class":1393},"sOLd2","git",[236,1396,1397],{"class":267}," clone",[236,1399,1400],{"class":267}," https:\u002F\u002Fgithub.com\u002Fowlat\u002Fowlat.git\n",[236,1402,1403,1407],{"class":238,"line":258},[236,1404,1406],{"class":1405},"sviXB","cd",[236,1408,1409],{"class":267}," owlat\n",[236,1411,1412],{"class":238,"line":271},[236,1413,350],{"emptyLinePlaceholder":349},[236,1415,1416],{"class":238,"line":279},[236,1417,1418],{"class":288},"# 2. Start the stack\n",[236,1420,1421,1424,1427,1430],{"class":238,"line":292},[236,1422,1423],{"class":1393},"docker",[236,1425,1426],{"class":267}," compose",[236,1428,1429],{"class":267}," up",[236,1431,1432],{"class":1405}," -d\n",[236,1434,1435],{"class":238,"line":303},[236,1436,350],{"emptyLinePlaceholder":349},[236,1438,1439],{"class":238,"line":314},[236,1440,1441],{"class":288},"# 3. Generate admin credentials\n",[236,1443,1444,1446,1448,1451,1454],{"class":238,"line":322},[236,1445,1423],{"class":1393},[236,1447,1426],{"class":267},[236,1449,1450],{"class":267}," exec",[236,1452,1453],{"class":267}," convex",[236,1455,1456],{"class":267}," .\u002Fgenerate_admin_key.sh\n",[236,1458,1459],{"class":238,"line":330},[236,1460,350],{"emptyLinePlaceholder":349},[236,1462,1463],{"class":238,"line":338},[236,1464,1465],{"class":288},"# 4. Deploy the Convex functions\n",[236,1467,1468,1471,1474,1478],{"class":238,"line":346},[236,1469,1470],{"class":1405},"echo",[236,1472,1473],{"class":267}," 'CONVEX_SELF_HOSTED_URL=http:\u002F\u002Flocalhost:3210'",[236,1475,1477],{"class":1476},"s7YZ4"," >",[236,1479,1480],{"class":267}," .env.local\n",[236,1482,1483,1485,1488,1491],{"class":238,"line":353},[236,1484,1470],{"class":1405},[236,1486,1487],{"class":267}," 'CONVEX_SELF_HOSTED_ADMIN_KEY=\u003Cyour-key>'",[236,1489,1490],{"class":1476}," >>",[236,1492,1480],{"class":267},[236,1494,1495,1498,1500],{"class":238,"line":361},[236,1496,1497],{"class":1393},"npx",[236,1499,1453],{"class":267},[236,1501,1502],{"class":267}," deploy\n",[236,1504,1505],{"class":238,"line":372},[236,1506,350],{"emptyLinePlaceholder":349},[236,1508,1509],{"class":238,"line":379},[236,1510,1511],{"class":288},"# 5. Open the dashboard\n",[236,1513,1514,1517],{"class":238,"line":387},[236,1515,1516],{"class":1393},"open",[236,1518,1519],{"class":267}," http:\u002F\u002Flocalhost:3000\n",[1521,1522,1523],"style",{},"html pre.shiki code .sIAta, html code.shiki .sIAta{--shiki-default:#22863A;--shiki-dark:#8DDB8C}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);}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 pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}",{"title":232,"searchDepth":250,"depth":250,"links":1525},[1526,1529,1530,1535,1536,1541],{"id":28,"depth":250,"text":29,"children":1527},[1528],{"id":160,"depth":258,"text":161},{"id":224,"depth":250,"text":225},{"id":706,"depth":250,"text":707,"children":1531},[1532,1533,1534],{"id":710,"depth":258,"text":711},{"id":795,"depth":258,"text":796},{"id":893,"depth":258,"text":894},{"id":979,"depth":250,"text":980},{"id":1074,"depth":250,"text":1075,"children":1537},[1538,1539,1540],{"id":1081,"depth":258,"text":1082},{"id":1168,"depth":258,"text":1169},{"id":1276,"depth":258,"text":1277},{"id":1375,"depth":250,"text":1376},"How Owlat runs as a fully self-hosted stack using Docker Compose — open-source Convex backend, custom MTA, and optional local LLM.","md",{},"\u002Fvision\u002Fself-hosting",{"title":5,"description":1542},"5.vision\u002F1.self-hosting","NZ78Z5TQwgZfgIhYY9in5WhNP4bUzD7ghmcYII9ONGw",[1550,1553],{"title":1551,"path":986,"stem":1552,"children":-1},"Vision","5.vision\u002F0.index",{"title":1554,"path":1555,"stem":1556,"children":-1},"Agent Pipeline","\u002Fvision\u002Fagent-pipeline","5.vision\u002F2.agent-pipeline",1774391045545]