[{"data":1,"prerenderedAt":5756},["ShallowReactive",2],{"seo-locale-availability:\u002Fblog":3,"all-posts:en":7},[4,5,6],"zh-CN","en","ja",[8,2534,4260,4525,4983,5388],{"id":9,"title":10,"accent":11,"body":12,"description":2506,"draft":2507,"experience":2508,"extension":2509,"featured":422,"image":2510,"meta":2516,"navigation":422,"path":2517,"publishedAt":2518,"readingTime":687,"seo":2519,"stem":2520,"tags":2521,"updatedAt":2508,"video":2526,"__hash__":2533,"contentLocale":5,"isFallback":2507},"posts_en\u002Fblog\u002Fcloudflare-email-service-send-receive-guide.md","Sending and Receiving Email with Cloudflare Email Service—from Routing to Workers, API, and SMTP","cyan",{"type":13,"value":14,"toc":2473},"minimark",[15,24,27,39,61,68,71,77,82,85,141,144,154,157,163,170,176,180,183,188,191,194,216,220,223,226,240,243,247,250,254,257,268,271,279,286,303,306,317,320,326,329,333,336,354,357,364,374,761,771,865,881,885,891,1026,1029,1046,1050,1076,1079,1100,1103,1107,1110,1116,1186,1192,1195,1214,1220,1224,1434,1440,1444,1447,1770,1773,1779,1784,1788,1791,1844,1847,1850,1870,1874,1877,1997,2000,2003,2007,2010,2016,2019,2042,2053,2056,2060,2063,2069,2072,2078,2082,2085,2109,2112,2132,2135,2139,2150,2156,2160,2163,2184,2187,2248,2251,2254,2286,2290,2293,2313,2316,2333,2336,2340,2343,2346,2349,2370,2373,2376,2469],[16,17,18,19,23],"p",{},"For a long time, Cloudflare’s relationship with email was mostly about two things: managing DNS and using Email Routing to forward ",[20,21,22],"code",{},"support@example.com"," to a real mailbox.",[16,25,26],{},"In 2026, that boundary moved considerably.",[16,28,29,30,34,35,38],{},"In April, Cloudflare moved ",[31,32,33],"strong",{},"Email Sending"," into public beta, allowing Workers to call ",[20,36,37],{},"env.EMAIL.send()"," directly for transactional email. Authenticated SMTP followed in June. Combined with Email Routing and Email Workers, Cloudflare Email Service can now:",[40,41,42,46,49,52,55,58],"ul",{},[43,44,45],"li",{},"Receive mail sent to your own domain.",[43,47,48],{},"Forward it to a verified mailbox.",[43,50,51],{},"Hand incoming mail to a Worker for parsing, filtering, storage, or automation.",[43,53,54],{},"Reply within the original email thread.",[43,56,57],{},"Send transactional mail from a Worker, REST API, or SMTP client.",[43,59,60],{},"Expose sending logs, bounces, suppressions, and delivery metrics.",[16,62,63,64,67],{},"This article reflects the official platform as of ",[31,65,66],{},"July 28, 2026",". It explains the complete sending and receiving model and proposes an implementation for projects built on Nuxt, Workers, and D1.",[16,69,70],{},"The short conclusion:",[72,73,74],"blockquote",{},[16,75,76],{},"Cloudflare Email Service is not a conventional IMAP \u002F POP3 mailbox, and it is not a webmail-based enterprise suite. It is better understood as a set of email entry points, sending channels, and programmable events for verification codes, password resets, order notifications, system alerts, support tickets, and email-driven automation.",[78,79,81],"h2",{"id":80},"separate-receiving-from-sending-first","Separate receiving from sending first",[16,83,84],{},"Sending and receiving look like one feature, but they are two independent paths inside Cloudflare.",[86,87,88,107],"table",{},[89,90,91],"thead",{},[92,93,94,98,101,104],"tr",{},[95,96,97],"th",{},"Direction",[95,99,100],{},"Cloudflare product",[95,102,103],{},"Entry point",[95,105,106],{},"What a Worker can do",[108,109,110,125],"tbody",{},[92,111,112,116,119,122],{},[113,114,115],"td",{},"Receive",[113,117,118],{},"Email Routing \u002F Email Workers",[113,120,121],{},"An external mail server connects to Cloudflare MX",[113,123,124],{},"Forward, reply, reject, parse, write to D1\u002FR2, or enqueue work",[92,126,127,130,132,135],{},[113,128,129],{},"Send",[113,131,33],{},[113,133,134],{},"Workers binding, REST API, or authenticated SMTP",[113,136,137,138],{},"Send text\u002FHTML, attachments, and custom headers, then receive a ",[20,139,140],{},"messageId",[16,142,143],{},"The receiving path is roughly:",[145,146,152],"pre",{"className":147,"code":149,"language":150,"meta":151},[148],"language-text","Sender SMTP\n  → Cloudflare MX\n  → SPF \u002F DKIM \u002F DMARC and reputation checks\n  → address rule or Catch-all match\n  → mailbox forwarding \u002F Worker \u002F drop\n","text","",[20,153,149],{"__ignoreMap":151},[16,155,156],{},"The sending path is:",[145,158,161],{"className":159,"code":160,"language":150,"meta":151},[148],"Nuxt API \u002F Worker \u002F external application\n  → EMAIL binding \u002F REST API \u002F SMTP\n  → sender-domain and parameter validation\n  → Cloudflare delivery pipeline\n  → recipient mail server\n",[20,162,160],{"__ignoreMap":151},[16,164,165],{},[166,167],"img",{"alt":168,"src":169},"Email is checked at the edge and then routed to a real mailbox, Worker processing node, or rejection path","\u002Fmedia\u002Fposts\u002Fcloudflare-email-service-send-receive-guide\u002Finbound-routing-v1.webp",[16,171,172],{},[173,174,175],"em",{},"This is a conceptual diagram. Real decisions also include the SMTP session, email authentication, rule matching, and Worker execution limits.",[78,177,179],{"id":178},"product-boundaries-and-pricing-in-2026","Product boundaries and pricing in 2026",[16,181,182],{},"Cloudflare currently groups receiving and sending under Email Service, but the two parts have different plan availability.",[184,185,187],"h3",{"id":186},"email-routing-receiving-and-forwarding","Email Routing: receiving and forwarding",[16,189,190],{},"Email Routing is available on both Workers Free and Workers Paid. The official pricing page lists unlimited inbound email, although messages processed by a Worker still consume the corresponding Worker requests and compute.",[16,192,193],{},"It works well for:",[40,195,196,203,206,213],{},[43,197,198,199,202],{},"Forwarding ",[20,200,201],{},"support@yourdomain.com"," to an existing personal mailbox.",[43,204,205],{},"Creating a separate contact address for each site.",[43,207,208,209,212],{},"Using sub-addresses such as ",[20,210,211],{},"orders+shop-a@yourdomain.com"," to identify a source.",[43,214,215],{},"Turning incoming mail into a ticket, webhook, or database record with a Worker.",[184,217,219],{"id":218},"email-sending-transactional-email","Email Sending: transactional email",[16,221,222],{},"Email Sending remains in public beta.",[16,224,225],{},"Sending to arbitrary recipients requires the Workers Paid plan. According to the official July 2026 pricing:",[40,227,228,231,234,237],{},[43,229,230],{},"Each account includes 3,000 outbound messages per month.",[43,232,233],{},"Additional messages cost $0.35 per 1,000.",[43,235,236],{},"Messages sent to “verified destination addresses” in the account are always free and do not use the included allowance.",[43,238,239],{},"A message accepted by Email Service normally counts even if it later hard-bounces; a request rejected at the API boundary does not.",[16,241,242],{},"Pricing and beta status can change. Check the official pricing page again before production instead of permanently encoding these numbers into a budget model.",[78,244,246],{"id":245},"step-one-connect-the-domain-correctly","Step one: connect the domain correctly",[16,248,249],{},"Email Service requires the domain to use Cloudflare DNS. Sending and receiving create two sets of records for different purposes.",[184,251,253],{"id":252},"receiving-uses-root-domain-records","Receiving uses root-domain records",[16,255,256],{},"When Email Routing is enabled, Cloudflare configures the domain root with:",[40,258,259,262,265],{},[43,260,261],{},"MX records that deliver incoming mail to Cloudflare.",[43,263,264],{},"SPF authorization for Cloudflare forwarding.",[43,266,267],{},"DKIM signatures for the forwarding path.",[16,269,270],{},"These records cover “someone sends mail to your domain.”",[184,272,274,275,278],{"id":273},"sending-uses-cf-bounce-records","Sending uses ",[20,276,277],{},"cf-bounce"," records",[16,280,281,282,285],{},"When Email Sending is enabled, Cloudflare configures ",[20,283,284],{},"cf-bounce.yourdomain.com"," with:",[40,287,288,291,294,297],{},[43,289,290],{},"MX records for bounce processing.",[43,292,293],{},"SPF authorization for Cloudflare to send on behalf of the domain.",[43,295,296],{},"DKIM verification that the message was not modified in transit.",[43,298,299,302],{},[20,300,301],{},"_dmarc.yourdomain.com",", which declares how authentication failures should be handled.",[16,304,305],{},"Sending and receiving use different DKIM selectors. Seeing two DKIM records does not mean the setup is duplicated.",[16,307,308,309,312,313,316],{},"If the domain already uses Google Workspace, Microsoft 365, or another mail service, check MX and SPF conflicts carefully. A domain can have only one valid SPF policy. To authorize several providers, merge their ",[20,310,311],{},"include"," values instead of creating a second ",[20,314,315],{},"v=spf1"," record.",[16,318,319],{},"I recommend separating reputation-sensitive purposes across subdomains:",[145,321,324],{"className":322,"code":323,"language":150,"meta":151},[148],"noreply@example.com          account verification and password resets\nnotify@mail.example.com      product notifications\nsupport@example.com          human support and incoming mail\n",[20,325,323],{"__ignoreMap":151},[16,327,328],{},"Transactional notifications, human correspondence, and possible future bulk mail will not damage one another’s reputation, and failure and migration boundaries become clearer.",[78,330,332],{"id":331},"receiving-mail-from-forwarding-rules-to-an-email-worker","Receiving mail: from forwarding rules to an Email Worker",[16,334,335],{},"The simplest way to receive mail requires no code:",[337,338,339,342,348,351],"ol",{},[43,340,341],{},"Add a verified destination mailbox in Email Routing.",[43,343,344,345,347],{},"Create ",[20,346,201],{},".",[43,349,350],{},"Select “Send to an email.”",[43,352,353],{},"Cloudflare forwards incoming messages to the destination.",[16,355,356],{},"If you need to classify by subject, extract attachments, create tickets, or send automatic replies, change the action to “Send to a Worker.”",[184,358,360,363],{"id":359},"email-is-the-inbound-entry-point",[20,361,362],{},"email()"," is the inbound entry point",[16,365,366,367,369,370,373],{},"An Email Worker enters through ",[20,368,362],{},", not ",[20,371,372],{},"fetch()",":",[145,375,379],{"className":376,"code":377,"language":378,"meta":151,"style":151},"language-ts shiki shiki-themes github-dark github-dark","interface Env {\n  SUPPORT_FORWARD_TO: string\n}\n\nexport default {\n  async email(message, env): Promise\u003Cvoid> {\n    const recipient = message.to.toLowerCase()\n    const subject = message.headers.get('subject') ?? ''\n\n    if (message.rawSize > 10 * 1024 * 1024) {\n      message.setReject('Message is too large for this mailbox')\n      return\n    }\n\n    if (recipient === 'support@example.com') {\n      console.log('Support email accepted', {\n        fromDomain: message.from.split('@').at(-1),\n        subjectLength: subject.length,\n        rawSize: message.rawSize,\n      })\n\n      await message.forward(env.SUPPORT_FORWARD_TO)\n      return\n    }\n\n    message.setReject('Unknown recipient')\n  },\n} satisfies ExportedHandler\u003CEnv>\n","ts",[20,380,381,398,411,417,424,435,473,494,525,530,558,575,581,587,592,608,625,656,668,674,680,685,705,710,715,720,735,741],{"__ignoreMap":151},[382,383,386,390,394],"span",{"class":384,"line":385},"line",1,[382,387,389],{"class":388},"sOPea","interface",[382,391,393],{"class":392},"sFR8T"," Env",[382,395,397],{"class":396},"suv1-"," {\n",[382,399,401,405,407],{"class":384,"line":400},2,[382,402,404],{"class":403},"s-3mD","  SUPPORT_FORWARD_TO",[382,406,373],{"class":388},[382,408,410],{"class":409},"s8ozJ"," string\n",[382,412,414],{"class":384,"line":413},3,[382,415,416],{"class":396},"}\n",[382,418,420],{"class":384,"line":419},4,[382,421,423],{"emptyLinePlaceholder":422},true,"\n",[382,425,427,430,433],{"class":384,"line":426},5,[382,428,429],{"class":388},"export",[382,431,432],{"class":388}," default",[382,434,397],{"class":396},[382,436,438,441,444,447,450,453,456,459,461,464,467,470],{"class":384,"line":437},6,[382,439,440],{"class":388},"  async",[382,442,443],{"class":392}," email",[382,445,446],{"class":396},"(",[382,448,449],{"class":403},"message",[382,451,452],{"class":396},", ",[382,454,455],{"class":403},"env",[382,457,458],{"class":396},")",[382,460,373],{"class":388},[382,462,463],{"class":392}," Promise",[382,465,466],{"class":396},"\u003C",[382,468,469],{"class":409},"void",[382,471,472],{"class":396},"> {\n",[382,474,476,479,482,485,488,491],{"class":384,"line":475},7,[382,477,478],{"class":388},"    const",[382,480,481],{"class":409}," recipient",[382,483,484],{"class":388}," =",[382,486,487],{"class":396}," message.to.",[382,489,490],{"class":392},"toLowerCase",[382,492,493],{"class":396},"()\n",[382,495,497,499,502,504,507,510,512,516,519,522],{"class":384,"line":496},8,[382,498,478],{"class":388},[382,500,501],{"class":409}," subject",[382,503,484],{"class":388},[382,505,506],{"class":396}," message.headers.",[382,508,509],{"class":392},"get",[382,511,446],{"class":396},[382,513,515],{"class":514},"s4wv1","'subject'",[382,517,518],{"class":396},") ",[382,520,521],{"class":388},"??",[382,523,524],{"class":514}," ''\n",[382,526,528],{"class":384,"line":527},9,[382,529,423],{"emptyLinePlaceholder":422},[382,531,533,536,539,542,545,548,551,553,555],{"class":384,"line":532},10,[382,534,535],{"class":388},"    if",[382,537,538],{"class":396}," (message.rawSize ",[382,540,541],{"class":388},">",[382,543,544],{"class":409}," 10",[382,546,547],{"class":388}," *",[382,549,550],{"class":409}," 1024",[382,552,547],{"class":388},[382,554,550],{"class":409},[382,556,557],{"class":396},") {\n",[382,559,561,564,567,569,572],{"class":384,"line":560},11,[382,562,563],{"class":396},"      message.",[382,565,566],{"class":392},"setReject",[382,568,446],{"class":396},[382,570,571],{"class":514},"'Message is too large for this mailbox'",[382,573,574],{"class":396},")\n",[382,576,578],{"class":384,"line":577},12,[382,579,580],{"class":388},"      return\n",[382,582,584],{"class":384,"line":583},13,[382,585,586],{"class":396},"    }\n",[382,588,590],{"class":384,"line":589},14,[382,591,423],{"emptyLinePlaceholder":422},[382,593,595,597,600,603,606],{"class":384,"line":594},15,[382,596,535],{"class":388},[382,598,599],{"class":396}," (recipient ",[382,601,602],{"class":388},"===",[382,604,605],{"class":514}," 'support@example.com'",[382,607,557],{"class":396},[382,609,611,614,617,619,622],{"class":384,"line":610},16,[382,612,613],{"class":396},"      console.",[382,615,616],{"class":392},"log",[382,618,446],{"class":396},[382,620,621],{"class":514},"'Support email accepted'",[382,623,624],{"class":396},", {\n",[382,626,628,631,634,636,639,642,645,647,650,653],{"class":384,"line":627},17,[382,629,630],{"class":396},"        fromDomain: message.from.",[382,632,633],{"class":392},"split",[382,635,446],{"class":396},[382,637,638],{"class":514},"'@'",[382,640,641],{"class":396},").",[382,643,644],{"class":392},"at",[382,646,446],{"class":396},[382,648,649],{"class":388},"-",[382,651,652],{"class":409},"1",[382,654,655],{"class":396},"),\n",[382,657,659,662,665],{"class":384,"line":658},18,[382,660,661],{"class":396},"        subjectLength: subject.",[382,663,664],{"class":409},"length",[382,666,667],{"class":396},",\n",[382,669,671],{"class":384,"line":670},19,[382,672,673],{"class":396},"        rawSize: message.rawSize,\n",[382,675,677],{"class":384,"line":676},20,[382,678,679],{"class":396},"      })\n",[382,681,683],{"class":384,"line":682},21,[382,684,423],{"emptyLinePlaceholder":422},[382,686,688,691,694,697,700,703],{"class":384,"line":687},22,[382,689,690],{"class":388},"      await",[382,692,693],{"class":396}," message.",[382,695,696],{"class":392},"forward",[382,698,699],{"class":396},"(env.",[382,701,702],{"class":409},"SUPPORT_FORWARD_TO",[382,704,574],{"class":396},[382,706,708],{"class":384,"line":707},23,[382,709,580],{"class":388},[382,711,713],{"class":384,"line":712},24,[382,714,586],{"class":396},[382,716,718],{"class":384,"line":717},25,[382,719,423],{"emptyLinePlaceholder":422},[382,721,723,726,728,730,733],{"class":384,"line":722},26,[382,724,725],{"class":396},"    message.",[382,727,566],{"class":392},[382,729,446],{"class":396},[382,731,732],{"class":514},"'Unknown recipient'",[382,734,574],{"class":396},[382,736,738],{"class":384,"line":737},27,[382,739,740],{"class":396},"  },\n",[382,742,744,747,750,753,755,758],{"class":384,"line":743},28,[382,745,746],{"class":396},"} ",[382,748,749],{"class":388},"satisfies",[382,751,752],{"class":392}," ExportedHandler",[382,754,466],{"class":396},[382,756,757],{"class":392},"Env",[382,759,760],{"class":396},">\n",[16,762,763,764,766,767,770],{},"Here, ",[20,765,449],{}," is a ",[20,768,769],{},"ForwardableEmailMessage",". Common fields and operations include:",[86,772,773,783],{},[89,774,775],{},[92,776,777,780],{},[95,778,779],{},"Capability",[95,781,782],{},"Purpose",[108,784,785,795,805,815,825,835,845,855],{},[92,786,787,792],{},[113,788,789],{},[20,790,791],{},"message.from",[113,793,794],{},"Sender in the SMTP envelope",[92,796,797,802],{},[113,798,799],{},[20,800,801],{},"message.to",[113,803,804],{},"Recipient in the SMTP envelope",[92,806,807,812],{},[113,808,809],{},[20,810,811],{},"message.headers",[113,813,814],{},"Read Subject, Message-ID, and other headers",[92,816,817,822],{},[113,818,819],{},[20,820,821],{},"message.raw",[113,823,824],{},"Raw MIME stream",[92,826,827,832],{},[113,828,829],{},[20,830,831],{},"message.rawSize",[113,833,834],{},"Size of the raw message",[92,836,837,842],{},[113,838,839],{},[20,840,841],{},"message.forward(address)",[113,843,844],{},"Forward to a verified destination address",[92,846,847,852],{},[113,848,849],{},[20,850,851],{},"message.reply(email)",[113,853,854],{},"Reply to the original sender within platform constraints",[92,856,857,862],{},[113,858,859],{},[20,860,861],{},"message.setReject(reason)",[113,863,864],{},"Return a permanent SMTP rejection",[16,866,867,868,870,871,873,874,870,877,880],{},"There is an easy-to-miss security detail: ",[20,869,791],{}," and ",[20,872,801],{}," come from the SMTP envelope, while ",[20,875,876],{},"From:",[20,878,879],{},"To:"," inside the message headers can contain different values. For authorization, routing, and auditing, do not mistake a user-visible header address for a trusted identity.",[184,882,884],{"id":883},"parse-mime-only-when-you-need-the-body","Parse MIME only when you need the body",[16,886,887,888,373],{},"There is no reason to fully parse a message if you only forward it or route it by address. To extract the body and attachments, use ",[20,889,890],{},"postal-mime",[145,892,894],{"className":376,"code":893,"language":378,"meta":151,"style":151},"import PostalMime from 'postal-mime'\n\nexport default {\n  async email(message): Promise\u003Cvoid> {\n    const parsed = await PostalMime.parse(message.raw)\n\n    console.log({\n      subject: parsed.subject,\n      textLength: parsed.text?.length ?? 0,\n      attachmentCount: parsed.attachments.length,\n    })\n  },\n} satisfies ExportedHandler\n",[20,895,896,910,914,922,944,965,969,979,984,999,1008,1013,1017],{"__ignoreMap":151},[382,897,898,901,904,907],{"class":384,"line":385},[382,899,900],{"class":388},"import",[382,902,903],{"class":396}," PostalMime ",[382,905,906],{"class":388},"from",[382,908,909],{"class":514}," 'postal-mime'\n",[382,911,912],{"class":384,"line":400},[382,913,423],{"emptyLinePlaceholder":422},[382,915,916,918,920],{"class":384,"line":413},[382,917,429],{"class":388},[382,919,432],{"class":388},[382,921,397],{"class":396},[382,923,924,926,928,930,932,934,936,938,940,942],{"class":384,"line":419},[382,925,440],{"class":388},[382,927,443],{"class":392},[382,929,446],{"class":396},[382,931,449],{"class":403},[382,933,458],{"class":396},[382,935,373],{"class":388},[382,937,463],{"class":392},[382,939,466],{"class":396},[382,941,469],{"class":409},[382,943,472],{"class":396},[382,945,946,948,951,953,956,959,962],{"class":384,"line":426},[382,947,478],{"class":388},[382,949,950],{"class":409}," parsed",[382,952,484],{"class":388},[382,954,955],{"class":388}," await",[382,957,958],{"class":396}," PostalMime.",[382,960,961],{"class":392},"parse",[382,963,964],{"class":396},"(message.raw)\n",[382,966,967],{"class":384,"line":437},[382,968,423],{"emptyLinePlaceholder":422},[382,970,971,974,976],{"class":384,"line":475},[382,972,973],{"class":396},"    console.",[382,975,616],{"class":392},[382,977,978],{"class":396},"({\n",[382,980,981],{"class":384,"line":496},[382,982,983],{"class":396},"      subject: parsed.subject,\n",[382,985,986,989,991,994,997],{"class":384,"line":527},[382,987,988],{"class":396},"      textLength: parsed.text?.",[382,990,664],{"class":409},[382,992,993],{"class":388}," ??",[382,995,996],{"class":409}," 0",[382,998,667],{"class":396},[382,1000,1001,1004,1006],{"class":384,"line":532},[382,1002,1003],{"class":396},"      attachmentCount: parsed.attachments.",[382,1005,664],{"class":409},[382,1007,667],{"class":396},[382,1009,1010],{"class":384,"line":560},[382,1011,1012],{"class":396},"    })\n",[382,1014,1015],{"class":384,"line":577},[382,1016,740],{"class":396},[382,1018,1019,1021,1023],{"class":384,"line":583},[382,1020,746],{"class":396},[382,1022,749],{"class":388},[382,1024,1025],{"class":392}," ExportedHandler\n",[16,1027,1028],{},"Parsed HTML, filenames, Content-Type values, and attachments all come from external input. Safe handling means:",[40,1030,1031,1034,1037,1040,1043],{},[43,1032,1033],{},"Do not inject incoming HTML directly into an administration page.",[43,1035,1036],{},"Limit body size, attachment count, and expanded size.",[43,1038,1039],{},"Generate a new object key before writing a file to R2; do not use the original filename as the path.",[43,1041,1042],{},"Never let message content directly construct SQL, shell commands, templates, or AI tool parameters.",[43,1044,1045],{},"Send expensive work to a Queue instead of putting malware scanning, OCR, and model calls into the synchronous CPU time of an inbound event.",[184,1047,1049],{"id":1048},"forwarding-replying-rejecting-and-silently-dropping-are-different","Forwarding, replying, rejecting, and silently dropping are different",[40,1051,1052,1058,1064,1070],{},[43,1053,1054,1057],{},[31,1055,1056],{},"Forward:"," continue handling in a human mailbox; the destination must already be verified.",[43,1059,1060,1063],{},[31,1061,1062],{},"Reply:"," useful for confirmations or mail bots, with additional restrictions.",[43,1065,1066,1069],{},[31,1067,1068],{},"Reject:"," explicitly tell the sender during SMTP that the message was not accepted.",[43,1071,1072,1075],{},[31,1073,1074],{},"Drop:"," stop processing after a rule match without business-level feedback to the sender.",[16,1077,1078],{},"An automatic reply is not an arbitrary sending interface. The current platform requires:",[40,1080,1081,1084,1087,1090,1093],{},[43,1082,1083],{},"A valid DMARC result on the incoming message.",[43,1085,1086],{},"Only one reply call per inbound event.",[43,1088,1089],{},"The reply recipient to be the original sender.",[43,1091,1092],{},"The reply sender domain to match the domain that received the message.",[43,1094,1095,1096,1099],{},"Fewer than 100 entries in ",[20,1097,1098],{},"References",", preventing mail loops.",[16,1101,1102],{},"These restrictions are helpful: “reply to this message” and “send a new message to any address” become two different permissions.",[78,1104,1106],{"id":1105},"sending-mail-prefer-a-workers-binding","Sending mail: prefer a Workers binding",[16,1108,1109],{},"If the application already runs on Cloudflare Workers, the Workers binding is the most natural option. It does not require a Cloudflare API token in business code.",[16,1111,1112,1113,373],{},"Declare the binding in ",[20,1114,1115],{},"wrangler.jsonc",[145,1117,1121],{"className":1118,"code":1119,"language":1120,"meta":151,"style":151},"language-jsonc shiki shiki-themes github-dark github-dark","{\n  \"send_email\": [\n    {\n      \"name\": \"EMAIL\",\n      \"allowed_sender_addresses\": [\n        \"noreply@example.com\"\n      ]\n    }\n  ],\n  \"vars\": {\n    \"EMAIL_FROM\": \"noreply@example.com\"\n  }\n}\n","jsonc",[20,1122,1123,1128,1133,1138,1143,1148,1153,1158,1162,1167,1172,1177,1182],{"__ignoreMap":151},[382,1124,1125],{"class":384,"line":385},[382,1126,1127],{},"{\n",[382,1129,1130],{"class":384,"line":400},[382,1131,1132],{},"  \"send_email\": [\n",[382,1134,1135],{"class":384,"line":413},[382,1136,1137],{},"    {\n",[382,1139,1140],{"class":384,"line":419},[382,1141,1142],{},"      \"name\": \"EMAIL\",\n",[382,1144,1145],{"class":384,"line":426},[382,1146,1147],{},"      \"allowed_sender_addresses\": [\n",[382,1149,1150],{"class":384,"line":437},[382,1151,1152],{},"        \"noreply@example.com\"\n",[382,1154,1155],{"class":384,"line":475},[382,1156,1157],{},"      ]\n",[382,1159,1160],{"class":384,"line":496},[382,1161,586],{},[382,1163,1164],{"class":384,"line":527},[382,1165,1166],{},"  ],\n",[382,1168,1169],{"class":384,"line":532},[382,1170,1171],{},"  \"vars\": {\n",[382,1173,1174],{"class":384,"line":560},[382,1175,1176],{},"    \"EMAIL_FROM\": \"noreply@example.com\"\n",[382,1178,1179],{"class":384,"line":577},[382,1180,1181],{},"  }\n",[382,1183,1184],{"class":384,"line":583},[382,1185,416],{},[16,1187,1188,1191],{},[20,1189,1190],{},"allowed_sender_addresses"," is not merely documentation; it is a runtime permission boundary. Even if an endpoint is misused, the binding cannot impersonate another sender address.",[16,1193,1194],{},"The following settings can constrain recipients further:",[40,1196,1197,1203,1209],{},[43,1198,1199,1202],{},[20,1200,1201],{},"destination_address",": fix the binding to one destination.",[43,1204,1205,1208],{},[20,1206,1207],{},"allowed_destination_addresses",": allow only a set of destinations.",[43,1210,1211,1213],{},[20,1212,1190],{},": restrict available sender addresses.",[16,1215,1216,1217,1219],{},"A Worker used for system alerts is a good fit for a fixed ",[20,1218,1201],{},". A user-verification service normally restricts the sender and validates recipient and frequency in the application.",[184,1221,1223],{"id":1222},"sending-inside-a-worker","Sending inside a Worker",[145,1225,1227],{"className":376,"code":1226,"language":378,"meta":151,"style":151},"interface Env {\n  EMAIL: SendEmail\n}\n\nexport default {\n  async fetch(_request, env): Promise\u003CResponse> {\n    const result = await env.EMAIL.send({\n      from: {\n        email: 'noreply@example.com',\n        name: 'Example App',\n      },\n      to: 'reader@example.net',\n      subject: 'Verify your email',\n      text: 'Your verification code is 482913. It is valid for 5 minutes.',\n      html: '\u003Cp>Your verification code is \u003Cstrong>482913\u003C\u002Fstrong>. It is valid for 5 minutes.\u003C\u002Fp>',\n    })\n\n    return Response.json({\n      messageId: result.messageId,\n    })\n  },\n} satisfies ExportedHandler\u003CEnv>\n",[20,1228,1229,1237,1247,1251,1255,1263,1292,1316,1321,1331,1341,1346,1356,1366,1376,1386,1390,1394,1407,1412,1416,1420],{"__ignoreMap":151},[382,1230,1231,1233,1235],{"class":384,"line":385},[382,1232,389],{"class":388},[382,1234,393],{"class":392},[382,1236,397],{"class":396},[382,1238,1239,1242,1244],{"class":384,"line":400},[382,1240,1241],{"class":403},"  EMAIL",[382,1243,373],{"class":388},[382,1245,1246],{"class":392}," SendEmail\n",[382,1248,1249],{"class":384,"line":413},[382,1250,416],{"class":396},[382,1252,1253],{"class":384,"line":419},[382,1254,423],{"emptyLinePlaceholder":422},[382,1256,1257,1259,1261],{"class":384,"line":426},[382,1258,429],{"class":388},[382,1260,432],{"class":388},[382,1262,397],{"class":396},[382,1264,1265,1267,1270,1272,1275,1277,1279,1281,1283,1285,1287,1290],{"class":384,"line":437},[382,1266,440],{"class":388},[382,1268,1269],{"class":392}," fetch",[382,1271,446],{"class":396},[382,1273,1274],{"class":403},"_request",[382,1276,452],{"class":396},[382,1278,455],{"class":403},[382,1280,458],{"class":396},[382,1282,373],{"class":388},[382,1284,463],{"class":392},[382,1286,466],{"class":396},[382,1288,1289],{"class":392},"Response",[382,1291,472],{"class":396},[382,1293,1294,1296,1299,1301,1303,1306,1309,1311,1314],{"class":384,"line":475},[382,1295,478],{"class":388},[382,1297,1298],{"class":409}," result",[382,1300,484],{"class":388},[382,1302,955],{"class":388},[382,1304,1305],{"class":396}," env.",[382,1307,1308],{"class":409},"EMAIL",[382,1310,347],{"class":396},[382,1312,1313],{"class":392},"send",[382,1315,978],{"class":396},[382,1317,1318],{"class":384,"line":496},[382,1319,1320],{"class":396},"      from: {\n",[382,1322,1323,1326,1329],{"class":384,"line":527},[382,1324,1325],{"class":396},"        email: ",[382,1327,1328],{"class":514},"'noreply@example.com'",[382,1330,667],{"class":396},[382,1332,1333,1336,1339],{"class":384,"line":532},[382,1334,1335],{"class":396},"        name: ",[382,1337,1338],{"class":514},"'Example App'",[382,1340,667],{"class":396},[382,1342,1343],{"class":384,"line":560},[382,1344,1345],{"class":396},"      },\n",[382,1347,1348,1351,1354],{"class":384,"line":577},[382,1349,1350],{"class":396},"      to: ",[382,1352,1353],{"class":514},"'reader@example.net'",[382,1355,667],{"class":396},[382,1357,1358,1361,1364],{"class":384,"line":583},[382,1359,1360],{"class":396},"      subject: ",[382,1362,1363],{"class":514},"'Verify your email'",[382,1365,667],{"class":396},[382,1367,1368,1371,1374],{"class":384,"line":589},[382,1369,1370],{"class":396},"      text: ",[382,1372,1373],{"class":514},"'Your verification code is 482913. It is valid for 5 minutes.'",[382,1375,667],{"class":396},[382,1377,1378,1381,1384],{"class":384,"line":594},[382,1379,1380],{"class":396},"      html: ",[382,1382,1383],{"class":514},"'\u003Cp>Your verification code is \u003Cstrong>482913\u003C\u002Fstrong>. It is valid for 5 minutes.\u003C\u002Fp>'",[382,1385,667],{"class":396},[382,1387,1388],{"class":384,"line":610},[382,1389,1012],{"class":396},[382,1391,1392],{"class":384,"line":627},[382,1393,423],{"emptyLinePlaceholder":422},[382,1395,1396,1399,1402,1405],{"class":384,"line":658},[382,1397,1398],{"class":388},"    return",[382,1400,1401],{"class":396}," Response.",[382,1403,1404],{"class":392},"json",[382,1406,978],{"class":396},[382,1408,1409],{"class":384,"line":670},[382,1410,1411],{"class":396},"      messageId: result.messageId,\n",[382,1413,1414],{"class":384,"line":676},[382,1415,1012],{"class":396},[382,1417,1418],{"class":384,"line":682},[382,1419,740],{"class":396},[382,1421,1422,1424,1426,1428,1430,1432],{"class":384,"line":687},[382,1423,746],{"class":396},[382,1425,749],{"class":388},[382,1427,752],{"class":392},[382,1429,466],{"class":396},[382,1431,757],{"class":392},[382,1433,760],{"class":396},[16,1435,1436,1437,1439],{},"A successful Workers binding call returns a ",[20,1438,140],{},". It means Cloudflare accepted the sending request; it does not prove the message is already visible in the user’s inbox. Final delivery, delay, bounce, and suppression status belong in Email Service logs and metrics.",[184,1441,1443],{"id":1442},"sending-from-a-nuxt-server-api","Sending from a Nuxt server API",[16,1445,1446],{},"Once Nuxt is deployed to Workers, a server route can read the same binding:",[145,1448,1450],{"className":376,"code":1449,"language":378,"meta":151,"style":151},"import { z } from 'zod'\n\nconst schema = z.object({\n  email: z.string().trim().email().max(254),\n})\n\nexport default defineEventHandler(async (event) => {\n  const { email } = schema.parse(await readBody(event))\n  const env = event.context.cloudflare.env as {\n    EMAIL: SendEmail\n    EMAIL_FROM: string\n  }\n\n  \u002F\u002F A real project should also validate same-origin requests,\n  \u002F\u002F rate-limit IPs and email addresses, and atomically claim\n  \u002F\u002F one sending opportunity in D1.\n  const result = await env.EMAIL.send({\n    from: {\n      email: env.EMAIL_FROM,\n      name: 'Example App',\n    },\n    to: email,\n    subject: 'Verify your email',\n    text: 'The verification code will expire in 5 minutes.',\n    html: '\u003Cp>The verification code will expire in \u003Cstrong>5 minutes\u003C\u002Fstrong>.\u003C\u002Fp>',\n  })\n\n  return {\n    ok: true,\n    messageId: result.messageId,\n  }\n})\n",[20,1451,1452,1464,1468,1486,1517,1522,1526,1553,1585,1602,1611,1620,1624,1628,1634,1639,1644,1664,1669,1679,1688,1693,1698,1707,1717,1727,1732,1736,1743,1754,1760,1765],{"__ignoreMap":151},[382,1453,1454,1456,1459,1461],{"class":384,"line":385},[382,1455,900],{"class":388},[382,1457,1458],{"class":396}," { z } ",[382,1460,906],{"class":388},[382,1462,1463],{"class":514}," 'zod'\n",[382,1465,1466],{"class":384,"line":400},[382,1467,423],{"emptyLinePlaceholder":422},[382,1469,1470,1473,1476,1478,1481,1484],{"class":384,"line":413},[382,1471,1472],{"class":388},"const",[382,1474,1475],{"class":409}," schema",[382,1477,484],{"class":388},[382,1479,1480],{"class":396}," z.",[382,1482,1483],{"class":392},"object",[382,1485,978],{"class":396},[382,1487,1488,1491,1494,1497,1500,1502,1505,1507,1510,1512,1515],{"class":384,"line":419},[382,1489,1490],{"class":396},"  email: z.",[382,1492,1493],{"class":392},"string",[382,1495,1496],{"class":396},"().",[382,1498,1499],{"class":392},"trim",[382,1501,1496],{"class":396},[382,1503,1504],{"class":392},"email",[382,1506,1496],{"class":396},[382,1508,1509],{"class":392},"max",[382,1511,446],{"class":396},[382,1513,1514],{"class":409},"254",[382,1516,655],{"class":396},[382,1518,1519],{"class":384,"line":426},[382,1520,1521],{"class":396},"})\n",[382,1523,1524],{"class":384,"line":437},[382,1525,423],{"emptyLinePlaceholder":422},[382,1527,1528,1530,1532,1535,1537,1540,1543,1546,1548,1551],{"class":384,"line":475},[382,1529,429],{"class":388},[382,1531,432],{"class":388},[382,1533,1534],{"class":392}," defineEventHandler",[382,1536,446],{"class":396},[382,1538,1539],{"class":388},"async",[382,1541,1542],{"class":396}," (",[382,1544,1545],{"class":403},"event",[382,1547,518],{"class":396},[382,1549,1550],{"class":388},"=>",[382,1552,397],{"class":396},[382,1554,1555,1558,1561,1563,1566,1569,1572,1574,1576,1579,1582],{"class":384,"line":496},[382,1556,1557],{"class":388},"  const",[382,1559,1560],{"class":396}," { ",[382,1562,1504],{"class":409},[382,1564,1565],{"class":396}," } ",[382,1567,1568],{"class":388},"=",[382,1570,1571],{"class":396}," schema.",[382,1573,961],{"class":392},[382,1575,446],{"class":396},[382,1577,1578],{"class":388},"await",[382,1580,1581],{"class":392}," readBody",[382,1583,1584],{"class":396},"(event))\n",[382,1586,1587,1589,1592,1594,1597,1600],{"class":384,"line":527},[382,1588,1557],{"class":388},[382,1590,1591],{"class":409}," env",[382,1593,484],{"class":388},[382,1595,1596],{"class":396}," event.context.cloudflare.env ",[382,1598,1599],{"class":388},"as",[382,1601,397],{"class":396},[382,1603,1604,1607,1609],{"class":384,"line":532},[382,1605,1606],{"class":403},"    EMAIL",[382,1608,373],{"class":388},[382,1610,1246],{"class":392},[382,1612,1613,1616,1618],{"class":384,"line":560},[382,1614,1615],{"class":403},"    EMAIL_FROM",[382,1617,373],{"class":388},[382,1619,410],{"class":409},[382,1621,1622],{"class":384,"line":577},[382,1623,1181],{"class":396},[382,1625,1626],{"class":384,"line":583},[382,1627,423],{"emptyLinePlaceholder":422},[382,1629,1630],{"class":384,"line":589},[382,1631,1633],{"class":1632},"sJ8bj","  \u002F\u002F A real project should also validate same-origin requests,\n",[382,1635,1636],{"class":384,"line":594},[382,1637,1638],{"class":1632},"  \u002F\u002F rate-limit IPs and email addresses, and atomically claim\n",[382,1640,1641],{"class":384,"line":610},[382,1642,1643],{"class":1632},"  \u002F\u002F one sending opportunity in D1.\n",[382,1645,1646,1648,1650,1652,1654,1656,1658,1660,1662],{"class":384,"line":627},[382,1647,1557],{"class":388},[382,1649,1298],{"class":409},[382,1651,484],{"class":388},[382,1653,955],{"class":388},[382,1655,1305],{"class":396},[382,1657,1308],{"class":409},[382,1659,347],{"class":396},[382,1661,1313],{"class":392},[382,1663,978],{"class":396},[382,1665,1666],{"class":384,"line":658},[382,1667,1668],{"class":396},"    from: {\n",[382,1670,1671,1674,1677],{"class":384,"line":670},[382,1672,1673],{"class":396},"      email: env.",[382,1675,1676],{"class":409},"EMAIL_FROM",[382,1678,667],{"class":396},[382,1680,1681,1684,1686],{"class":384,"line":676},[382,1682,1683],{"class":396},"      name: ",[382,1685,1338],{"class":514},[382,1687,667],{"class":396},[382,1689,1690],{"class":384,"line":682},[382,1691,1692],{"class":396},"    },\n",[382,1694,1695],{"class":384,"line":687},[382,1696,1697],{"class":396},"    to: email,\n",[382,1699,1700,1703,1705],{"class":384,"line":707},[382,1701,1702],{"class":396},"    subject: ",[382,1704,1363],{"class":514},[382,1706,667],{"class":396},[382,1708,1709,1712,1715],{"class":384,"line":712},[382,1710,1711],{"class":396},"    text: ",[382,1713,1714],{"class":514},"'The verification code will expire in 5 minutes.'",[382,1716,667],{"class":396},[382,1718,1719,1722,1725],{"class":384,"line":717},[382,1720,1721],{"class":396},"    html: ",[382,1723,1724],{"class":514},"'\u003Cp>The verification code will expire in \u003Cstrong>5 minutes\u003C\u002Fstrong>.\u003C\u002Fp>'",[382,1726,667],{"class":396},[382,1728,1729],{"class":384,"line":722},[382,1730,1731],{"class":396},"  })\n",[382,1733,1734],{"class":384,"line":737},[382,1735,423],{"emptyLinePlaceholder":422},[382,1737,1738,1741],{"class":384,"line":743},[382,1739,1740],{"class":388},"  return",[382,1742,397],{"class":396},[382,1744,1746,1749,1752],{"class":384,"line":1745},29,[382,1747,1748],{"class":396},"    ok: ",[382,1750,1751],{"class":409},"true",[382,1753,667],{"class":396},[382,1755,1757],{"class":384,"line":1756},30,[382,1758,1759],{"class":396},"    messageId: result.messageId,\n",[382,1761,1763],{"class":384,"line":1762},31,[382,1764,1181],{"class":396},[382,1766,1768],{"class":384,"line":1767},32,[382,1769,1521],{"class":396},[16,1771,1772],{},"Do not turn this sample into an anonymous public API with unlimited frequency and freely supplied subjects and bodies. That would turn your domain into a spam relay.",[16,1774,1775],{},[166,1776],{"alt":1777,"src":1778},"A Worker creates transactional email that passes through several layers of authentication before delivery to different recipient systems","\u002Fmedia\u002Fposts\u002Fcloudflare-email-service-send-receive-guide\u002Foutbound-delivery-v1.webp",[16,1780,1781],{},[173,1782,1783],{},"SPF, DKIM, and DMARC answer who may send, whether content was modified, and what to do on failure. They do not guarantee that every message reaches the primary inbox.",[78,1785,1787],{"id":1786},"when-are-rest-api-and-authenticated-smtp-a-better-fit","When are REST API and authenticated SMTP a better fit?",[16,1789,1790],{},"Cloudflare currently offers three sending entry points:",[86,1792,1793,1806],{},[89,1794,1795],{},[92,1796,1797,1800,1803],{},[95,1798,1799],{},"Method",[95,1801,1802],{},"Best fit",[95,1804,1805],{},"Credentials and boundaries",[108,1807,1808,1819,1830],{},[92,1809,1810,1813,1816],{},[113,1811,1812],{},"Workers binding",[113,1814,1815],{},"Nuxt, Hono, or an agent already running on Workers",[113,1817,1818],{},"No API token in code; sender and recipient restrictions can be enforced by the binding",[92,1820,1821,1824,1827],{},[113,1822,1823],{},"REST API",[113,1825,1826],{},"Programs running in another cloud, CI system, or backend platform",[113,1828,1829],{},"Uses a least-privilege API token; responses distinguish delivered, queued, and permanent bounce per recipient",[92,1831,1832,1835,1838],{},[113,1833,1834],{},"Authenticated SMTP",[113,1836,1837],{},"Legacy systems already using Nodemailer, PHPMailer, JavaMail, or another SMTP client",[113,1839,1840,1843],{},[20,1841,1842],{},"smtp.mx.cloudflare.net:465",", implicit TLS only; the password is an API token with Email Sending permission",[16,1845,1846],{},"Authenticated SMTP was added as a beta feature in June 2026. It currently supports only SMTPS on port 465. There is no STARTTLS on port 587 and no anonymous relay on port 25.",[16,1848,1849],{},"If an SMTP token leaks, its holder can send using the sender domains connected to the account. Therefore:",[40,1851,1852,1855,1861,1864,1867],{},[43,1853,1854],{},"Prefer an account-owned token.",[43,1856,1857,1858,347],{},"Grant only ",[20,1859,1860],{},"Email Sending: Edit",[43,1862,1863],{},"Never put it in a repository, image, or frontend environment variable.",[43,1865,1866],{},"Give each system a separate token so it can be revoked independently.",[43,1868,1869],{},"If a Worker binding is available, do not introduce a long-lived credential merely because SMTP feels familiar.",[78,1871,1873],{"id":1872},"limits-you-need-to-know","Limits you need to know",[16,1875,1876],{},"As of July 2026, these platform limits deserve explicit application-side defenses:",[86,1878,1879,1893],{},[89,1880,1881],{},[92,1882,1883,1886,1890],{},[95,1884,1885],{},"Limit",[95,1887,1889],{"align":1888},"right","Current value",[95,1891,1892],{},"Implementation effect",[108,1894,1895,1906,1916,1927,1938,1949,1959,1970,1981],{},[92,1896,1897,1900,1903],{},[113,1898,1899],{},"Email Routing rules per domain",[113,1901,1902],{"align":1888},"200",[113,1904,1905],{},"A multi-tenant system should not create one static rule per user",[92,1907,1908,1911,1913],{},[113,1909,1910],{},"Verified destination addresses per account",[113,1912,1902],{"align":1888},[113,1914,1915],{},"Plan account-level destinations before forwarding to many mailboxes",[92,1917,1918,1921,1924],{},[113,1919,1920],{},"Inbound message size",[113,1922,1923],{"align":1888},"25 MiB",[113,1925,1926],{},"Larger messages are rejected during SMTP",[92,1928,1929,1932,1935],{},[113,1930,1931],{},"Recipients per outbound message",[113,1933,1934],{"align":1888},"50",[113,1936,1937],{},"Combined To, Cc, and Bcc",[92,1939,1940,1943,1946],{},[113,1941,1942],{},"Standard outbound message size",[113,1944,1945],{"align":1888},"5 MiB",[113,1947,1948],{},"Includes attachments and MIME encoding overhead",[92,1950,1951,1954,1956],{},[113,1952,1953],{},"Message size to a verified destination",[113,1955,1923],{"align":1888},[113,1957,1958],{},"Only for verified destination addresses",[92,1960,1961,1964,1967],{},[113,1962,1963],{},"Total custom-header size",[113,1965,1966],{"align":1888},"16 KB",[113,1968,1969],{},"Do not put business data into headers",[92,1971,1972,1975,1978],{},[113,1973,1974],{},"Sending\u002Frouting domains per Zone",[113,1976,1977],{"align":1888},"30",[113,1979,1980],{},"Root domains and subdomains both count",[92,1982,1983,1988,1991],{},[113,1984,1985,1987],{},[20,1986,1098],{}," entries in a reply",[113,1989,1990],{"align":1888},"100",[113,1992,1993,1996],{},[20,1994,1995],{},"reply()"," fails above the limit",[16,1998,1999],{},"Attachments are wrapped in Base64 and MIME. A raw 3 MiB attachment does not mean the final email is still 3 MiB, so leave room for encoding, body content, and headers.",[16,2001,2002],{},"The platform can also adjust daily sending allowances based on account reputation, delivery performance, and usage history. Do not assume that unused monthly allowance means unlimited concurrency at any given second.",[78,2004,2006],{"id":2005},"how-verification-codes-and-password-resets-should-work","How verification codes and password resets should work",[16,2008,2009],{},"This blog sends registration and password-reset messages through the following path:",[145,2011,2014],{"className":2012,"code":2013,"language":150,"meta":151},[148],"User submits email\n  → same-origin check\n  → IP and email rate limits\n  → generate a six-digit code\n  → D1 stores only salted hash, purpose, expiry, and attempt count\n  → EMAIL binding sends the message\n  → successful verification atomically marks the code as used\n",[20,2015,2013],{"__ignoreMap":151},[16,2017,2018],{},"The important rules are:",[40,2020,2021,2024,2027,2030,2033,2036,2039],{},[43,2022,2023],{},"The same email address can receive only one message every five minutes.",[43,2025,2026],{},"A verification code expires after five minutes.",[43,2028,2029],{},"A code can be used only once.",[43,2031,2032],{},"D1 never stores a plaintext code.",[43,2034,2035],{},"Verification stops after a threshold of consecutive incorrect attempts.",[43,2037,2038],{},"If sending fails, release the claimed slot so an undelivered code does not lock the user out.",[43,2040,2041],{},"Resetting a password revokes existing login sessions instead of only replacing the password hash.",[16,2043,2044,2045,2048,2049,2052],{},"The rate limit must ",[31,2046,2047],{},"atomically claim eligibility"," in the database. Do not query the previous sending time and then insert a row. Two concurrent requests can both pass that check. A unique key combined with ",[20,2050,2051],{},"INSERT ... ON CONFLICT ... WHERE"," lets the database decide which request receives the sending slot.",[16,2054,2055],{},"The email API response should not reveal whether an address is registered, or an attacker can enumerate users in bulk.",[78,2057,2059],{"id":2058},"a-more-complete-production-architecture","A more complete production architecture",[16,2061,2062],{},"Once email moves from a demo into production, separate the responsibilities:",[145,2064,2067],{"className":2065,"code":2066,"language":150,"meta":151},[148],"HTTP request \u002F business event\n  → authentication, origin check, validation, rate limit, idempotency\n  → Queue\n  → email template rendering\n  → EMAIL binding\n  → write messageId \u002F error code to D1\n  → Email Service logs and alerts\n",[20,2068,2066],{"__ignoreMap":151},[16,2070,2071],{},"The inbound path can be:",[145,2073,2076],{"className":2074,"code":2075,"language":150,"meta":151},[148],"support@yourdomain.com\n  → Email Routing\n  → Email Worker\n  → authentication and size checks\n  → MIME parsing\n  → save attachments to R2 \u002F create ticket in D1\n  → Queue for expensive processing\n  → forward to a human mailbox or send a constrained reply\n",[20,2077,2075],{"__ignoreMap":151},[184,2079,2081],{"id":2080},"what-should-d1-record","What should D1 record?",[16,2083,2084],{},"Useful fields include:",[40,2086,2087,2090,2093,2097,2100,2103,2106],{},[43,2088,2089],{},"Business event ID and idempotency key.",[43,2091,2092],{},"Template ID and template version.",[43,2094,2095,347],{},[20,2096,140],{},[43,2098,2099],{},"Recipient count, without necessarily storing every complete address.",[43,2101,2102],{},"Application-side status such as accepted or failed.",[43,2104,2105],{},"Cloudflare error code.",[43,2107,2108],{},"Creation, sending, and last-updated timestamps.",[16,2110,2111],{},"Avoid storing by default:",[40,2113,2114,2117,2120,2123,2126,2129],{},[43,2115,2116],{},"Plaintext verification codes.",[43,2118,2119],{},"Password-reset tokens.",[43,2121,2122],{},"Complete email bodies.",[43,2124,2125],{},"Attachment contents.",[43,2127,2128],{},"SMTP\u002FAPI tokens.",[43,2130,2131],{},"Raw inbound messages retained indefinitely for convenient debugging.",[16,2133,2134],{},"Logs can become a new source of private data themselves.",[184,2136,2138],{"id":2137},"do-not-treat-dropped-in-email-routing-as-an-outbound-failure","Do not treat “Dropped” in Email Routing as an outbound failure",[16,2140,2141,2142,2145,2146,2149],{},"The official documentation specifically notes that messages sent through the ",[20,2143,2144],{},"send_email"," binding can appear as ",[31,2147,2148],{},"dropped"," on the Email Routing summary page even when they were delivered successfully.",[16,2151,2152,2153,2155],{},"Inspect Email Sending Metrics and Logs for outbound results and correlate application logs with the ",[20,2154,140],{},". The Routing page describes inbound routing behavior, not the complete outbound delivery state machine.",[78,2157,2159],{"id":2158},"testing-locally","Testing locally",[16,2161,2162],{},"Wrangler supports local Email Worker events. Start it with:",[145,2164,2168],{"className":2165,"code":2166,"language":2167,"meta":151,"style":151},"language-bash shiki shiki-themes github-dark github-dark","pnpm exec wrangler dev\n","bash",[20,2169,2170],{"__ignoreMap":151},[382,2171,2172,2175,2178,2181],{"class":384,"line":385},[382,2173,2174],{"class":392},"pnpm",[382,2176,2177],{"class":514}," exec",[382,2179,2180],{"class":514}," wrangler",[382,2182,2183],{"class":514}," dev\n",[16,2185,2186],{},"Then send a raw message to the local handler:",[145,2188,2190],{"className":2165,"code":2189,"language":2167,"meta":151,"style":151},"curl -X POST \\\n  'http:\u002F\u002Flocalhost:8787\u002Fcdn-cgi\u002Fhandler\u002Femail?from=sender@example.net&to=support@example.com' \\\n  -H 'Content-Type: text\u002Fplain' \\\n  --data-binary $'From: sender@example.net\\nTo: support@example.com\\nSubject: Local test\\n\\nHello from local development.'\n",[20,2191,2192,2206,2213,2223],{"__ignoreMap":151},[382,2193,2194,2197,2200,2203],{"class":384,"line":385},[382,2195,2196],{"class":392},"curl",[382,2198,2199],{"class":409}," -X",[382,2201,2202],{"class":514}," POST",[382,2204,2205],{"class":409}," \\\n",[382,2207,2208,2211],{"class":384,"line":400},[382,2209,2210],{"class":514},"  'http:\u002F\u002Flocalhost:8787\u002Fcdn-cgi\u002Fhandler\u002Femail?from=sender@example.net&to=support@example.com'",[382,2212,2205],{"class":409},[382,2214,2215,2218,2221],{"class":384,"line":413},[382,2216,2217],{"class":409},"  -H",[382,2219,2220],{"class":514}," 'Content-Type: text\u002Fplain'",[382,2222,2205],{"class":409},[382,2224,2225,2228,2231,2234,2237,2239,2242,2245],{"class":384,"line":419},[382,2226,2227],{"class":409},"  --data-binary",[382,2229,2230],{"class":514}," $'From: sender@example.net",[382,2232,2233],{"class":409},"\\n",[382,2235,2236],{"class":514},"To: support@example.com",[382,2238,2233],{"class":409},[382,2240,2241],{"class":514},"Subject: Local test",[382,2243,2244],{"class":409},"\\n\\n",[382,2246,2247],{"class":514},"Hello from local development.'\n",[16,2249,2250],{},"The local sending binding stores text and HTML as inspectable files, which is useful for template validation. Local simulation still has separate limitations for binary attachments, so attachment flows need an additional remote test.",[16,2252,2253],{},"Cover at least:",[40,2255,2256,2259,2262,2265,2268,2271,2274,2277,2280,2283],{},[43,2257,2258],{},"Normal text and HTML.",[43,2260,2261],{},"Non-ASCII subjects and sender display names.",[43,2263,2264],{},"Duplicate requests and idempotency.",[43,2266,2267],{},"Repeated verification-code requests before the cooldown expires.",[43,2269,2270],{},"Large body and attachment boundaries.",[43,2272,2273],{},"Header newline injection.",[43,2275,2276],{},"Unverified sender domains or restricted recipients.",[43,2278,2279],{},"Bounce, suppression, allowance, and frequency errors.",[43,2281,2282],{},"Unmatched inbound addresses, Catch-all, and rejection paths.",[43,2284,2285],{},"Scripts, remote resources, and dangerous attachment names in HTML mail.",[78,2287,2289],{"id":2288},"what-is-it-suitable-forand-what-is-it-not","What is it suitable for—and what is it not?",[16,2291,2292],{},"Good fits:",[40,2294,2295,2298,2301,2304,2307,2310],{},[43,2296,2297],{},"Registration verification, magic links, and password resets.",[43,2299,2300],{},"Order, billing, deployment, and monitoring notifications.",[43,2302,2303],{},"Personal-domain email forwarding.",[43,2305,2306],{},"Contact forms and support tickets.",[43,2308,2309],{},"Email-triggered agents or automation.",[43,2311,2312],{},"Cloudflare applications that want to avoid maintaining an SMTP server.",[16,2314,2315],{},"Poor direct fits:",[40,2317,2318,2321,2324,2327,2330],{},[43,2319,2320],{},"Marketing blasts without consent.",[43,2322,2323],{},"Newsletters without unsubscribe, complaint, and suppression management.",[43,2325,2326],{},"An anonymous public “send email for me” endpoint.",[43,2328,2329],{},"Enterprise mail that needs IMAP folders, calendars, contacts, and shared-mailbox collaboration.",[43,2331,2332],{},"Passing arbitrary incoming message bodies directly to an agent with high-privilege tools.",[16,2334,2335],{},"Cloudflare maintains the email entry and delivery infrastructure, but sender reputation, user consent, content safety, frequency controls, and data minimization remain the application’s responsibility.",[78,2337,2339],{"id":2338},"final-assessment","Final assessment",[16,2341,2342],{},"The most valuable part of Cloudflare Email Service is not that it is “another SMTP provider.” It puts email inside the same runtime boundary as Workers, D1, R2, Queues, and Agents.",[16,2344,2345],{},"Incoming mail no longer has to be forwarded only to a human mailbox; it can become a constrained event entry point. Outbound mail no longer requires a separate third-party SDK and credential system just to send a verification code.",[16,2347,2348],{},"If the project already runs on Workers, begin with this minimal path:",[337,2350,2351,2358,2361,2364,2367],{},[43,2352,2353,2354,2357],{},"Receive ",[20,2355,2356],{},"support@"," through Email Routing.",[43,2359,2360],{},"Send verification and password-reset messages through a Worker binding.",[43,2362,2363],{},"Use D1 for cooldowns, one-time verification, and minimal logs.",[43,2365,2366],{},"Restrict binding permissions with a sender allowlist.",[43,2368,2369],{},"Verify delivery in Email Sending logs instead of trusting only a successful API response.",[16,2371,2372],{},"Introduce the REST API only when an external platform truly needs to send mail. Use authenticated SMTP only when an existing system already depends deeply on an SMTP client.",[78,2374,1098],{"id":2375},"references",[40,2377,2378,2387,2394,2401,2408,2415,2422,2429,2435,2441,2448,2455,2462],{},[43,2379,2380],{},[2381,2382,2386],"a",{"href":2383,"rel":2384},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002F",[2385],"nofollow","Cloudflare Email Service overview",[43,2388,2389],{},[2381,2390,2393],{"href":2391,"rel":2392},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fget-started\u002Fsend-emails\u002F",[2385],"Set up Email Sending",[43,2395,2396],{},[2381,2397,2400],{"href":2398,"rel":2399},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fget-started\u002Froute-emails\u002F",[2385],"Set up Email Routing",[43,2402,2403],{},[2381,2404,2407],{"href":2405,"rel":2406},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconfiguration\u002Fdomains\u002F",[2385],"Domain and DNS configuration",[43,2409,2410],{},[2381,2411,2414],{"href":2412,"rel":2413},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconfiguration\u002Fsend-bindings\u002F",[2385],"Sending binding permissions",[43,2416,2417],{},[2381,2418,2421],{"href":2419,"rel":2420},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Fworkers-api\u002F",[2385],"Workers sending API",[43,2423,2424],{},[2381,2425,2428],{"href":2426,"rel":2427},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Froute-emails\u002Femail-handler\u002F",[2385],"Email Worker receiving API",[43,2430,2431],{},[2381,2432,1823],{"href":2433,"rel":2434},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Frest-api\u002F",[2385],[43,2436,2437],{},[2381,2438,1834],{"href":2439,"rel":2440},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fapi\u002Fsend-emails\u002Fsmtp\u002F",[2385],[43,2442,2443],{},[2381,2444,2447],{"href":2445,"rel":2446},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fplatform\u002Flimits\u002F",[2385],"Platform limits",[43,2449,2450],{},[2381,2451,2454],{"href":2452,"rel":2453},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fplatform\u002Fpricing\u002F",[2385],"Pricing",[43,2456,2457],{},[2381,2458,2461],{"href":2459,"rel":2460},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Fconcepts\u002Femail-authentication\u002F",[2385],"Email authentication and delivery",[43,2463,2464],{},[2381,2465,2468],{"href":2466,"rel":2467},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Femail-service\u002Flocal-development\u002Frouting\u002F",[2385],"Local Email Worker testing",[2470,2471,2472],"style",{},"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 .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":151,"searchDepth":413,"depth":413,"links":2474},[2475,2476,2480,2485,2491,2495,2496,2497,2498,2502,2503,2504,2505],{"id":80,"depth":400,"text":81},{"id":178,"depth":400,"text":179,"children":2477},[2478,2479],{"id":186,"depth":413,"text":187},{"id":218,"depth":413,"text":219},{"id":245,"depth":400,"text":246,"children":2481},[2482,2483],{"id":252,"depth":413,"text":253},{"id":273,"depth":413,"text":2484},"Sending uses cf-bounce records",{"id":331,"depth":400,"text":332,"children":2486},[2487,2489,2490],{"id":359,"depth":413,"text":2488},"email() is the inbound entry point",{"id":883,"depth":413,"text":884},{"id":1048,"depth":413,"text":1049},{"id":1105,"depth":400,"text":1106,"children":2492},[2493,2494],{"id":1222,"depth":413,"text":1223},{"id":1442,"depth":413,"text":1443},{"id":1786,"depth":400,"text":1787},{"id":1872,"depth":400,"text":1873},{"id":2005,"depth":400,"text":2006},{"id":2058,"depth":400,"text":2059,"children":2499},[2500,2501],{"id":2080,"depth":413,"text":2081},{"id":2137,"depth":413,"text":2138},{"id":2158,"depth":400,"text":2159},{"id":2288,"depth":400,"text":2289},{"id":2338,"depth":400,"text":2339},{"id":2375,"depth":400,"text":1098},"A practical July 2026 guide to receiving, forwarding, parsing, replying to, and sending transactional email with Cloudflare Email Service, including Nuxt, Workers, D1, security, and rate limiting.",false,null,"md",{"src":2511,"alt":2512,"caption":2513,"width":2514,"height":2515},"\u002Fmedia\u002Fposts\u002Fcloudflare-email-service-send-receive-guide\u002Fcover-v1.webp","Email being received, authenticated, and sent by Worker nodes across a global edge network","Cloudflare Email Service turns email from a server you maintain into an event your code can handle",1600,900,{},"\u002Fblog\u002Fcloudflare-email-service-send-receive-guide","2026-07-28",{"title":10,"description":2506},"blog\u002Fcloudflare-email-service-send-receive-guide",[2522,2523,2524,2525],"Cloudflare","Email","Workers","Serverless",{"src":2527,"poster":2511,"title":2528,"description":2529,"duration":2530,"width":2531,"height":2532},"\u002Fmedia\u002Fvideos\u002Fcloudflare-email-service-explained\u002Fcloudflare-email-service-explained-v1.mp4","Cloudflare Email Service in Practice—an 11-minute Configuration Guide","From DNS, Email Routing, Email Workers, and the send_email binding to REST, SMTP, Nuxt, D1, five-minute verification codes, and production checks.",661,1920,1080,"h991yui4fjahs1D60SDwiPiDJL2VcLvH982rhbGT8p4",{"id":2535,"title":2536,"accent":2537,"body":2538,"description":4245,"draft":2507,"experience":2508,"extension":2509,"featured":422,"image":4246,"meta":4250,"navigation":422,"path":4251,"publishedAt":4252,"readingTime":658,"seo":4253,"stem":4254,"tags":4255,"updatedAt":2508,"video":2508,"__hash__":4259,"contentLocale":5,"isFallback":2507},"posts_en\u002Fblog\u002Fvue-3-6-rc-upgrade-guide.md","The Complete Vue 3.6 RC Upgrade Guide—from 3.5 to Vapor Mode and alien-signals","mint",{"type":13,"value":2539,"toc":4206},[2540,2546,2560,2563,2572,2576,2715,2718,2723,2727,2730,2765,2768,2774,2778,2785,2788,2901,2904,2910,2915,2919,2922,2935,2941,2944,2948,2965,2968,2972,2988,3005,3008,3012,3015,3018,3026,3029,3033,3036,3161,3164,3192,3220,3223,3227,3230,3280,3283,3287,3294,3355,3358,3361,3367,3373,3376,3380,3383,3397,3406,3453,3460,3463,3476,3479,3483,3486,3623,3631,3637,3640,3727,3730,3764,3768,3781,3784,3788,3791,3794,3797,3801,3804,3808,3811,3814,3818,3822,3825,3840,3847,3864,3867,3900,3903,3907,3910,3930,3933,3937,3940,3963,3966,3970,3973,3976,3996,4000,4004,4007,4011,4014,4018,4021,4025,4028,4032,4115,4117,4120,4128,4131,4138,4141,4144,4148,4198,4203],[16,2541,2542,2543],{},"If you look only at the version number, Vue 3.6 appears to be an ordinary minor release. After reading the release notes in full, however, my impression is different: ",[31,2544,2545],{},"it feels more like Vue is replacing its engine for the next stage.",[16,2547,2548,2549,452,2552,2555,2556,2559],{},"On one side is a reactivity core rebuilt on alien-signals while the public ",[20,2550,2551],{},"ref",[20,2553,2554],{},"computed",", and ",[20,2557,2558],{},"watch"," APIs remain largely unchanged. On the other is Vapor Mode entering the RC stage, allowing Vue single-file components to bypass the Virtual DOM and compile into more direct DOM updates.",[16,2561,2562],{},"First, the most important version detail:",[72,2564,2565],{},[16,2566,2567,2568,2571],{},"As of July 26, 2026, the latest Vue 3.6 release is ",[31,2569,2570],{},"3.6.0-rc.2",". RC means release candidate, not stable. This article is intended for evaluation, experimentation, and migration planning; it does not mean every production project should upgrade immediately.",[78,2573,2575],{"id":2574},"the-short-version-what-changed-in-36","The short version: what changed in 3.6?",[86,2577,2578,2594],{},[89,2579,2580],{},[92,2581,2582,2585,2588,2591],{},[95,2583,2584],{},"Dimension",[95,2586,2587],{},"Vue 3.5",[95,2589,2590],{},"Vue 3.6 RC",[95,2592,2593],{},"Practical effect on existing projects",[108,2595,2596,2610,2624,2638,2652,2666,2683,2701],{},[92,2597,2598,2601,2604,2607],{},[113,2599,2600],{},"Reactivity system",[113,2602,2603],{},"Internally optimized in 3.5",[113,2605,2606],{},"Substantially rebuilt on alien-signals",[113,2608,2609],{},"Ordinary Composition API code usually needs no rewrite, but complex effects and scheduling deserve regression tests",[92,2611,2612,2615,2618,2621],{},[113,2613,2614],{},"Default rendering",[113,2616,2617],{},"Virtual DOM",[113,2619,2620],{},"Still Virtual DOM",[113,2622,2623],{},"Upgrading dependencies alone does not move an application into Vapor",[92,2625,2626,2629,2632,2635],{},[113,2627,2628],{},"Vapor Mode",[113,2630,2631],{},"Not delivered as a production capability in 3.5",[113,2633,2634],{},"Feature set is in RC and 100% opt-in",[113,2636,2637],{},"Teams can experiment with one performance-sensitive area",[92,2639,2640,2643,2646,2649],{},[113,2641,2642],{},"Application size",[113,2644,2645],{},"Includes the VDOM runtime by default",[113,2647,2648],{},"A pure Vapor application can omit the VDOM runtime",[113,2650,2651],{},"Size savings are easiest to realize in pure Vapor applications or clearly separated regions",[92,2653,2654,2657,2660,2663],{},[113,2655,2656],{},"API coverage",[113,2658,2659],{},"Full Vue 3 API ecosystem",[113,2661,2662],{},"Vapor supports a subset of Vue APIs",[113,2664,2665],{},"Options API and instance-proxy features cannot be migrated directly",[92,2667,2668,2671,2674,2680],{},[113,2669,2670],{},"VDOM \u002F Vapor mixing",[113,2672,2673],{},"Not applicable",[113,2675,2676,2677],{},"Interoperability through ",[20,2678,2679],{},"vaporInteropPlugin",[113,2681,2682],{},"Standard props, events, and slots are covered, but complex nesting still needs testing",[92,2684,2685,2688,2691,2698],{},[113,2686,2687],{},"Event delegation",[113,2689,2690],{},"Native listener semantics",[113,2692,2693,2694,2697],{},"Direct binding by default since rc.2; ",[20,2695,2696],{},".delegate"," opts in",[113,2699,2700],{},"Do not carry the rc.1 assumption of automatic delegation forward",[92,2702,2703,2706,2709,2712],{},[113,2704,2705],{},"Production readiness",[113,2707,2708],{},"Stable",[113,2710,2711],{},"Still a prerelease",[113,2713,2714],{},"Production projects should wait for stable Vue and explicit ecosystem support",[16,2716,2717],{},"In one sentence:",[16,2719,2720],{},[31,2721,2722],{},"Vue 3.5 focused more on developer experience and SSR capabilities, while Vue 3.6 focuses on the reactivity core and rendering architecture.",[78,2724,2726],{"id":2725},"from-35-to-36-the-center-of-gravity-has-moved","From 3.5 to 3.6: the center of gravity has moved",[16,2728,2729],{},"Vue 3.5 introduced no breaking changes, and most of its improvements were immediately visible in application code:",[40,2731,2732,2735,2741,2747,2753,2756,2762],{},[43,2733,2734],{},"Reactive Props Destructure became stable.",[43,2736,2737,2740],{},[20,2738,2739],{},"useTemplateRef()"," improved how template refs are organized.",[43,2742,2743,2746],{},[20,2744,2745],{},"onWatcherCleanup()"," made watcher cleanup more natural.",[43,2748,2749,2752],{},[20,2750,2751],{},"useId()"," provided stable, SSR-friendly IDs.",[43,2754,2755],{},"Async components gained Lazy Hydration.",[43,2757,2758,2761],{},[20,2759,2760],{},"data-allow-mismatch"," allowed declared hydration differences.",[43,2763,2764],{},"Deferred Teleport and custom elements were improved.",[16,2766,2767],{},"Vue 3.6 contains many fixes too, but its two most important changes—alien-signals and Vapor—sit beneath application code. You may not write a new API in your business logic, but you will encounter different performance characteristics, compiler output, and runtime boundaries.",[16,2769,2770,2771],{},"That changes the upgrade strategy: ",[31,2772,2773],{},"do not stop at “Does it compile?” Test reactive behavior, SSR hydration, component interoperability, and real performance.",[78,2775,2777],{"id":2776},"change-one-the-reactivity-core-moves-to-alien-signals","Change one: the reactivity core moves to alien-signals",[16,2779,2780,2781,2784],{},"Vue 3.6 substantially rewrites ",[20,2782,2783],{},"@vue\u002Freactivity"," on a foundation derived from StackBlitz’s open-source alien-signals.",[16,2786,2787],{},"For most application developers, this is still familiar Vue:",[145,2789,2791],{"className":376,"code":2790,"language":378,"meta":151,"style":151},"import { computed, ref, watchEffect } from 'vue'\n\nconst price = ref(99)\nconst count = ref(2)\nconst total = computed(() => price.value * count.value)\n\nwatchEffect(() => {\n  console.log(total.value)\n})\n",[20,2792,2793,2805,2809,2828,2846,2872,2876,2887,2897],{"__ignoreMap":151},[382,2794,2795,2797,2800,2802],{"class":384,"line":385},[382,2796,900],{"class":388},[382,2798,2799],{"class":396}," { computed, ref, watchEffect } ",[382,2801,906],{"class":388},[382,2803,2804],{"class":514}," 'vue'\n",[382,2806,2807],{"class":384,"line":400},[382,2808,423],{"emptyLinePlaceholder":422},[382,2810,2811,2813,2816,2818,2821,2823,2826],{"class":384,"line":413},[382,2812,1472],{"class":388},[382,2814,2815],{"class":409}," price",[382,2817,484],{"class":388},[382,2819,2820],{"class":392}," ref",[382,2822,446],{"class":396},[382,2824,2825],{"class":409},"99",[382,2827,574],{"class":396},[382,2829,2830,2832,2835,2837,2839,2841,2844],{"class":384,"line":419},[382,2831,1472],{"class":388},[382,2833,2834],{"class":409}," count",[382,2836,484],{"class":388},[382,2838,2820],{"class":392},[382,2840,446],{"class":396},[382,2842,2843],{"class":409},"2",[382,2845,574],{"class":396},[382,2847,2848,2850,2853,2855,2858,2861,2863,2866,2869],{"class":384,"line":426},[382,2849,1472],{"class":388},[382,2851,2852],{"class":409}," total",[382,2854,484],{"class":388},[382,2856,2857],{"class":392}," computed",[382,2859,2860],{"class":396},"(() ",[382,2862,1550],{"class":388},[382,2864,2865],{"class":396}," price.value ",[382,2867,2868],{"class":388},"*",[382,2870,2871],{"class":396}," count.value)\n",[382,2873,2874],{"class":384,"line":437},[382,2875,423],{"emptyLinePlaceholder":422},[382,2877,2878,2881,2883,2885],{"class":384,"line":475},[382,2879,2880],{"class":392},"watchEffect",[382,2882,2860],{"class":396},[382,2884,1550],{"class":388},[382,2886,397],{"class":396},[382,2888,2889,2892,2894],{"class":384,"line":496},[382,2890,2891],{"class":396},"  console.",[382,2893,616],{"class":392},[382,2895,2896],{"class":396},"(total.value)\n",[382,2898,2899],{"class":384,"line":527},[382,2900,1521],{"class":396},[16,2902,2903],{},"The changes are internal: how dependencies are established, how state is marked, when computed values become stale, how effects propagate through the dependency graph, and how those relationships are cleaned up and reused.",[16,2905,2906],{},[166,2907],{"alt":2908,"src":2909},"A fine-grained reactive dependency network made of active and dormant paths","\u002Fmedia\u002Fposts\u002Fvue-3-6-rc-upgrade-guide\u002Freactivity-core-v1.webp",[16,2911,2912],{},[173,2913,2914],{},"This illustration expresses the concept of dependency propagation; it is not a field-by-field diagram of Vue’s internal data structures.",[184,2916,2918],{"id":2917},"how-to-interpret-the-performance-numbers","How to interpret the performance numbers",[16,2920,2921],{},"The alien-signals merge PR in Vue’s core repository reported two kinds of microbenchmark:",[40,2923,2924,2932],{},[43,2925,2926,2927,452,2929,2931],{},"In a scenario constructing many ",[20,2928,2551],{},[20,2930,2554],{},", and effect instances, sample memory fell from roughly 2.3 MB to 2.0 MB, a reduction of about 13%.",[43,2933,2934],{},"In a particular stress case where many computed values were read after one source changed, performance could improve by more than 30 times.",[16,2936,2937,2938],{},"The second number is striking, but it does not mean “a Vue 3.6 application is 30 times faster.” It demonstrates that ",[31,2939,2940],{},"the new dependency-propagation algorithm has better complexity characteristics for some graph structures whose cost previously grew sharply with scale.",[16,2942,2943],{},"A real page also contains DOM work, layout, networking, component libraries, serialization, and business computation. Evaluate an upgrade using your own flame charts, interaction latency, memory curves, and bundle output—not a framework microbenchmark transplanted into an application promise.",[184,2945,2947],{"id":2946},"which-projects-are-most-likely-to-notice-a-benefit","Which projects are most likely to notice a benefit?",[40,2949,2950,2953,2956,2959,2962],{},[43,2951,2952],{},"Data dashboards with many interdependent computed values.",[43,2954,2955],{},"Editors with frequent mutations, filtering, and derived state.",[43,2957,2958],{},"Long-running workbenches that create and destroy many effects.",[43,2960,2961],{},"Applications with many form fields and complex validation dependencies.",[43,2963,2964],{},"State-management or utility libraries built on Vue’s reactive primitives.",[16,2966,2967],{},"Ordinary content pages receive the internal improvements too, but users may not perceive a dramatic difference.",[184,2969,2971],{"id":2970},"who-should-be-especially-cautious","Who should be especially cautious?",[16,2973,2974,2975,452,2977,452,2980,452,2982,2555,2984,2987],{},"Migration risk is relatively contained when code uses only the public ",[20,2976,2551],{},[20,2978,2979],{},"reactive",[20,2981,2554],{},[20,2983,2558],{},[20,2985,2986],{},"effectScope"," APIs. The real warning signs are:",[40,2989,2990,2993,2996,2999,3002],{},[43,2991,2992],{},"Reading or mutating Vue’s private reactive fields directly.",[43,2994,2995],{},"Depending on unpublished effect, Dep, or scheduler implementation details.",[43,2997,2998],{},"Monkey-patching reactive functions.",[43,3000,3001],{},"Relying on implicit watcher order, synchronous flush behavior, or cleanup timing.",[43,3003,3004],{},"Supporting several Vue minor versions in a library without boundary tests.",[16,3006,3007],{},"The purpose of an internal rewrite is precisely to let Vue change implementation while preserving its public API. Conversely, code tied to internals is the code most likely to break during this kind of upgrade.",[78,3009,3011],{"id":3010},"change-two-vapor-mode-is-no-longer-only-an-experimental-idea","Change two: Vapor Mode is no longer only an experimental idea",[16,3013,3014],{},"Vapor Mode is a new compilation mode for Vue SFCs. A conventional Vue template generates VNodes; the Virtual DOM runtime compares the previous and next trees and applies their differences to the real DOM. Vapor lets the compiler identify dynamic regions ahead of time and generate more direct, fine-grained update logic.",[16,3016,3017],{},"It has two goals:",[337,3019,3020,3023],{},[43,3021,3022],{},"Reduce the application’s baseline bundle size.",[43,3024,3025],{},"Reduce the runtime cost of creating, traversing, and comparing VNodes.",[16,3027,3028],{},"The official release notes say Vapor reaches a level comparable with Solid and Svelte 5 in the third-party js-framework-benchmark. That statement should also be read as a benchmark result, not as a single ranking that represents every real application and framework.",[184,3030,3032],{"id":3031},"vapor-does-not-turn-on-automatically-after-an-upgrade","Vapor does not turn on automatically after an upgrade",[16,3034,3035],{},"Vue 3.6 still uses the VDOM by default. An SFC must opt into Vapor explicitly:",[145,3037,3041],{"className":3038,"code":3039,"language":3040,"meta":151,"style":151},"language-vue shiki shiki-themes github-dark github-dark","\u003Cscript setup vapor lang=\"ts\">\nimport { ref } from 'vue'\n\nconst count = ref(0)\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cbutton @click=\"count++\">\n    Count: {{ count }}\n  \u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n","vue",[20,3042,3043,3067,3078,3082,3099,3108,3112,3121,3139,3144,3153],{"__ignoreMap":151},[382,3044,3045,3047,3051,3054,3057,3060,3062,3065],{"class":384,"line":385},[382,3046,466],{"class":396},[382,3048,3050],{"class":3049},"sxg3X","script",[382,3052,3053],{"class":392}," setup",[382,3055,3056],{"class":392}," vapor",[382,3058,3059],{"class":392}," lang",[382,3061,1568],{"class":396},[382,3063,3064],{"class":514},"\"ts\"",[382,3066,760],{"class":396},[382,3068,3069,3071,3074,3076],{"class":384,"line":400},[382,3070,900],{"class":388},[382,3072,3073],{"class":396}," { ref } ",[382,3075,906],{"class":388},[382,3077,2804],{"class":514},[382,3079,3080],{"class":384,"line":413},[382,3081,423],{"emptyLinePlaceholder":422},[382,3083,3084,3086,3088,3090,3092,3094,3097],{"class":384,"line":419},[382,3085,1472],{"class":388},[382,3087,2834],{"class":409},[382,3089,484],{"class":388},[382,3091,2820],{"class":392},[382,3093,446],{"class":396},[382,3095,3096],{"class":409},"0",[382,3098,574],{"class":396},[382,3100,3101,3104,3106],{"class":384,"line":426},[382,3102,3103],{"class":396},"\u003C\u002F",[382,3105,3050],{"class":3049},[382,3107,760],{"class":396},[382,3109,3110],{"class":384,"line":437},[382,3111,423],{"emptyLinePlaceholder":422},[382,3113,3114,3116,3119],{"class":384,"line":475},[382,3115,466],{"class":396},[382,3117,3118],{"class":3049},"template",[382,3120,760],{"class":396},[382,3122,3123,3126,3129,3132,3134,3137],{"class":384,"line":496},[382,3124,3125],{"class":396},"  \u003C",[382,3127,3128],{"class":3049},"button",[382,3130,3131],{"class":392}," @click",[382,3133,1568],{"class":396},[382,3135,3136],{"class":514},"\"count++\"",[382,3138,760],{"class":396},[382,3140,3141],{"class":384,"line":527},[382,3142,3143],{"class":396},"    Count: {{ count }}\n",[382,3145,3146,3149,3151],{"class":384,"line":532},[382,3147,3148],{"class":396},"  \u003C\u002F",[382,3150,3128],{"class":3049},[382,3152,760],{"class":396},[382,3154,3155,3157,3159],{"class":384,"line":560},[382,3156,3103],{"class":396},[382,3158,3118],{"class":3049},[382,3160,760],{"class":396},[16,3162,3163],{},"Two equivalent entry points are also supported:",[145,3165,3167],{"className":3038,"code":3166,"language":3040,"meta":151,"style":151},"\u003Cscript vapor>\n\u002F\u002F Shorthand for \u003Cscript setup vapor>\n\u003C\u002Fscript>\n",[20,3168,3169,3179,3184],{"__ignoreMap":151},[382,3170,3171,3173,3175,3177],{"class":384,"line":385},[382,3172,466],{"class":396},[382,3174,3050],{"class":3049},[382,3176,3056],{"class":392},[382,3178,760],{"class":396},[382,3180,3181],{"class":384,"line":400},[382,3182,3183],{"class":1632},"\u002F\u002F Shorthand for \u003Cscript setup vapor>\n",[382,3185,3186,3188,3190],{"class":384,"line":413},[382,3187,3103],{"class":396},[382,3189,3050],{"class":3049},[382,3191,760],{"class":396},[145,3193,3195],{"className":3038,"code":3194,"language":3040,"meta":151,"style":151},"\u003Ctemplate vapor>\n  \u003C!-- Compile the entire SFC with Vapor -->\n\u003C\u002Ftemplate>\n",[20,3196,3197,3207,3212],{"__ignoreMap":151},[382,3198,3199,3201,3203,3205],{"class":384,"line":385},[382,3200,466],{"class":396},[382,3202,3118],{"class":3049},[382,3204,3056],{"class":392},[382,3206,760],{"class":396},[382,3208,3209],{"class":384,"line":400},[382,3210,3211],{"class":1632},"  \u003C!-- Compile the entire SFC with Vapor -->\n",[382,3213,3214,3216,3218],{"class":384,"line":413},[382,3215,3103],{"class":396},[382,3217,3118],{"class":3049},[382,3219,760],{"class":396},[16,3221,3222],{},"This means a team can upgrade to 3.6, keep its existing VDOM application, and selectively migrate a component or page later.",[184,3224,3226],{"id":3225},"pure-vapor-applications","Pure Vapor applications",[16,3228,3229],{},"If an application consists entirely of Vapor components, it can use:",[145,3231,3233],{"className":376,"code":3232,"language":378,"meta":151,"style":151},"import { createVaporApp } from 'vue'\nimport App from '.\u002FApp.vue'\n\ncreateVaporApp(App).mount('#app')\n",[20,3234,3235,3246,3258,3262],{"__ignoreMap":151},[382,3236,3237,3239,3242,3244],{"class":384,"line":385},[382,3238,900],{"class":388},[382,3240,3241],{"class":396}," { createVaporApp } ",[382,3243,906],{"class":388},[382,3245,2804],{"class":514},[382,3247,3248,3250,3253,3255],{"class":384,"line":400},[382,3249,900],{"class":388},[382,3251,3252],{"class":396}," App ",[382,3254,906],{"class":388},[382,3256,3257],{"class":514}," '.\u002FApp.vue'\n",[382,3259,3260],{"class":384,"line":413},[382,3261,423],{"emptyLinePlaceholder":422},[382,3263,3264,3267,3270,3273,3275,3278],{"class":384,"line":419},[382,3265,3266],{"class":392},"createVaporApp",[382,3268,3269],{"class":396},"(App).",[382,3271,3272],{"class":392},"mount",[382,3274,446],{"class":396},[382,3276,3277],{"class":514},"'#app'",[382,3279,574],{"class":396},[16,3281,3282],{},"This mode can omit the Virtual DOM runtime and is therefore the clearest route to a smaller baseline bundle. It is best suited to small new applications, interactive tools, or independent pages with a tightly controlled technology stack.",[184,3284,3286],{"id":3285},"using-vapor-inside-an-existing-vdom-application","Using Vapor inside an existing VDOM application",[16,3288,3289,3290,3293],{},"An existing ",[20,3291,3292],{},"createApp()"," project needs the interoperability plugin:",[145,3295,3297],{"className":376,"code":3296,"language":378,"meta":151,"style":151},"import { createApp, vaporInteropPlugin } from 'vue'\nimport App from '.\u002FApp.vue'\n\ncreateApp(App)\n  .use(vaporInteropPlugin)\n  .mount('#app')\n",[20,3298,3299,3310,3320,3324,3332,3343],{"__ignoreMap":151},[382,3300,3301,3303,3306,3308],{"class":384,"line":385},[382,3302,900],{"class":388},[382,3304,3305],{"class":396}," { createApp, vaporInteropPlugin } ",[382,3307,906],{"class":388},[382,3309,2804],{"class":514},[382,3311,3312,3314,3316,3318],{"class":384,"line":400},[382,3313,900],{"class":388},[382,3315,3252],{"class":396},[382,3317,906],{"class":388},[382,3319,3257],{"class":514},[382,3321,3322],{"class":384,"line":413},[382,3323,423],{"emptyLinePlaceholder":422},[382,3325,3326,3329],{"class":384,"line":419},[382,3327,3328],{"class":392},"createApp",[382,3330,3331],{"class":396},"(App)\n",[382,3333,3334,3337,3340],{"class":384,"line":426},[382,3335,3336],{"class":396},"  .",[382,3338,3339],{"class":392},"use",[382,3341,3342],{"class":396},"(vaporInteropPlugin)\n",[382,3344,3345,3347,3349,3351,3353],{"class":384,"line":437},[382,3346,3336],{"class":396},[382,3348,3272],{"class":392},[382,3350,446],{"class":396},[382,3352,3277],{"class":514},[382,3354,574],{"class":396},[16,3356,3357],{},"A Vapor application can also install the plugin to host VDOM components, but doing so brings the VDOM runtime back and weakens the bundle-size advantage.",[16,3359,3360],{},"Components written with Render Functions or JSX remain VDOM components. They also require interoperability inside a pure Vapor application.",[16,3362,3363],{},[166,3364],{"alt":3365,"src":3366},"An existing VDOM region connected to an independent Vapor region through an explicit interoperability boundary","\u002Fmedia\u002Fposts\u002Fvue-3-6-rc-upgrade-guide\u002Fvapor-boundaries-v1.webp",[16,3368,3369,3370],{},"My recommendation matches the official guidance: ",[31,3371,3372],{},"organize the two rendering modes into clearly bounded regions instead of nesting them back and forth at every level.",[16,3374,3375],{},"For example, a high-frequency data canvas could become a Vapor page or subtree while the surrounding admin navigation, dialogs, and mature component library remain on the VDOM. The gain, failure radius, and rollback path are all easier to measure that way.",[78,3377,3379],{"id":3378},"the-easiest-rc2-trap-event-delegation-became-opt-in","The easiest rc.2 trap: event delegation became opt-in",[16,3381,3382],{},"This section is especially important if you already tried Vapor during rc.1.",[16,3384,3385,3386,3389,3390,3393,3394,3396],{},"In rc.1, eligible events were automatically delegated to ",[20,3387,3388],{},"document",". That behavior differs from native DOM and standard Vue semantics: if an ancestor calls ",[20,3391,3392],{},"stopPropagation()"," first, the event never reaches ",[20,3395,3388],{},", so the child’s delegated handler never runs.",[16,3398,3399,3400,3402,3403,3405],{},"Starting with ",[31,3401,2570],{},", Vapor binds DOM listeners directly to elements by default. Add the Vapor-specific ",[20,3404,2696],{}," modifier only when you explicitly want document-level delegation:",[145,3407,3409],{"className":3038,"code":3408,"language":3040,"meta":151,"style":151},"\u003Cbutton @click.delegate=\"onClick\">\n  Save\n\u003C\u002Fbutton>\n",[20,3410,3411,3440,3445],{"__ignoreMap":151},[382,3412,3413,3415,3417,3420,3423,3425,3428,3430,3433,3436,3438],{"class":384,"line":385},[382,3414,466],{"class":396},[382,3416,3128],{"class":3049},[382,3418,3419],{"class":396}," @",[382,3421,3422],{"class":392},"click",[382,3424,347],{"class":396},[382,3426,3427],{"class":392},"delegate",[382,3429,1568],{"class":396},[382,3431,3432],{"class":514},"\"",[382,3434,3435],{"class":396},"onClick",[382,3437,3432],{"class":514},[382,3439,760],{"class":396},[382,3441,3442],{"class":384,"line":400},[382,3443,3444],{"class":396},"  Save\n",[382,3446,3447,3449,3451],{"class":384,"line":413},[382,3448,3103],{"class":396},[382,3450,3128],{"class":3049},[382,3452,760],{"class":396},[16,3454,3455,3456,3459],{},"The old ",[20,3457,3458],{},"compilerOptions.eventDelegation"," option has also been removed.",[16,3461,3462],{},"Migration requires two actions:",[337,3464,3465,3470],{},[43,3466,3467,3468,347],{},"Remove any existing ",[20,3469,3458],{},[43,3471,3472,3473,3475],{},"Use ",[20,3474,2696],{}," only for large numbers of stable listeners whose propagation path has been verified.",[16,3477,3478],{},"This is not an optimization for a global mechanical replacement. Whether delegation is worthwhile depends on node count, lifecycle, and propagation semantics.",[78,3480,3482],{"id":3481},"compatibility-boundaries-between-vapor-and-standard-vue","Compatibility boundaries between Vapor and standard Vue",[16,3484,3485],{},"Vapor is not intended to cover every historical Vue capability on day one. The official documentation says it supports a subset of the current Vue API. Features that depend on VNodes or public component-instance proxies cannot currently be used directly.",[86,3487,3488,3500],{},[89,3489,3490],{},[92,3491,3492,3494,3497],{},[95,3493,779],{},[95,3495,3496],{},"VDOM component",[95,3498,3499],{},"Vapor component",[108,3501,3502,3513,3523,3534,3550,3561,3573,3585,3612],{},[92,3503,3504,3507,3510],{},[113,3505,3506],{},"Composition API",[113,3508,3509],{},"Supported",[113,3511,3512],{},"Primary programming model",[92,3514,3515,3518,3520],{},[113,3516,3517],{},"Options API",[113,3519,3509],{},[113,3521,3522],{},"Not supported",[92,3524,3525,3530,3532],{},[113,3526,3527],{},[20,3528,3529],{},"app.config.globalProperties",[113,3531,3509],{},[113,3533,3522],{},[92,3535,3536,3541,3544],{},[113,3537,3538],{},[20,3539,3540],{},"getCurrentInstance()",[113,3542,3543],{},"Returns the current instance",[113,3545,3546,3547],{},"Returns ",[20,3548,3549],{},"null",[92,3551,3552,3555,3558],{},[113,3553,3554],{},"Render Function \u002F JSX",[113,3556,3557],{},"Native VDOM",[113,3559,3560],{},"Still VDOM; mixing requires interop",[92,3562,3563,3568,3570],{},[113,3564,3565],{},[20,3566,3567],{},"v-memo",[113,3569,3509],{},[113,3571,3572],{},"Not applicable \u002F unsupported",[92,3574,3575,3581,3583],{},[113,3576,3577,3580],{},[20,3578,3579],{},"@vue:xxx"," element lifecycle events",[113,3582,3509],{},[113,3584,3522],{},[92,3586,3587,3590,3593],{},[113,3588,3589],{},"Component template refs",[113,3591,3592],{},"Expose public instance capabilities",[113,3594,3595,3596,452,3599,452,3602,452,3605,452,3608,3611],{},"Do not expose ",[20,3597,3598],{},"$el",[20,3600,3601],{},"$props",[20,3603,3604],{},"$attrs",[20,3606,3607],{},"$slots",[20,3609,3610],{},"$refs",", and similar properties",[92,3613,3614,3617,3620],{},[113,3615,3616],{},"Custom directives",[113,3618,3619],{},"Standard directive lifecycle object",[113,3621,3622],{},"Vapor-specific function interface",[184,3624,3626,3627,3630],{"id":3625},"do-not-use-slotsdefault-as-a-side-effect-free-probe","Do not use ",[20,3628,3629],{},"slots.default()"," as a side-effect-free probe",[16,3632,3633,3634,3636],{},"In the VDOM world, some components call ",[20,3635,3629],{}," first, inspect the returned content, and then decide whether to render a fallback. In Vapor, calling a slot may immediately create Blocks and DOM, register reactive effects, or claim existing SSR DOM during hydration.",[16,3638,3639],{},"Do not write this:",[145,3641,3643],{"className":3038,"code":3642,"language":3040,"meta":151,"style":151},"\u003Cscript setup vapor>\nimport { useSlots } from 'vue'\n\nconst slots = useSlots()\nconst content = slots.default?.()\nconst showFallback = !content\n\u003C\u002Fscript>\n",[20,3644,3645,3657,3668,3672,3686,3704,3719],{"__ignoreMap":151},[382,3646,3647,3649,3651,3653,3655],{"class":384,"line":385},[382,3648,466],{"class":396},[382,3650,3050],{"class":3049},[382,3652,3053],{"class":392},[382,3654,3056],{"class":392},[382,3656,760],{"class":396},[382,3658,3659,3661,3664,3666],{"class":384,"line":400},[382,3660,900],{"class":388},[382,3662,3663],{"class":396}," { useSlots } ",[382,3665,906],{"class":388},[382,3667,2804],{"class":514},[382,3669,3670],{"class":384,"line":413},[382,3671,423],{"emptyLinePlaceholder":422},[382,3673,3674,3676,3679,3681,3684],{"class":384,"line":419},[382,3675,1472],{"class":388},[382,3677,3678],{"class":409}," slots",[382,3680,484],{"class":388},[382,3682,3683],{"class":392}," useSlots",[382,3685,493],{"class":396},[382,3687,3688,3690,3693,3695,3698,3701],{"class":384,"line":426},[382,3689,1472],{"class":388},[382,3691,3692],{"class":409}," content",[382,3694,484],{"class":388},[382,3696,3697],{"class":396}," slots.",[382,3699,3700],{"class":392},"default",[382,3702,3703],{"class":396},"?.()\n",[382,3705,3706,3708,3711,3713,3716],{"class":384,"line":437},[382,3707,1472],{"class":388},[382,3709,3710],{"class":409}," showFallback",[382,3712,484],{"class":388},[382,3714,3715],{"class":388}," !",[382,3717,3718],{"class":396},"content\n",[382,3720,3721,3723,3725],{"class":384,"line":475},[382,3722,3103],{"class":396},[382,3724,3050],{"class":3049},[382,3726,760],{"class":396},[16,3728,3729],{},"It is generally safer to let the template perform the actual slot rendering:",[145,3731,3733],{"className":3038,"code":3732,"language":3040,"meta":151,"style":151},"\u003Ctemplate>\n  \u003Cslot \u002F>\n\u003C\u002Ftemplate>\n",[20,3734,3735,3743,3756],{"__ignoreMap":151},[382,3736,3737,3739,3741],{"class":384,"line":385},[382,3738,466],{"class":396},[382,3740,3118],{"class":3049},[382,3742,760],{"class":396},[382,3744,3745,3747,3750,3754],{"class":384,"line":400},[382,3746,3125],{"class":396},[382,3748,3749],{"class":3049},"slot",[382,3751,3753],{"class":3752},"sX7ps"," \u002F",[382,3755,760],{"class":396},[382,3757,3758,3760,3762],{"class":384,"line":413},[382,3759,3103],{"class":396},[382,3761,3118],{"class":3049},[382,3763,760],{"class":396},[184,3765,3767],{"id":3766},"custom-directives-need-a-separate-migration","Custom directives need a separate migration",[16,3769,3770,3771,452,3774,2555,3777,3780],{},"A Vapor custom directive receives a node and reactive getters, and can return a cleanup function. It is not a direct copy of the ",[20,3772,3773],{},"mounted",[20,3775,3776],{},"updated",[20,3778,3779],{},"unmounted"," object used by a VDOM directive.",[16,3782,3783],{},"If a project or component library depends on many custom directives, list them as separate migration items and verify reactive updates, cleanup, and SSR behavior one by one.",[78,3785,3787],{"id":3786},"how-should-we-think-about-nuxt-router-pinia-and-component-libraries","How should we think about Nuxt, Router, Pinia, and component libraries?",[16,3789,3790],{},"Availability in Vue core does not mean the frameworks above it have completed their Vapor integration automatically.",[16,3792,3793],{},"The official Vapor Roadmap tracks Router, Pinia, Nuxt, DevTools, and Vue Test Utils separately. Nuxt also depends on SSR hydration support. In a real project, compiler recognition, server output, client hydration, and component inspection in development tools are all part of one complete chain.",[16,3795,3796],{},"I would therefore divide projects into two groups:",[184,3798,3800],{"id":3799},"ordinary-vue-vite-applications","Ordinary Vue \u002F Vite applications",[16,3802,3803],{},"Upgrade to the 3.6 RC on a branch, verify the existing VDOM mode first, and then try Vapor on one leaf page. The dependency chain is relatively direct, so problems are easier to attribute.",[184,3805,3807],{"id":3806},"nuxt-ssr-or-component-library-heavy-applications","Nuxt, SSR, or component-library-heavy applications",[16,3809,3810],{},"Do not force the underlying Vue version to the RC with a package override and call the Vapor migration complete. First confirm explicit support in the framework, build plugins, and SSR pipeline, then test hydration, route transitions, async components, and component-library interop.",[16,3812,3813],{},"Even without Vapor, testing Vue 3.6’s new reactivity core is worthwhile. Keep this experiment on an isolated branch or in a test environment.",[78,3815,3817],{"id":3816},"a-safer-upgrade-path","A safer upgrade path",[184,3819,3821],{"id":3820},"phase-one-upgrade-dependencies-without-enabling-vapor","Phase one: upgrade dependencies without enabling Vapor",[16,3823,3824],{},"An ordinary Vue \u002F Vite project can install the RC on an experimental branch:",[145,3826,3828],{"className":2165,"code":3827,"language":2167,"meta":151,"style":151},"pnpm add vue@3.6.0-rc.2\n",[20,3829,3830],{"__ignoreMap":151},[382,3831,3832,3834,3837],{"class":384,"line":385},[382,3833,2174],{"class":392},[382,3835,3836],{"class":514}," add",[382,3838,3839],{"class":514}," vue@3.6.0-rc.2\n",[16,3841,3842,3843,3846],{},"If the project depends directly on ",[20,3844,3845],{},"@vue\u002Fserver-renderer",", keep it on the same version as Vue:",[145,3848,3850],{"className":2165,"code":3849,"language":2167,"meta":151,"style":151},"pnpm add vue@3.6.0-rc.2 @vue\u002Fserver-renderer@3.6.0-rc.2\n",[20,3851,3852],{"__ignoreMap":151},[382,3853,3854,3856,3858,3861],{"class":384,"line":385},[382,3855,2174],{"class":392},[382,3857,3836],{"class":514},[382,3859,3860],{"class":514}," vue@3.6.0-rc.2",[382,3862,3863],{"class":514}," @vue\u002Fserver-renderer@3.6.0-rc.2\n",[16,3865,3866],{},"Then verify the original VDOM application first:",[40,3868,3869,3872,3875,3885,3888,3891,3894,3897],{},[43,3870,3871],{},"Unit and component tests.",[43,3873,3874],{},"Initial SSR and client hydration.",[43,3876,3877,3878,452,3880,2555,3882,3884],{},"Complex ",[20,3879,2558],{},[20,3881,2554],{},[20,3883,2986],{}," scenarios.",[43,3886,3887],{},"Effect cleanup after rapid route transitions.",[43,3889,3890],{},"Custom directives, Teleport, Suspense, and async components.",[43,3892,3893],{},"Development and production builds.",[43,3895,3896],{},"Browser console warnings and errors.",[43,3898,3899],{},"Timing and memory baselines for critical interactions.",[16,3901,3902],{},"If this phase fails, the cause is more likely the new reactivity implementation or an RC regression than Vapor, making diagnosis much easier.",[184,3904,3906],{"id":3905},"phase-two-choose-one-clearly-bounded-region","Phase two: choose one clearly bounded region",[16,3908,3909],{},"A good pilot usually has all of these properties:",[40,3911,3912,3915,3918,3921,3924,3927],{},[43,3913,3914],{},"It uses the Composition API.",[43,3916,3917],{},"It rarely depends on component instances.",[43,3919,3920],{},"It does not rely on a complex VDOM component library.",[43,3922,3923],{},"It updates frequently and has real performance pressure.",[43,3925,3926],{},"It has an independent route or a clear component-tree boundary.",[43,3928,3929],{},"It has a repeatable performance test.",[16,3931,3932],{},"Record JavaScript size, interaction latency, CPU time, and memory before the migration, then enable Vapor. Without a baseline, it is easy to mistake “it runs” for “it was worth migrating.”",[184,3934,3936],{"id":3935},"phase-three-verify-interoperability-and-ssr","Phase three: verify interoperability and SSR",[16,3938,3939],{},"At minimum, cover these combinations:",[337,3941,3942,3945,3948,3951,3954,3957,3960],{},[43,3943,3944],{},"A VDOM parent containing a Vapor child.",[43,3946,3947],{},"A Vapor parent containing a VDOM child.",[43,3949,3950],{},"Props, emits, and two-way binding.",[43,3952,3953],{},"Default, named, and scoped slots.",[43,3955,3956],{},"Suspense, async components, and error boundaries.",[43,3958,3959],{},"Server output, hydration, and mismatch recovery.",[43,3961,3962],{},"Watcher, event, and directive cleanup after unmount.",[16,3964,3965],{},"Many fixes in the rc.2 list concentrate on hydration, Suspense, slot anchors, and VDOM interoperability. That shows these areas are converging quickly—and that they deserve the most testing right now.",[184,3967,3969],{"id":3968},"phase-four-wait-for-stable-before-setting-a-production-schedule","Phase four: wait for stable before setting a production schedule",[16,3971,3972],{},"An RC means the feature set is close to final and the focus has shifted to regressions and edge cases. It does not mean the ecosystem is fully stable.",[16,3974,3975],{},"A production project should at least meet all of these conditions:",[40,3977,3978,3981,3984,3987,3990,3993],{},[43,3979,3980],{},"The stable Vue 3.6 release is available.",[43,3982,3983],{},"The framework and build tools explicitly support it.",[43,3985,3986],{},"Core component libraries pass interoperability tests.",[43,3988,3989],{},"DevTools, testing tools, and the SSR pipeline work.",[43,3991,3992],{},"The performance gain justifies migration and maintenance cost.",[43,3994,3995],{},"A rollback to VDOM is always available.",[78,3997,3999],{"id":3998},"four-things-i-would-not-do","Four things I would not do",[184,4001,4003],{"id":4002},"_1-do-not-treat-an-rc-like-an-ordinary-patch","1. Do not treat an RC like an ordinary patch",[16,4005,4006],{},"Prerelease behavior can still change. The event-delegation change from rc.1 to rc.2 is the clearest example.",[184,4008,4010],{"id":4009},"_2-do-not-migrate-every-component-just-to-use-vapor","2. Do not migrate every component just to “use Vapor”",[16,4012,4013],{},"Navigation, static content, and low-frequency settings pages may gain very little. Migrate actual bottlenecks before chasing a migration percentage.",[184,4015,4017],{"id":4016},"_3-do-not-turn-microbenchmarks-into-application-promises","3. Do not turn microbenchmarks into application promises",[16,4019,4020],{},"The “more than 30 times” result comes from a specific reactive stress case. “Comparable with Solid and Svelte 5” comes from a third-party benchmark. Both establish technical direction; neither replaces measurement in your project.",[184,4022,4024],{"id":4023},"_4-do-not-ignore-ecosystem-boundaries","4. Do not ignore ecosystem boundaries",[16,4026,4027],{},"Vue core, the SFC compiler, Vite plugins, Nuxt, SSR, Router, Pinia, component libraries, DevTools, and testing tools form the developer experience together. Moving one package to an RC does not upgrade the whole chain.",[78,4029,4031],{"id":4030},"recommendations-by-project-type","Recommendations by project type",[86,4033,4034,4047],{},[89,4035,4036],{},[92,4037,4038,4041,4044],{},[95,4039,4040],{},"Project type",[95,4042,4043],{},"Worth trying now?",[95,4045,4046],{},"Recommendation",[108,4048,4049,4060,4071,4082,4093,4104],{},[92,4050,4051,4054,4057],{},[113,4052,4053],{},"Small new tool or internal experiment",[113,4055,4056],{},"Yes",[113,4058,4059],{},"Try pure Vapor and measure bundle size and interaction",[92,4061,4062,4065,4068],{},[113,4063,4064],{},"Existing Vue SPA",[113,4066,4067],{},"Yes, on a branch",[113,4069,4070],{},"Keep VDOM first, then migrate one performance-sensitive page",[92,4072,4073,4076,4079],{},[113,4074,4075],{},"Nuxt \u002F SSR site",[113,4077,4078],{},"Good for research; wait for production",[113,4080,4081],{},"Confirm framework support and focus on hydration",[92,4083,4084,4087,4090],{},[113,4085,4086],{},"Component library",[113,4088,4089],{},"Begin compatibility testing early",[113,4091,4092],{},"Check instance proxies, slots, custom directives, and interop",[92,4094,4095,4098,4101],{},[113,4096,4097],{},"Large dashboard \u002F editor",[113,4099,4100],{},"Strong candidate for stress testing",[113,4102,4103],{},"Compare the separate gains from alien-signals and Vapor",[92,4105,4106,4109,4112],{},[113,4107,4108],{},"Content-focused site",[113,4110,4111],{},"Lower priority",[113,4113,4114],{},"Wait for stable Vue and a mature ecosystem rather than adding complexity for a limited gain",[78,4116,2339],{"id":2338},[16,4118,4119],{},"The most valuable part of Vue 3.6 RC is not the number of new APIs you can immediately add to application code. It advances two long-term questions at the same time:",[40,4121,4122,4125],{},[43,4123,4124],{},"Can Vue’s reactivity system remain efficient in more complex dependency graphs?",[43,4126,4127],{},"Can Vue templates preserve a familiar development experience without always paying the cost of a Virtual DOM?",[16,4129,4130],{},"alien-signals addresses the first question; Vapor attempts to address the second.",[16,4132,4133,4134,4137],{},"For an existing project, the most sensible strategy is: ",[31,4135,4136],{},"test 3.6 first as a reactivity-core upgrade, then evaluate Vapor as an optional, local compilation mode."," Testing them separately reveals where performance changes come from and keeps the risk inside clear boundaries.",[16,4139,4140],{},"Vue 3.6 is not yet at the stage where everyone should upgrade today, but it has reached the stage where every Vue team should begin to understand it.",[4142,4143],"hr",{},[78,4145,4147],{"id":4146},"sources-and-notes","Sources and notes",[40,4149,4150,4158,4166,4174,4182,4190],{},[43,4151,4152,4157],{},[2381,4153,4156],{"href":4154,"rel":4155},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fblob\u002Fminor\u002FCHANGELOG.md",[2385],"Vue 3.6 minor branch Changelog",": the Vapor overview and compatibility boundaries from 3.6.0-rc.1, plus the event-delegation change in rc.2.",[43,4159,4160,4165],{},[2381,4161,4164],{"href":4162,"rel":4163},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Freleases\u002Ftag\u002Fv3.6.0-rc.2",[2385],"Vue 3.6.0-rc.2 Release",": the latest release candidate when this article was published.",[43,4167,4168,4173],{},[2381,4169,4172],{"href":4170,"rel":4171},"https:\u002F\u002Fblog.vuejs.org\u002Fposts\u002Fvue-3-5",[2385],"Vue 3.5 release notes",": the baseline for reactivity improvements, SSR, and developer APIs in Vue 3.5.",[43,4175,4176,4181],{},[2381,4177,4180],{"href":4178,"rel":4179},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fpull\u002F12349",[2385],"Vue core: alien-signals refactor PR",": the reactivity implementation, memory benchmark, and selected stress-case microbenchmarks.",[43,4183,4184,4189],{},[2381,4185,4188],{"href":4186,"rel":4187},"https:\u002F\u002Fgithub.com\u002Fvuejs\u002Fcore\u002Fissues\u002F13687",[2385],"Vapor Mode Roadmap",": the roadmap for core capabilities and Vue ecosystem integration.",[43,4191,4192,4197],{},[2381,4193,4196],{"href":4194,"rel":4195},"https:\u002F\u002Fgithub.com\u002Fkrausest\u002Fjs-framework-benchmark",[2385],"js-framework-benchmark",": the third-party frontend benchmark cited by the Vue release notes.",[72,4199,4200],{},[16,4201,4202],{},"The upgrade recommendations in this article are my judgment based on official material and engineering experience. AI assisted with source organization and language review; each project should still rely on its own test results.",[2470,4204,4205],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}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 .sxg3X, html code.shiki .sxg3X{--shiki-default:#85E89D;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sX7ps, html code.shiki .sX7ps{--shiki-default:#FDAEB7;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":151,"searchDepth":413,"depth":413,"links":4207},[4208,4209,4210,4215,4220,4221,4226,4230,4236,4242,4243,4244],{"id":2574,"depth":400,"text":2575},{"id":2725,"depth":400,"text":2726},{"id":2776,"depth":400,"text":2777,"children":4211},[4212,4213,4214],{"id":2917,"depth":413,"text":2918},{"id":2946,"depth":413,"text":2947},{"id":2970,"depth":413,"text":2971},{"id":3010,"depth":400,"text":3011,"children":4216},[4217,4218,4219],{"id":3031,"depth":413,"text":3032},{"id":3225,"depth":413,"text":3226},{"id":3285,"depth":413,"text":3286},{"id":3378,"depth":400,"text":3379},{"id":3481,"depth":400,"text":3482,"children":4222},[4223,4225],{"id":3625,"depth":413,"text":4224},"Do not use slots.default() as a side-effect-free probe",{"id":3766,"depth":413,"text":3767},{"id":3786,"depth":400,"text":3787,"children":4227},[4228,4229],{"id":3799,"depth":413,"text":3800},{"id":3806,"depth":413,"text":3807},{"id":3816,"depth":400,"text":3817,"children":4231},[4232,4233,4234,4235],{"id":3820,"depth":413,"text":3821},{"id":3905,"depth":413,"text":3906},{"id":3935,"depth":413,"text":3936},{"id":3968,"depth":413,"text":3969},{"id":3998,"depth":400,"text":3999,"children":4237},[4238,4239,4240,4241],{"id":4002,"depth":413,"text":4003},{"id":4009,"depth":413,"text":4010},{"id":4016,"depth":413,"text":4017},{"id":4023,"depth":413,"text":4024},{"id":4030,"depth":400,"text":4031},{"id":2338,"depth":400,"text":2339},{"id":4146,"depth":400,"text":4147},"As of Vue 3.6.0-rc.2, this guide compares Vue 3.5 and 3.6 across the reactivity core, Vapor Mode, event delegation, interoperability boundaries, and a progressive upgrade strategy.",{"src":4247,"alt":4248,"caption":4249,"width":2514,"height":2515},"\u002Fmedia\u002Fposts\u002Fvue-3-6-rc-upgrade-guide\u002Fcover-v1.webp","A Vue 3.5 Virtual DOM component tree passing through a crystalline channel into Vue 3.6's fine-grained signal network","Vue 3.6 RC feels more like an architectural gear change in the compiler and runtime than a list of new APIs",{},"\u002Fblog\u002Fvue-3-6-rc-upgrade-guide","2026-07-26",{"title":2536,"description":4245},"blog\u002Fvue-3-6-rc-upgrade-guide",[4256,4257,4258],"Vue","JavaScript","Frontend Engineering","J94AWVXN7gh8NSw1A38RH85n-bwRB0Nf9N9KH9LFraw",{"id":4261,"title":4262,"accent":4263,"body":4264,"description":4505,"draft":2507,"experience":4506,"extension":2509,"featured":422,"image":4511,"meta":4515,"navigation":422,"path":4516,"publishedAt":4517,"readingTime":475,"seo":4518,"stem":4519,"tags":4520,"updatedAt":2508,"video":2508,"__hash__":4524,"contentLocale":5,"isFallback":2507},"posts_en\u002Fblog\u002Ffrom-gpt-4-invite-to-agents.md","From a GPT‑4 Invitation Email to the Age of Agents","violet",{"type":13,"value":4265,"toc":4497},[4266,4269,4272,4277,4284,4290,4293,4296,4300,4303,4306,4312,4315,4318,4322,4325,4339,4342,4359,4365,4368,4371,4375,4378,4381,4395,4398,4412,4415,4419,4422,4429,4446,4449,4453,4456,4459,4462,4465,4467,4471],[16,4267,4268],{},"While sorting through old email today, I came across a message dated March 18, 2023.",[16,4270,4271],{},"Its subject was direct:",[72,4273,4274],{},[16,4275,4276],{},"You’re invited to use the OpenAI GPT-4 API.",[16,4278,4279,4280,4283],{},"The body told me I could now use GPT‑4 with an ",[31,4281,4282],{},"8K context window"," through the API.",[16,4285,4286],{},[166,4287],{"alt":4288,"src":4289},"The 2023 GPT‑4 API invitation email, stating that 8K context access was available","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Femail-invite-v1.webp",[16,4291,4292],{},"GPT‑4 had been released only days earlier. Developers had to apply, wait, and receive access as OpenAI gradually expanded availability. The launch material described a standard context length of 8,192 tokens, with a 32,768-token version available on a limited basis. At the time, API access itself felt like a small door that had just opened.",[16,4294,4295],{},"More than three years later, that message reads less like a product notification and more like a time capsule.",[78,4297,4299],{"id":4298},"bigger-numbers-are-only-the-most-visible-change","Bigger numbers are only the most visible change",[16,4301,4302],{},"Early developers became familiar with context windows measured in 4K or 8K. While writing prompts, we counted the remaining space, trimmed source material, split long documents into smaller pieces, and devised ways for one request to carry the result of the previous one.",[16,4304,4305],{},"By 2026, million-token context windows had entered the mainstream of frontier models. OpenAI's current model catalog lists the GPT‑5.6 family with a context window of roughly 1.05 million tokens. Google's long-context documentation likewise describes multiple Gemini models that support one million tokens or more.",[16,4307,4308],{},[166,4309],{"alt":4310,"src":4311},"Context grows from 4K and 8K into a multimodal corridor of more than one million tokens","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fcontext-window-v1.webp",[16,4313,4314],{},"This is not simply a matter of fitting in more text. Code, images, audio, video, and large document sets can now form the background of a single task. Material that once had to be fragmented, indexed, and summarized has a better chance of sharing one working context.",[16,4316,4317],{},"But a longer context window is not perfect memory, and it does not guarantee that a model will notice what matters. More material can also introduce more noise, cost, latency, and false associations. The important skill remains deciding what information to provide, in what order, and how to verify that the model understood the critical constraints.",[78,4319,4321],{"id":4320},"the-real-change-is-the-unit-of-interaction","The real change is the unit of interaction",[16,4323,4324],{},"Three years ago, my usual pattern was “question and answer”:",[337,4326,4327,4330,4333,4336],{},[43,4328,4329],{},"Write a prompt.",[43,4331,4332],{},"Wait for the model to generate a response.",[43,4334,4335],{},"Copy the result.",[43,4337,4338],{},"Complete the remaining work myself.",[16,4340,4341],{},"Today, more tasks look like “goal, context, action, and verification”:",[337,4343,4344,4347,4350,4353,4356],{},[43,4345,4346],{},"State the desired outcome and the definition of done.",[43,4348,4349],{},"Provide the repository, documents, and current state.",[43,4351,4352],{},"Allow the model to use search, files, a terminal, or other tools.",[43,4354,4355],{},"Let it execute a sequence of dependent steps.",[43,4357,4358],{},"Confirm the result through tests, builds, diffs, or human review.",[16,4360,4361],{},[166,4362],{"alt":4363,"src":4364},"A chat box expands into an agent workflow with documents, code, a browser, tool calls, and human confirmation","\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fagent-workflow-v1.webp",[16,4366,4367],{},"OpenAI's current model and Responses API documentation lists function calling, web search, file search, and computer use among the available tools. Models no longer only tell us how something might be done. Within the authority they are given, they can participate in the work itself.",[16,4369,4370],{},"That is the most important difference I see between a chat model and an agent: the first delivers a piece of content; the second advances a task whose state changes over time.",[78,4372,4374],{"id":4373},"programming-has-not-disappeared-attention-has-moved-to-both-ends","Programming has not disappeared; attention has moved to both ends",[16,4376,4377],{},"When a model can read code, edit files, run commands, and inspect results, programmers do not suddenly become irrelevant. Instead, our attention moves toward both ends of the task.",[16,4379,4380],{},"Before action, the problem must be defined more clearly:",[40,4382,4383,4386,4389,4392],{},[43,4384,4385],{},"What is the goal, and what is out of scope?",[43,4387,4388],{},"Which sources are authoritative, and which are only references?",[43,4390,4391],{},"Which permissions can be granted, and which actions require confirmation?",[43,4393,4394],{},"Should the system stop, retry, or roll back when something fails?",[16,4396,4397],{},"After action, the result must be verified more rigorously:",[40,4399,4400,4403,4406,4409],{},[43,4401,4402],{},"Do the code and build pass?",[43,4404,4405],{},"Does the page truly match the requirement, rather than merely having been generated?",[43,4407,4408],{},"Has the external state actually changed?",[43,4410,4411],{},"Can the outcome be explained, reproduced, and audited?",[16,4413,4414],{},"Three years ago, we were learning how to write a better prompt. Today, we must also learn how to design context, tools, permissions, checkpoints, and acceptance criteria.",[78,4416,4418],{"id":4417},"capability-can-be-delegated-responsibility-cannot","Capability can be delegated; responsibility cannot",[16,4420,4421],{},"As agents gain the ability to perform real actions, boundaries become more important.",[16,4423,4424,4425,4428],{},"Asking a model to summarize an article is not the same authorization as asking it to modify a production database. Asking it to draft an email is not the same risk as allowing it to send that email directly to a customer. A mature agent system should not only pursue what it ",[173,4426,4427],{},"can"," do. It should make explicit:",[40,4430,4431,4434,4437,4440,4443],{},[43,4432,4433],{},"whether access is read-only or writable by default;",[43,4435,4436],{},"which actions require human approval before execution;",[43,4438,4439],{},"how targets, directories, accounts, and data scopes are constrained;",[43,4441,4442],{},"how the necessary operation history is retained;",[43,4444,4445],{},"how the system stops and recovers when something goes wrong.",[16,4447,4448],{},"Models can take on more action, but judgment and accountability do not disappear. Someone must still answer: Why are we doing this? What evidence supports the decision? Who carries the risk? Is the result acceptable?",[78,4450,4452],{"id":4451},"how-will-we-see-today-three-years-from-now","How will we see today three years from now?",[16,4454,4455],{},"That old email now feels like a ticket I happened to keep.",[16,4457,4458],{},"In 2023, I applied for a chance to access a model. By 2026, we were learning how to help models understand long-term goals, use tools, move through multiple steps, and complete work under human supervision.",[16,4460,4461],{},"What strikes me is not how many times a parameter increased. It is how compressed the timeline became. Capabilities that once belonged to the distant future did not arrive gradually over decades; they quietly became ordinary between a handful of version updates.",[16,4463,4464],{},"Perhaps, three years from now, the million-token context windows and agents that impress us today will look like this email: an old object left behind at the moment the future had only just begun.",[4142,4466],{},[78,4468,4470],{"id":4469},"source-notes","Source notes",[40,4472,4473,4481,4489],{},[43,4474,4475,4480],{},[2381,4476,4479],{"href":4477,"rel":4478},"https:\u002F\u002Fopenai.com\u002Findex\u002Fgpt-4-research\u002F",[2385],"OpenAI: GPT‑4 research"," — GPT‑4 launched in March 2023 with a standard 8,192-token context and limited access to a 32,768-token version.",[43,4482,4483,4488],{},[2381,4484,4487],{"href":4485,"rel":4486},"https:\u002F\u002Fdevelopers.openai.com\u002Fapi\u002Fdocs\u002Fmodels",[2385],"OpenAI: model catalog"," — At publication, the frontier GPT‑5.6 family lists a context window of roughly 1.05 million tokens and support for multiple tool categories.",[43,4490,4491,4496],{},[2381,4492,4495],{"href":4493,"rel":4494},"https:\u002F\u002Fai.google.dev\u002Fgemini-api\u002Fdocs\u002Flong-context",[2385],"Google AI: Long context"," — An overview of the capabilities, uses, and limits of Gemini models with contexts of one million tokens or more.",{"title":151,"searchDepth":413,"depth":413,"links":4498},[4499,4500,4501,4502,4503,4504],{"id":4298,"depth":400,"text":4299},{"id":4320,"depth":400,"text":4321},{"id":4373,"depth":400,"text":4374},{"id":4417,"depth":400,"text":4418},{"id":4451,"depth":400,"text":4452},{"id":4469,"depth":400,"text":4470},"In 2023, I waited for access to an API with an 8K context window. Three years later, we are learning how to give agents goals, permissions, boundaries, and acceptance criteria.",{"src":4507,"title":4508,"description":4509,"duration":4510,"hasAudio":422},"\u002Fexperiences\u002Ffrom-gpt-4-invite-to-agents","From the GPT-4 Invitation to the Agent Era · An Interactive Story in Six Acts","A 2 minute 26 second look back at context windows, agent workflows, and the changing relationship between capability and responsibility. Chinese narration.",146,{"src":4512,"alt":4513,"caption":4514,"width":2514,"height":2515},"\u002Fmedia\u002Fposts\u002Ffrom-gpt-4-invite-to-agents\u002Fcover-en-v1.webp","An email from 2023 extends from a glass time capsule into a modern agent workspace","Three years ago we applied for model access; today we are learning how to work alongside models",{},"\u002Fblog\u002Ffrom-gpt-4-invite-to-agents","2026-07-25",{"title":4262,"description":4505},"blog\u002Ffrom-gpt-4-invite-to-agents",[4521,4522,4523],"AI","GPT-4","Agents","JHiIWjUNsM0I-8e6SyFL1qVit-yvg_sb-_B8b0ZiH_E",{"id":4526,"title":4527,"accent":11,"body":4528,"description":4969,"draft":2507,"experience":2508,"extension":2509,"featured":422,"image":4970,"meta":4974,"navigation":422,"path":4975,"publishedAt":4976,"readingTime":496,"seo":4977,"stem":4978,"tags":4979,"updatedAt":4981,"video":2508,"__hash__":4982,"contentLocale":5,"isFallback":2507},"posts_en\u002Fblog\u002Fedge-stateful-apps.md","Building Stateful Applications at the Edge",{"type":13,"value":4529,"toc":4961},[4530,4536,4539,4543,4546,4596,4599,4604,4607,4611,4614,4684,4687,4769,4772,4776,4779,4856,4871,4874,4878,4881,4884,4895,4898,4901,4905,4908,4922,4925,4929,4932,4935,4952,4958],[16,4531,4532,4533],{},"The most compelling promise of edge computing is that it brings code closer to users. But once an application starts storing accounts, sessions, and comments, the question is no longer simply how to deploy a function to more locations. It becomes: ",[31,4534,4535],{},"Where does state actually live, who is responsible for it, and how does the system degrade when something fails?",[16,4537,4538],{},"This article uses a blog with authentication and comments as a minimal example to examine the design boundaries that matter when building a stateful application on Cloudflare Workers and D1.",[78,4540,4542],{"id":4541},"start-by-mapping-the-state","Start by mapping the state",[16,4544,4545],{},"Even a small blog contains at least three very different kinds of state:",[86,4547,4548,4561],{},[89,4549,4550],{},[92,4551,4552,4555,4558],{},[95,4553,4554],{},"State",[95,4556,4557],{},"Source",[95,4559,4560],{},"Lifecycle",[108,4562,4563,4574,4585],{},[92,4564,4565,4568,4571],{},[113,4566,4567],{},"Article content",[113,4569,4570],{},"Markdown in Git",[113,4572,4573],{},"Updated with deployments",[92,4575,4576,4579,4582],{},[113,4577,4578],{},"Users and comments",[113,4580,4581],{},"D1",[113,4583,4584],{},"Long-term persistence",[92,4586,4587,4590,4593],{},[113,4588,4589],{},"Login sessions",[113,4591,4592],{},"D1 + Cookie",[113,4594,4595],{},"Explicit expiration",[16,4597,4598],{},"Article content belongs with the code version. It needs review, rollback, and build-time validation, but readers do not need to modify it at runtime. User data is the opposite: it must survive independently of the deployment lifecycle.",[72,4600,4601],{},[16,4602,4603],{},"Before choosing a database, decide whether a piece of data should follow code versions or user behavior.",[16,4605,4606],{},"That distinction naturally leads to a simple architecture: Nuxt Content owns the Markdown, D1 owns runtime business data, and the Worker combines them for each request.",[78,4608,4610],{"id":4609},"bindings-matter-more-than-network-requests","Bindings matter more than network requests",[16,4612,4613],{},"A Worker does not need to store an API token to access D1, and it should not call the Cloudflare REST API at runtime. D1 enters the runtime directly through a binding:",[145,4615,4618],{"className":4616,"code":4617,"filename":1115,"language":1404,"meta":151,"style":151},"language-json shiki shiki-themes github-dark github-dark","{\n  \"d1_databases\": [\n    {\n      \"binding\": \"DB\",\n      \"database_name\": \"signal-log-db\",\n      \"database_id\": \"\u003CYOUR_DATABASE_ID>\"\n    }\n  ]\n}\n",[20,4619,4620,4624,4632,4636,4649,4661,4671,4675,4680],{"__ignoreMap":151},[382,4621,4622],{"class":384,"line":385},[382,4623,1127],{"class":396},[382,4625,4626,4629],{"class":384,"line":400},[382,4627,4628],{"class":409},"  \"d1_databases\"",[382,4630,4631],{"class":396},": [\n",[382,4633,4634],{"class":384,"line":413},[382,4635,1137],{"class":396},[382,4637,4638,4641,4644,4647],{"class":384,"line":419},[382,4639,4640],{"class":409},"      \"binding\"",[382,4642,4643],{"class":396},": ",[382,4645,4646],{"class":514},"\"DB\"",[382,4648,667],{"class":396},[382,4650,4651,4654,4656,4659],{"class":384,"line":426},[382,4652,4653],{"class":409},"      \"database_name\"",[382,4655,4643],{"class":396},[382,4657,4658],{"class":514},"\"signal-log-db\"",[382,4660,667],{"class":396},[382,4662,4663,4666,4668],{"class":384,"line":437},[382,4664,4665],{"class":409},"      \"database_id\"",[382,4667,4643],{"class":396},[382,4669,4670],{"class":514},"\"\u003CYOUR_DATABASE_ID>\"\n",[382,4672,4673],{"class":384,"line":475},[382,4674,586],{"class":396},[382,4676,4677],{"class":384,"line":496},[382,4678,4679],{"class":396},"  ]\n",[382,4681,4682],{"class":384,"line":527},[382,4683,416],{"class":396},[16,4685,4686],{},"In a server route, the database object comes from the current request context. Every SQL query uses a prepared statement:",[145,4688,4690],{"className":376,"code":4689,"language":378,"meta":151,"style":151},"const comment = await db\n  .prepare(`\n    SELECT id, body, created_at\n    FROM comments\n    WHERE post_slug = ?1 AND status = 'published'\n    ORDER BY created_at DESC\n    LIMIT 100\n  `)\n  .bind(postSlug)\n  .all()\n",[20,4691,4692,4706,4718,4723,4728,4733,4738,4743,4750,4760],{"__ignoreMap":151},[382,4693,4694,4696,4699,4701,4703],{"class":384,"line":385},[382,4695,1472],{"class":388},[382,4697,4698],{"class":409}," comment",[382,4700,484],{"class":388},[382,4702,955],{"class":388},[382,4704,4705],{"class":396}," db\n",[382,4707,4708,4710,4713,4715],{"class":384,"line":400},[382,4709,3336],{"class":396},[382,4711,4712],{"class":392},"prepare",[382,4714,446],{"class":396},[382,4716,4717],{"class":514},"`\n",[382,4719,4720],{"class":384,"line":413},[382,4721,4722],{"class":514},"    SELECT id, body, created_at\n",[382,4724,4725],{"class":384,"line":419},[382,4726,4727],{"class":514},"    FROM comments\n",[382,4729,4730],{"class":384,"line":426},[382,4731,4732],{"class":514},"    WHERE post_slug = ?1 AND status = 'published'\n",[382,4734,4735],{"class":384,"line":437},[382,4736,4737],{"class":514},"    ORDER BY created_at DESC\n",[382,4739,4740],{"class":384,"line":475},[382,4741,4742],{"class":514},"    LIMIT 100\n",[382,4744,4745,4748],{"class":384,"line":496},[382,4746,4747],{"class":514},"  `",[382,4749,574],{"class":396},[382,4751,4752,4754,4757],{"class":384,"line":527},[382,4753,3336],{"class":396},[382,4755,4756],{"class":392},"bind",[382,4758,4759],{"class":396},"(postSlug)\n",[382,4761,4762,4764,4767],{"class":384,"line":532},[382,4763,3336],{"class":396},[382,4765,4766],{"class":392},"all",[382,4768,493],{"class":396},[16,4770,4771],{},"Parameter binding is not merely a matter of code style; it is part of the input boundary. Article paths, email addresses, display names, and comment bodies all come from users. None of them should ever be concatenated into SQL.",[78,4773,4775],{"id":4774},"store-only-irreversible-session-digests","Store only irreversible session digests",[16,4777,4778],{},"After a successful login, the browser receives a random session token while the database stores only its SHA-256 digest. If the session table is accidentally exposed, the original tokens still cannot be used directly to sign in.",[145,4780,4782],{"className":376,"code":4781,"language":378,"meta":151,"style":151},"const token = createRandomToken()\nconst tokenHash = await sha256(token)\n\nawait db\n  .prepare('INSERT INTO sessions (token_hash, user_id, expires_at) VALUES (?1, ?2, ?3)')\n  .bind(tokenHash, userId, expiresAt)\n  .run()\n",[20,4783,4784,4798,4815,4819,4825,4838,4847],{"__ignoreMap":151},[382,4785,4786,4788,4791,4793,4796],{"class":384,"line":385},[382,4787,1472],{"class":388},[382,4789,4790],{"class":409}," token",[382,4792,484],{"class":388},[382,4794,4795],{"class":392}," createRandomToken",[382,4797,493],{"class":396},[382,4799,4800,4802,4805,4807,4809,4812],{"class":384,"line":400},[382,4801,1472],{"class":388},[382,4803,4804],{"class":409}," tokenHash",[382,4806,484],{"class":388},[382,4808,955],{"class":388},[382,4810,4811],{"class":392}," sha256",[382,4813,4814],{"class":396},"(token)\n",[382,4816,4817],{"class":384,"line":413},[382,4818,423],{"emptyLinePlaceholder":422},[382,4820,4821,4823],{"class":384,"line":419},[382,4822,1578],{"class":388},[382,4824,4705],{"class":396},[382,4826,4827,4829,4831,4833,4836],{"class":384,"line":426},[382,4828,3336],{"class":396},[382,4830,4712],{"class":392},[382,4832,446],{"class":396},[382,4834,4835],{"class":514},"'INSERT INTO sessions (token_hash, user_id, expires_at) VALUES (?1, ?2, ?3)'",[382,4837,574],{"class":396},[382,4839,4840,4842,4844],{"class":384,"line":437},[382,4841,3336],{"class":396},[382,4843,4756],{"class":392},[382,4845,4846],{"class":396},"(tokenHash, userId, expiresAt)\n",[382,4848,4849,4851,4854],{"class":384,"line":475},[382,4850,3336],{"class":396},[382,4852,4853],{"class":392},"run",[382,4855,493],{"class":396},[16,4857,4858,4859,4862,4863,4866,4867,4870],{},"The cookie uses ",[20,4860,4861],{},"HttpOnly"," so frontend JavaScript cannot read it, ",[20,4864,4865],{},"SameSite=Lax"," to reduce cross-site request risk, and ",[20,4868,4869],{},"Secure"," in production.",[16,4872,4873],{},"This does not make the system “absolutely secure.” A more accurate description is that every layer holds only the minimum capability it needs to do its job.",[78,4875,4877],{"id":4876},"design-consistency-around-user-actions","Design consistency around user actions",[16,4879,4880],{},"D1 is a relational database, but an edge application still needs to identify which flows require strong consistency.",[16,4882,4883],{},"For a blog, there are only a few critical actions:",[337,4885,4886,4889,4892],{},[43,4887,4888],{},"A newly registered user should be able to sign in immediately.",[43,4890,4891],{},"A successfully submitted comment should appear on the current page immediately.",[43,4893,4894],{},"An old session should become invalid immediately after logout.",[16,4896,4897],{},"These flows should write to and immediately read from the same primary database. Read-only scenarios such as article lists and public comment lists have much more room for caching.",[16,4899,4900],{},"Do not begin with “Where can we add a cache?” Begin with “What promise did the user just complete?”",[78,4902,4904],{"id":4903},"failure-also-needs-product-design","Failure also needs product design",[16,4906,4907],{},"Edge runtimes can fail, and databases can become temporarily unavailable. The difference is whether those failures become states that users can understand.",[40,4909,4910,4913,4916,4919],{},[43,4911,4912],{},"When login fails, do not distinguish between “email not found” and “wrong password,” because that would expose account-enumeration information.",[43,4914,4915],{},"When a comment write fails, preserve the draft so the user can retry.",[43,4917,4918],{},"When a content query fails, return a real error state instead of a blank page.",[43,4920,4921],{},"Use structured logs for server errors, but never send internal SQL or stack traces to the browser.",[16,4923,4924],{},"Reliability does not mean “nothing ever breaks.” It means the boundaries remain intact when something does.",[78,4926,4928],{"id":4927},"finally-keep-the-architecture-boring","Finally, keep the architecture boring",[16,4930,4931],{},"This design has no message queue, distributed lock, or separate identity service. That is not because those tools have no value, but because the current problem has not asked for them.",[16,4933,4934],{},"A maintainable personal site usually starts with:",[40,4936,4937,4940,4943,4946,4949],{},[43,4938,4939],{},"Content follows Git.",[43,4941,4942],{},"Runtime data goes into D1.",[43,4944,4945],{},"Secrets stay in Worker secrets.",[43,4947,4948],{},"Inputs are validated at the boundary.",[43,4950,4951],{},"Failures are clear, observable, and recoverable.",[16,4953,4954,4955],{},"When traffic or product requirements actually change, let the architecture evolve with the evidence. ",[31,4956,4957],{},"The edge is already fast enough; engineering’s job is to keep it simple.",[2470,4959,4960],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}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":151,"searchDepth":413,"depth":413,"links":4962},[4963,4964,4965,4966,4967,4968],{"id":4541,"depth":400,"text":4542},{"id":4609,"depth":400,"text":4610},{"id":4774,"depth":400,"text":4775},{"id":4876,"depth":400,"text":4877},{"id":4903,"depth":400,"text":4904},{"id":4927,"depth":400,"text":4928},"Cloudflare Workers are fast and D1 is close, but the hard part is designing clear boundaries for state, consistency, and failure.",{"src":4971,"alt":4972,"caption":4973,"width":2514,"height":2515},"\u002Fmedia\u002Fposts\u002Fedge-stateful-apps\u002Fcover-v1.webp","Edge nodes forming a stateful application topology around a central database","State, edge nodes, and consistency boundaries",{},"\u002Fblog\u002Fedge-stateful-apps","2026-07-21",{"title":4527,"description":4969},"blog\u002Fedge-stateful-apps",[2522,4581,4980],"Engineering","2026-07-24","CI8axFA_TIbwus2kc1I4Qy9mGCj2mlvbMr_npgZk08E",{"id":4984,"title":4985,"accent":2537,"body":4986,"description":5374,"draft":2507,"experience":2508,"extension":2509,"featured":2507,"image":5375,"meta":5379,"navigation":422,"path":5380,"publishedAt":5381,"readingTime":437,"seo":5382,"stem":5383,"tags":5384,"updatedAt":2508,"video":2508,"__hash__":5387,"contentLocale":5,"isFallback":2507},"posts_en\u002Fblog\u002Fnuxt-4-architecture-subtraction.md","Architectural Subtraction in Nuxt 4",{"type":13,"value":4987,"toc":5366},[4988,4991,4997,5001,5004,5055,5058,5065,5069,5072,5196,5199,5216,5219,5223,5226,5229,5232,5237,5241,5248,5254,5257,5260,5264,5267,5333,5336,5340,5343,5360,5363],[16,4989,4990],{},"Many Nuxt projects become complicated not because the business is genuinely complex, but because we put every possible future into the architecture too early.",[16,4992,4993,4994,347],{},"A personal blog is a useful calibration tool: it has a limited number of pages, a stable content structure, and interaction concentrated in a few areas. It reminds us that a framework’s most valuable contribution is often not adding capabilities, but ",[31,4995,4996],{},"removing unnecessary choices",[78,4998,5000],{"id":4999},"begin-with-ssr-by-default","Begin with SSR by default",[16,5002,5003],{},"Technical articles need to be understood by search engines, and their body text should appear as early as possible on a slow connection. Nuxt’s default server-side rendering matches that goal.",[145,5005,5008],{"className":376,"code":5006,"filename":5007,"language":378,"meta":151,"style":151},"export default defineNuxtConfig({\n  modules: ['@nuxt\u002Fcontent'],\n  nitro: {\n    preset: 'cloudflare',\n  },\n})\n","nuxt.config.ts",[20,5009,5010,5021,5032,5037,5047,5051],{"__ignoreMap":151},[382,5011,5012,5014,5016,5019],{"class":384,"line":385},[382,5013,429],{"class":388},[382,5015,432],{"class":388},[382,5017,5018],{"class":392}," defineNuxtConfig",[382,5020,978],{"class":396},[382,5022,5023,5026,5029],{"class":384,"line":400},[382,5024,5025],{"class":396},"  modules: [",[382,5027,5028],{"class":514},"'@nuxt\u002Fcontent'",[382,5030,5031],{"class":396},"],\n",[382,5033,5034],{"class":384,"line":413},[382,5035,5036],{"class":396},"  nitro: {\n",[382,5038,5039,5042,5045],{"class":384,"line":419},[382,5040,5041],{"class":396},"    preset: ",[382,5043,5044],{"class":514},"'cloudflare'",[382,5046,667],{"class":396},[382,5048,5049],{"class":384,"line":426},[382,5050,740],{"class":396},[382,5052,5053],{"class":384,"line":437},[382,5054,1521],{"class":396},[16,5056,5057],{},"There is no need to turn the whole site into an SPA or invent a separate loading strategy for every page. The page performs its first query and render on the server, and the client continues with the same router after taking over.",[16,5059,5060,5061,5064],{},"If one small area truly depends on browser APIs, use ",[20,5062,5063],{},"ClientOnly"," locally. Do not sacrifice the semantics and first screen of an entire page for one client component.",[78,5066,5068],{"id":5067},"keep-content-queries-at-the-page-boundary","Keep content queries at the page boundary",[16,5070,5071],{},"The homepage, article index, and article detail page need different data shapes. Queries should stay close to the pages that consume them:",[145,5073,5075],{"className":376,"code":5074,"language":378,"meta":151,"style":151},"const { data: posts } = await useAsyncData('latest-posts', () => {\n  return queryCollection('posts')\n    .where('draft', '=', false)\n    .order('publishedAt', 'DESC')\n    .limit(6)\n    .all()\n})\n",[20,5076,5077,5112,5126,5151,5170,5184,5192],{"__ignoreMap":151},[382,5078,5079,5081,5083,5086,5088,5091,5093,5095,5097,5100,5102,5105,5108,5110],{"class":384,"line":385},[382,5080,1472],{"class":388},[382,5082,1560],{"class":396},[382,5084,5085],{"class":403},"data",[382,5087,4643],{"class":396},[382,5089,5090],{"class":409},"posts",[382,5092,1565],{"class":396},[382,5094,1568],{"class":388},[382,5096,955],{"class":388},[382,5098,5099],{"class":392}," useAsyncData",[382,5101,446],{"class":396},[382,5103,5104],{"class":514},"'latest-posts'",[382,5106,5107],{"class":396},", () ",[382,5109,1550],{"class":388},[382,5111,397],{"class":396},[382,5113,5114,5116,5119,5121,5124],{"class":384,"line":400},[382,5115,1740],{"class":388},[382,5117,5118],{"class":392}," queryCollection",[382,5120,446],{"class":396},[382,5122,5123],{"class":514},"'posts'",[382,5125,574],{"class":396},[382,5127,5128,5131,5134,5136,5139,5141,5144,5146,5149],{"class":384,"line":413},[382,5129,5130],{"class":396},"    .",[382,5132,5133],{"class":392},"where",[382,5135,446],{"class":396},[382,5137,5138],{"class":514},"'draft'",[382,5140,452],{"class":396},[382,5142,5143],{"class":514},"'='",[382,5145,452],{"class":396},[382,5147,5148],{"class":409},"false",[382,5150,574],{"class":396},[382,5152,5153,5155,5158,5160,5163,5165,5168],{"class":384,"line":419},[382,5154,5130],{"class":396},[382,5156,5157],{"class":392},"order",[382,5159,446],{"class":396},[382,5161,5162],{"class":514},"'publishedAt'",[382,5164,452],{"class":396},[382,5166,5167],{"class":514},"'DESC'",[382,5169,574],{"class":396},[382,5171,5172,5174,5177,5179,5182],{"class":384,"line":426},[382,5173,5130],{"class":396},[382,5175,5176],{"class":392},"limit",[382,5178,446],{"class":396},[382,5180,5181],{"class":409},"6",[382,5183,574],{"class":396},[382,5185,5186,5188,5190],{"class":384,"line":437},[382,5187,5130],{"class":396},[382,5189,4766],{"class":392},[382,5191,493],{"class":396},[382,5193,5194],{"class":384,"line":475},[382,5195,1521],{"class":396},[16,5197,5198],{},"This has several direct benefits:",[40,5200,5201,5207,5210,5213],{},[43,5202,5203,5204,347],{},"The data participates in SSR instead of flashing into view after ",[20,5205,5206],{},"onMounted",[43,5208,5209],{},"The query key is explicit, so Nuxt can reuse the payload during hydration.",[43,5211,5212],{},"The page knows what it needs without going through a generic store.",[43,5214,5215],{},"Draft filtering and sorting rules appear where they are actually relevant.",[16,5217,5218],{},"Only extract a composable or store when several pages truly share the same complex business state.",[78,5220,5222],{"id":5221},"extract-only-stable-repetition-into-components","Extract only stable repetition into components",[16,5224,5225],{},"“It might be reusable” is not a component boundary. Stable repetition usually includes structure, semantics, and behavior at the same time.",[16,5227,5228],{},"On this blog, an article card is a good component. It appears on both the homepage and the index, with consistent metadata, title, summary, tags, and navigation behavior.",[16,5230,5231],{},"The homepage hero, however, does not necessarily need to become five small components just because it is large. It appears once, and its internal elements work together as one visual narrative. Splitting it too early only spreads styling relationships across files.",[72,5233,5234],{},[16,5235,5236],{},"A component is not a file-organization tool. It is a UI contract that can be understood and reused independently.",[78,5238,5240],{"id":5239},"keep-server-apis-narrow","Keep server APIs narrow",[16,5242,5243,5244,5247],{},"Authentication and comments are runtime data, so they belong in ",[20,5245,5246],{},"server\u002Fapi",". Each endpoint does one job:",[145,5249,5252],{"className":5250,"code":5251,"language":150,"meta":151},[148],"POST \u002Fapi\u002Fauth\u002Fregister\nPOST \u002Fapi\u002Fauth\u002Flogin\nPOST \u002Fapi\u002Fauth\u002Flogout\nGET  \u002Fapi\u002Fauth\u002Fsession\nGET  \u002Fapi\u002Fcomments\u002F:post\nPOST \u002Fapi\u002Fcomments\u002F:post\n",[20,5253,5251],{"__ignoreMap":151},[16,5255,5256],{},"Narrow interfaces make authorization rules easier to review. Reading comments is public, creating one requires a session, and logging out destroys only the current token without touching the user profile.",[16,5258,5259],{},"If an endpoint handles reads, writes, login, and format conversion at the same time, it has already lost its boundary.",[78,5261,5263],{"id":5262},"put-state-back-in-the-right-place","Put state back in the right place",[16,5265,5266],{},"Vue’s reactive state is convenient, but not every state belongs in JavaScript:",[86,5268,5269,5278],{},[89,5270,5271],{},[92,5272,5273,5275],{},[95,5274,4554],{},[95,5276,5277],{},"Best location",[108,5279,5280,5288,5298,5306,5317,5325],{},[92,5281,5282,5285],{},[113,5283,5284],{},"Current article",[113,5286,5287],{},"Route",[92,5289,5290,5293],{},[113,5291,5292],{},"Search term",[113,5294,5295,5296],{},"Page ",[20,5297,2551],{},[92,5299,5300,5303],{},[113,5301,5302],{},"Tag filter",[113,5304,5305],{},"URL query + page state",[92,5307,5308,5311],{},[113,5309,5310],{},"Signed-in user",[113,5312,5313,5314],{},"SSR-safe ",[20,5315,5316],{},"useState",[92,5318,5319,5322],{},[113,5320,5321],{},"Article body",[113,5323,5324],{},"Nuxt Content",[92,5326,5327,5330],{},[113,5328,5329],{},"Session truth",[113,5331,5332],{},"Server-side D1",[16,5334,5335],{},"When state lives in the right place, a great deal of synchronization logic disappears naturally. We no longer watch routes to repair a store, and we no longer use localStorage to pretend that authentication is trustworthy.",[78,5337,5339],{"id":5338},"subtraction-does-not-mean-fewer-features","Subtraction does not mean fewer features",[16,5341,5342],{},"Good architectural subtraction does not take anything away from the user. It preserves the complexity budget for the parts people can actually feel:",[40,5344,5345,5348,5351,5354,5357],{},[43,5346,5347],{},"Clearer typography.",[43,5349,5350],{},"A faster first screen.",[43,5352,5353],{},"More reliable authentication.",[43,5355,5356],{},"More comfortable mobile reading.",[43,5358,5359],{},"A Markdown writing workflow that is easier to sustain.",[16,5361,5362],{},"Nuxt 4 provides more than enough capability. A mature project is not one that uses all of it, but one that knows which capabilities it can leave unused for now.",[2470,5364,5365],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}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-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}",{"title":151,"searchDepth":413,"depth":413,"links":5367},[5368,5369,5370,5371,5372,5373],{"id":4999,"depth":400,"text":5000},{"id":5067,"depth":400,"text":5068},{"id":5221,"depth":400,"text":5222},{"id":5239,"depth":400,"text":5240},{"id":5262,"depth":400,"text":5263},{"id":5338,"depth":400,"text":5339},"A content site does not need complex frontend state. Let routing, server rendering, and content queries each return to what they do best.",{"src":5376,"alt":5377,"caption":5378,"width":2514,"height":2515},"\u002Fmedia\u002Fposts\u002Fnuxt-4-architecture-subtraction\u002Fcover-v1.webp","Complex modules gradually converging into a clear and compact Nuxt application core","Using architectural subtraction to create clearer boundaries",{},"\u002Fblog\u002Fnuxt-4-architecture-subtraction","2026-07-16",{"title":4985,"description":5374},"blog\u002Fnuxt-4-architecture-subtraction",[5385,4256,5386],"Nuxt","Architecture","uaj3FaI4QruRhMdDn2xZBaLeLZ-4BB8Og4XAu_PB4uA",{"id":5389,"title":5390,"accent":4263,"body":5391,"description":5743,"draft":2507,"experience":2508,"extension":2509,"featured":2507,"image":5744,"meta":5748,"navigation":422,"path":5749,"publishedAt":5750,"readingTime":475,"seo":5751,"stem":5752,"tags":5753,"updatedAt":2508,"video":2508,"__hash__":5755,"contentLocale":5,"isFallback":2507},"posts_en\u002Fblog\u002Fobservable-ai-systems.md","AI Observability Is More Than a Token Bill",{"type":13,"value":5392,"toc":5730},[5393,5396,5401,5404,5408,5411,5415,5418,5422,5425,5429,5432,5436,5439,5442,5446,5453,5567,5570,5573,5590,5594,5597,5600,5654,5657,5661,5664,5681,5684,5688,5691,5694,5708,5711,5715,5718,5724,5727],[16,5394,5395],{},"When a traditional service fails, we usually inspect status codes, latency, and stack traces. Those metrics still matter when an AI application fails, but they are often not enough to answer the most important question:",[16,5397,5398],{},[31,5399,5400],{},"Why did the model make this choice?",[16,5402,5403],{},"The answer does not live in a single log entry. It lives across the entire path from the user’s goal and context assembly to model decisions, tool execution, and the final result.",[78,5405,5407],{"id":5406},"first-distinguish-four-kinds-of-failure","First, distinguish four kinds of failure",[16,5409,5410],{},"If every failure is recorded as “the model performed poorly,” the team cannot take specific action. At minimum, we should distinguish:",[184,5412,5414],{"id":5413},"_1-input-failure","1. Input failure",[16,5416,5417],{},"The user’s goal is incomplete, or the system did not receive the context required to finish the task. Upgrading the model rarely helps here.",[184,5419,5421],{"id":5420},"_2-reasoning-failure","2. Reasoning failure",[16,5423,5424],{},"The context is correct, but the model chooses the wrong path, misses a constraint, or reaches a conclusion too early.",[184,5426,5428],{"id":5427},"_3-tool-failure","3. Tool failure",[16,5430,5431],{},"The model makes the right choice, but execution fails because of permissions, networking, parameters, or external state.",[184,5433,5435],{"id":5434},"_4-delivery-failure","4. Delivery failure",[16,5437,5438],{},"The internal work is complete, but the final response does not explain the result, limitations, or next step clearly.",[16,5440,5441],{},"These four problems need different owners: product, prompt design, platform engineering, or interaction design. The first value of observability is returning a problem to the right person.",[78,5443,5445],{"id":5444},"every-run-needs-a-shared-trace","Every run needs a shared trace",[16,5447,5448,5449,5452],{},"Each user request should have a stable ",[20,5450,5451],{},"trace_id"," that connects model calls and tool execution:",[145,5454,5456],{"className":376,"code":5455,"language":378,"meta":151,"style":151},"type AgentEvent = {\n  traceId: string\n  spanId: string\n  parentSpanId?: string\n  type: 'model' | 'tool' | 'handoff' | 'result'\n  startedAt: string\n  durationMs?: number\n  status: 'running' | 'ok' | 'error'\n}\n",[20,5457,5458,5470,5479,5488,5498,5524,5533,5543,5563],{"__ignoreMap":151},[382,5459,5460,5463,5466,5468],{"class":384,"line":385},[382,5461,5462],{"class":388},"type",[382,5464,5465],{"class":392}," AgentEvent",[382,5467,484],{"class":388},[382,5469,397],{"class":396},[382,5471,5472,5475,5477],{"class":384,"line":400},[382,5473,5474],{"class":403},"  traceId",[382,5476,373],{"class":388},[382,5478,410],{"class":409},[382,5480,5481,5484,5486],{"class":384,"line":413},[382,5482,5483],{"class":403},"  spanId",[382,5485,373],{"class":388},[382,5487,410],{"class":409},[382,5489,5490,5493,5496],{"class":384,"line":419},[382,5491,5492],{"class":403},"  parentSpanId",[382,5494,5495],{"class":388},"?:",[382,5497,410],{"class":409},[382,5499,5500,5503,5505,5508,5511,5514,5516,5519,5521],{"class":384,"line":426},[382,5501,5502],{"class":403},"  type",[382,5504,373],{"class":388},[382,5506,5507],{"class":514}," 'model'",[382,5509,5510],{"class":388}," |",[382,5512,5513],{"class":514}," 'tool'",[382,5515,5510],{"class":388},[382,5517,5518],{"class":514}," 'handoff'",[382,5520,5510],{"class":388},[382,5522,5523],{"class":514}," 'result'\n",[382,5525,5526,5529,5531],{"class":384,"line":437},[382,5527,5528],{"class":403},"  startedAt",[382,5530,373],{"class":388},[382,5532,410],{"class":409},[382,5534,5535,5538,5540],{"class":384,"line":475},[382,5536,5537],{"class":403},"  durationMs",[382,5539,5495],{"class":388},[382,5541,5542],{"class":409}," number\n",[382,5544,5545,5548,5550,5553,5555,5558,5560],{"class":384,"line":496},[382,5546,5547],{"class":403},"  status",[382,5549,373],{"class":388},[382,5551,5552],{"class":514}," 'running'",[382,5554,5510],{"class":388},[382,5556,5557],{"class":514}," 'ok'",[382,5559,5510],{"class":388},[382,5561,5562],{"class":514}," 'error'\n",[382,5564,5565],{"class":384,"line":527},[382,5566,416],{"class":396},[16,5568,5569],{},"A tool call is the external expression of a model decision. Recording the tool output without the reason for the call loses the decision context. Saving the prompt without the tool result makes it impossible to know whether execution matched the plan.",[16,5571,5572],{},"Observation does not mean unlimited collection. Raw prompts may contain personal information, credentials, or business data. A safer approach is to:",[40,5574,5575,5578,5581,5584,5587],{},[43,5576,5577],{},"Record structured metadata by default.",[43,5579,5580],{},"Redact content at the field level.",[43,5582,5583],{},"Give debugging samples a short retention period.",[43,5585,5586],{},"Let users and businesses explicitly opt out of training or analysis.",[43,5588,5589],{},"Never store secrets or complete identity data in logs.",[78,5591,5593],{"id":5592},"quality-metrics-must-stay-close-to-the-task","Quality metrics must stay close to the task",[16,5595,5596],{},"Tokens, latency, and model error rates are platform metrics. They cannot tell us whether the user received the result they needed.",[16,5598,5599],{},"More meaningful metrics for a code-repair agent might include:",[86,5601,5602,5612],{},[89,5603,5604],{},[92,5605,5606,5609],{},[95,5607,5608],{},"Layer",[95,5610,5611],{},"Example metric",[108,5613,5614,5622,5630,5638,5646],{},[92,5615,5616,5619],{},[113,5617,5618],{},"Outcome",[113,5620,5621],{},"Did the affected tests pass?",[92,5623,5624,5627],{},[113,5625,5626],{},"Process",[113,5628,5629],{},"Were the relevant constraint files read?",[92,5631,5632,5635],{},[113,5633,5634],{},"Safety",[113,5636,5637],{},"Were files outside the task scope modified?",[92,5639,5640,5643],{},[113,5641,5642],{},"Efficiency",[113,5644,5645],{},"Useful tool calls \u002F total tool calls",[92,5647,5648,5651],{},[113,5649,5650],{},"Delivery",[113,5652,5653],{},"Was a verifiable file or commit provided?",[16,5655,5656],{},"“Useful tool calls” matter more than raw call count. One accurate search can replace ten speculative reads.",[78,5658,5660],{"id":5659},"turn-evaluation-into-an-everyday-feedback-loop","Turn evaluation into an everyday feedback loop",[16,5662,5663],{},"Offline benchmarks can compare model versions, but they cannot cover your real workflow. A sustainable evaluation set comes from high-value production samples:",[337,5665,5666,5669,5672,5675,5678],{},[43,5667,5668],{},"Collect runs that are clearly successful or clearly unsuccessful.",[43,5670,5671],{},"Remove private information while preserving the decision structure.",[43,5673,5674],{},"Label the type of failure instead of assigning only one overall score.",[43,5676,5677],{},"Rerun the cases when prompts, models, or tools change.",[43,5679,5680],{},"Compare result quality, cost, and latency together.",[16,5682,5683],{},"An evaluation set does not need to be large at the beginning. Twenty cases with clear boundaries and stable reproduction usually guide engineering better than two thousand samples with uncertain origins.",[78,5685,5687],{"id":5686},"observability-for-people","Observability for people",[16,5689,5690],{},"The final layer of observation is not on a dashboard; it is in the product interface.",[16,5692,5693],{},"When an agent is reading files, waiting for approval, or deploying a release, the user needs to know which stage it has reached. A good status update does not dump internal reasoning. It explains:",[40,5695,5696,5699,5702,5705],{},[43,5697,5698],{},"What is being completed now.",[43,5700,5701],{},"What has already been confirmed.",[43,5703,5704],{},"Whether the user needs to take action.",[43,5706,5707],{},"Which risks remain open.",[16,5709,5710],{},"This visibility directly changes how reliable the system feels. A task that takes tens of seconds but shows clear progress often inspires more confidence than an opaque result that appears suddenly after ten seconds.",[78,5712,5714],{"id":5713},"explainable-does-not-mean-exposing-everything","Explainable does not mean exposing everything",[16,5716,5717],{},"An AI system does not need to show every internal token to the user, and it should not copy sensitive context into logs. What it needs is a sufficiently clear causal chain:",[145,5719,5722],{"className":5720,"code":5721,"language":150,"meta":151},[148],"User goal\n  → which context was used\n  → which tool was selected\n  → what status the tool returned\n  → what result was ultimately delivered\n",[20,5723,5721],{"__ignoreMap":151},[16,5725,5726],{},"When that chain can be queried, compared, and reproduced, a team can turn “occasionally brilliant” into “reliably trustworthy.”",[2470,5728,5729],{},"html pre.shiki code .sOPea, html code.shiki .sOPea{--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .sFR8T, html code.shiki .sFR8T{--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .suv1-, html code.shiki .suv1-{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .s-3mD, html code.shiki .s-3mD{--shiki-default:#FFAB70;--shiki-dark:#FFAB70}html pre.shiki code .s8ozJ, html code.shiki .s8ozJ{--shiki-default:#79B8FF;--shiki-dark:#79B8FF}html pre.shiki code .s4wv1, html code.shiki .s4wv1{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}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":151,"searchDepth":413,"depth":413,"links":5731},[5732,5738,5739,5740,5741,5742],{"id":5406,"depth":400,"text":5407,"children":5733},[5734,5735,5736,5737],{"id":5413,"depth":413,"text":5414},{"id":5420,"depth":413,"text":5421},{"id":5427,"depth":413,"text":5428},{"id":5434,"depth":413,"text":5435},{"id":5444,"depth":400,"text":5445},{"id":5592,"depth":400,"text":5593},{"id":5659,"depth":400,"text":5660},{"id":5686,"depth":400,"text":5687},{"id":5713,"depth":400,"text":5714},"From prompts and tool calls to user-visible outcomes, an AI application needs an observation chain that can explain why something happened.",{"src":5745,"alt":5746,"caption":5747,"width":2514,"height":2515},"\u002Fmedia\u002Fposts\u002Fobservable-ai-systems\u002Fcover-v1.webp","An AI request forming a complete observation chain from input through model and tool nodes","An explainable trace from prompt to outcome",{},"\u002Fblog\u002Fobservable-ai-systems","2026-07-09",{"title":5390,"description":5743},"blog\u002Fobservable-ai-systems",[4521,5754,4980],"Observability","iJ9YNzOb3tfjmbEE34Jj6qbr7KZ8jwBfwlwAkRlB_zE",1785385297791]