[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"docs-navigation":3,"docs-page-email":166},[4,38,57,65,114,138,157],{"id":5,"group":6,"pages":7},"get-started","Get started",[8,13,18,23,28,33],{"path":9,"title":10,"description":11,"navTitle":10,"group":6,"order":12},"\u002Fdocs\u002Fgetting-started","Getting started","Create your app from the kit, run it locally with no credentials, make yourself superadmin and find where your own code goes.",1,{"path":14,"title":15,"description":16,"navTitle":15,"group":6,"order":17},"\u002Fdocs\u002Flayout","Project structure","Where code lives by where it runs, and which server\u002F directories turn a new file into a route, a job or a provider.",2,{"path":19,"title":20,"description":21,"navTitle":20,"group":6,"order":22},"\u002Fdocs\u002Fconfiguration","Configuration","starter.config.ts switches each part of the kit and its providers, environment variables hold the secrets: every key and what it changes.",3,{"path":24,"title":25,"description":26,"navTitle":25,"group":6,"order":27},"\u002Fdocs\u002Ftemplate","Updating from the kit","Keep the kit as a git remote and take each update as one commit you review, with pnpm template:update, then resolve what conflicts.",4,{"path":29,"title":30,"description":31,"navTitle":30,"group":6,"order":32},"\u002Fdocs\u002Fdeployment","Deployment","Ship the app as one Cloudflare Worker: create the resources, set the secrets, deploy main from GitHub Actions or your machine.",5,{"path":34,"title":35,"description":36,"navTitle":35,"group":6,"order":37},"\u002Fdocs\u002Fmcp","MCP server","Connect Claude Code, Cursor or VS Code to https:\u002F\u002Fnuxter.app\u002Fmcp, so your agent searches and reads these docs and their design notes.",6,{"id":39,"group":40,"pages":41},"accounts","Accounts",[42,47,52],{"path":43,"title":44,"description":45,"navTitle":44,"group":40,"order":46},"\u002Fdocs\u002Fauth","Auth","Pick the sign-in methods, add an OAuth provider, protect a route, and handle sessions, second factors and account deletion.",7,{"path":48,"title":49,"description":50,"navTitle":49,"group":40,"order":51},"\u002Fdocs\u002Ftenancy","Organizations","Scope routes and pages to an organization, and work with its members, roles, seats, invitations and a second level such as branches.",8,{"path":53,"title":54,"description":55,"navTitle":54,"group":40,"order":56},"\u002Fdocs\u002Fpermissions","Roles and permissions","Routes check permissions that an account’s role or its organization’s plan grants: checking one, adding a role, and the staff tools.",9,{"id":58,"group":59,"pages":60},"payments","Payments",[61],{"path":62,"title":59,"description":63,"navTitle":59,"group":59,"order":64},"\u002Fdocs\u002Fpayments","Sell plans through Stripe, Polar, Lemon Squeezy or Creem: define plans, connect a provider, gate features, trials, coupons and refunds.",10,{"id":66,"group":67,"pages":68},"building-blocks","Building blocks",[69,74,79,84,89,94,99,104,109],{"path":70,"title":71,"description":72,"navTitle":71,"group":67,"order":73},"\u002Fdocs\u002Fdatabase","Database","Postgres through Drizzle, on Hyperdrive in production and PGlite locally: add a table, query, transactions, migrations and seed data.",11,{"path":75,"title":76,"description":77,"navTitle":76,"group":67,"order":78},"\u002Fdocs\u002Fbackground-jobs","Background jobs","Run work after the response on Cloudflare Queues with retries: add and enqueue a job, run jobs locally, and schedule cron tasks.",12,{"path":80,"title":81,"description":82,"navTitle":81,"group":67,"order":83},"\u002Fdocs\u002Fevents","Events","React to what the kit does by adding a file to server\u002Flisteners\u002F, fire events of your own, and look up every event’s payload.",13,{"path":85,"title":86,"description":87,"navTitle":86,"group":67,"order":88},"\u002Fdocs\u002Femail","Email","Set up sending, send your own mail with deliverEmail, change the templates and wording, handle unsubscribes and add a provider.",14,{"path":90,"title":91,"description":92,"navTitle":91,"group":67,"order":93},"\u002Fdocs\u002Fcontact","Contact","A \u002Fcontact page that mails you a message or shows your address after a Turnstile check: choose the channels, set the address, know the limits.",15,{"path":95,"title":96,"description":97,"navTitle":96,"group":67,"order":98},"\u002Fdocs\u002Fnotifications","Notifications","The dashboard bell: add a notification type, send one with notify(), and how categories, opt-outs and retention apply.",16,{"path":100,"title":101,"description":102,"navTitle":101,"group":67,"order":103},"\u002Fdocs\u002Fstorage","Storage","Uploads in R2 with one policy per purpose: add a purpose, accept an upload, replace or delete a file, and serve it to its owner.",17,{"path":105,"title":106,"description":107,"navTitle":106,"group":67,"order":108},"\u002Fdocs\u002Ffeature-flags","Feature flags","Declare typed flags in starter.config.ts, read them with flag() or useFlag(), roll them out by rule or percentage, and add a provider.",18,{"path":110,"title":111,"description":112,"navTitle":111,"group":67,"order":113},"\u002Fdocs\u002Fai","AI","Call models from server code by role: text, images, speech, transcription, embeddings and video, with provider fallbacks.",19,{"id":115,"group":116,"pages":117},"web","Web",[118,123,128,133],{"path":119,"title":120,"description":121,"navTitle":120,"group":116,"order":122},"\u002Fdocs\u002Fsecurity-headers","Security headers","The Content-Security-Policy and hardening headers on every response: allow another origin, keep tokens out of Referer, accept cross-site posts.",20,{"path":124,"title":125,"description":126,"navTitle":125,"group":116,"order":127},"\u002Fdocs\u002Frate-limiting","Rate limiting","Cap how often a caller hits a route with requireRateLimit, count only failures, space out sends, and protect the zone.",21,{"path":129,"title":130,"description":131,"navTitle":130,"group":116,"order":132},"\u002Fdocs\u002Fanalytics","Analytics","Product analytics and ad conversions sent from the server with no browser script: add an event, connect a provider, and handle consent.",22,{"path":134,"title":135,"description":136,"navTitle":135,"group":116,"order":137},"\u002Fdocs\u002Fobservability","Observability","Find a failed request by its id, send faults to Sentry or Better Stack, report an error you caught, and change the error page.",23,{"id":139,"group":140,"pages":141},"content-and-look","Content and look",[142,147,152],{"path":143,"title":144,"description":145,"navTitle":144,"group":140,"order":146},"\u002Fdocs\u002Fblog","Blog","Markdown posts with an RSS feed per language, and workflows that draft posts with an AI model and open each one as a pull request.",24,{"path":148,"title":149,"description":150,"navTitle":149,"group":140,"order":151},"\u002Fdocs\u002Flanguages","Languages","Add or remove a language, write copy in the locale files, and write code, refusals and links that follow the reader’s language.",25,{"path":153,"title":154,"description":155,"navTitle":154,"group":140,"order":156},"\u002Fdocs\u002Ftheme","Theme","Change the brand colour and font, and build dashboard sections, list pages, loading placeholders and charts that match the kit.",26,{"id":158,"group":159,"pages":160},"quality","Quality",[161],{"path":162,"title":163,"description":164,"navTitle":163,"group":159,"order":165},"\u002Fdocs\u002Ftesting","Testing","Unit tests in Node, component tests in a real Nuxt app and Playwright end to end: test a route or a component, and what CI runs.",27,{"page":167,"surround":1862,"linksToKit":543},{"path":85,"title":86,"description":87,"navTitle":86,"group":67,"seo":168,"body":170},{"title":169,"description":87},"Send email from Nuxt with Cloudflare or Resend",{"type":171,"value":172,"toc":1849},"minimark",[173,177,186,191,296,300,307,372,381,440,477,491,495,513,517,520,606,670,676,695,699,717,742,761,765,789,813,835,838,874,881,885,919,930,934,949,1462,1465,1524,1550,1586,1590,1612,1615,1781,1785,1845],[174,175,86],"h1",{"id":176},"email",[178,179,180,181,185],"p",{},"The kit sends its own account mail (sign-in links, password resets, address\nverification, security notices, organization invitations) and gives you\n",[182,183,184],"code",{},"deliverEmail"," for mail of your own. You work here when you set up sending for\na deploy, change how the mails look or read, send product or marketing mail, or\nplug in another provider.",[187,188,190],"h2",{"id":189},"how-it-works","How it works",[192,193,194,255,273,280],"ol",{},[195,196,197,198,201,202,205,206,205,209,205,212,215,216,219,220,223,224,227,228,231,232,235,236,246,247,254],"li",{},"A mail is an ",[182,199,200],{},"EmailMessage",": ",[182,203,204],{},"to",", ",[182,207,208],{},"subject",[182,210,211],{},"html",[182,213,214],{},"text"," and a\n",[182,217,218],{},"category",". Optional ",[182,221,222],{},"replyTo"," replaces ",[182,225,226],{},"NUXT_EMAIL_REPLY_TO"," on that mail\nalone, and ",[182,229,230],{},"logSubject"," is logged in place of a subject that holds a\nsender's own words. The kit's own mails come from ",[182,233,234],{},"renderEmail",", which fills\na compiled template from ",[237,238,243],"a",{"href":239,"rel":240,"title":242},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Ftree\u002Fmain\u002Femails",[241],"nofollow","Licensees only",[182,244,245],{},"emails\u002F"," with localized copy from\n",[237,248,251],{"href":249,"rel":250,"title":242},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Femail\u002Fcopy.ts",[241],[182,252,253],{},"server\u002Femail\u002Fcopy.ts",".",[195,256,257,260,261,268,269,272],{},[182,258,259],{},"deliverEmail(event, message)"," in\n",[237,262,265],{"href":263,"rel":264,"title":242},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Femail\u002Findex.ts",[241],[182,266,267],{},"server\u002Femail\u002Findex.ts"," checks the category. A\nproduct or marketing mail goes out only to an account that still accepts\nthat category, and gets ",[182,270,271],{},"List-Unsubscribe"," headers.",[195,274,275,276,279],{},"It hands the mail to each configured provider in ",[182,277,278],{},"email.providers"," order. A\nfailure the provider marks retryable gets one more try on the same\nprovider, then the next provider gets the mail.",[195,281,282,283,285,286,288,289,291,292,295],{},"When every provider refuses and at least one failure was transient, the\nmail goes to the ",[182,284,176],{}," job on the queue (",[237,287,76],{"href":75},").\nOtherwise ",[182,290,184],{}," throws a 502. Pass ",[182,293,294],{},"{ queueRetry: false }"," as its\nthird argument to get the 502 instead of the queue, as the contact form\ndoes, so a mail never outlives the request that sent it.",[187,297,299],{"id":298},"set-up-sending","Set up sending",[178,301,302,303,306],{},"A provider counts as configured when ",[182,304,305],{},"NUXT_EMAIL_FROM"," is set and the provider\nhas what it needs:",[308,309,310,326],"table",{},[311,312,313],"thead",{},[314,315,316,320,323],"tr",{},[317,318,319],"th",{},"Provider",[317,321,322],{},"Needs",[317,324,325],{},"Setup",[327,328,329,357],"tbody",{},[314,330,331,337,351],{},[332,333,334],"td",{},[182,335,336],{},"cloudflare",[332,338,339,340,343,344,347,348],{},"The ",[182,341,342],{},"send_email"," binding named ",[182,345,346],{},"EMAIL"," in ",[182,349,350],{},"wrangler.jsonc",[332,352,353,354],{},"Onboard the sending domain once: ",[182,355,356],{},"pnpm exec wrangler email sending enable \u003Cdomain>",[314,358,359,364,369],{},[332,360,361],{},[182,362,363],{},"resend",[332,365,366],{},[182,367,368],{},"NUXT_EMAIL_RESEND_API_KEY",[332,370,371],{},"resend.com, API Keys. Verify the sending domain under Domains first",[178,373,374,375,377,378,380],{},"The shipped config lists ",[182,376,336],{}," alone, and ",[182,379,350],{}," already\ndeclares its binding. For Resend, or Resend as a fallback:",[382,383,388],"pre",{"className":384,"code":385,"language":386,"meta":387,"style":387},"language-ts shiki shiki-themes github-light github-light github-dark","email: {\n  enabled: true,\n  providers: ['cloudflare', 'resend'],\n},\n","ts","",[182,389,390,401,415,435],{"__ignoreMap":387},[391,392,394,397],"span",{"class":393,"line":12},"line",[391,395,176],{"class":396},"snPdu",[391,398,400],{"class":399},"sxrX7",": {\n",[391,402,403,406,408,412],{"class":393,"line":17},[391,404,405],{"class":396},"  enabled",[391,407,201],{"class":399},[391,409,411],{"class":410},"sBjJW","true",[391,413,414],{"class":399},",\n",[391,416,417,420,423,427,429,432],{"class":393,"line":22},[391,418,419],{"class":396},"  providers",[391,421,422],{"class":399},": [",[391,424,426],{"class":425},"sIIMD","'cloudflare'",[391,428,205],{"class":399},[391,430,431],{"class":425},"'resend'",[391,433,434],{"class":399},"],\n",[391,436,437],{"class":393,"line":27},[391,438,439],{"class":399},"},\n",[382,441,445],{"className":442,"code":443,"language":444,"meta":387,"style":387},"language-sh shiki shiki-themes github-light github-light github-dark","NUXT_EMAIL_FROM=hello@example.com\nNUXT_EMAIL_FROM_NAME=Example\nNUXT_EMAIL_RESEND_API_KEY=re_...\n","sh",[182,446,447,458,468],{"__ignoreMap":387},[391,448,449,451,455],{"class":393,"line":12},[391,450,305],{"class":399},[391,452,454],{"class":453},"s8jYJ","=",[391,456,457],{"class":425},"hello@example.com\n",[391,459,460,463,465],{"class":393,"line":17},[391,461,462],{"class":399},"NUXT_EMAIL_FROM_NAME",[391,464,454],{"class":453},[391,466,467],{"class":425},"Example\n",[391,469,470,472,474],{"class":393,"line":22},[391,471,368],{"class":399},[391,473,454],{"class":453},[391,475,476],{"class":425},"re_...\n",[178,478,479,480,483,484,486,487,490],{},"Every mailed link is built from ",[182,481,482],{},"NUXT_SITE_URL",", so a deploy needs that too\n(",[237,485,30],{"href":29},"). Outside ",[182,488,489],{},"pnpm dev",", the Worker logs at boot\nwhich of these variables are missing, and a send with no configured provider\nanswers 501.",[187,492,494],{"id":493},"mail-in-local-development","Mail in local development",[178,496,497,498,500,501,503,504,506,507,510,511,254],{},"With ",[182,499,305],{}," unset, ",[182,502,489],{}," sends nothing and prints each mail's\nrecipient, subject and text to the terminal, links included. Set it and mail\ngoes to the providers. Resend sends for real; the ",[182,505,336],{}," binding runs in\nWrangler's local simulator unless you add ",[182,508,509],{},"\"remote\": true"," to it in\n",[182,512,350],{},[187,514,516],{"id":515},"send-your-own-mail","Send your own mail",[178,518,519],{},"Build the message yourself and state its category:",[382,521,523],{"className":384,"code":522,"language":386,"meta":387,"style":387},"import { deliverEmail } from '#server\u002Femail'\n\nawait deliverEmail(event, {\n  to: user.email,\n  subject: 'Exports are here',\n  html: '\u003Cp>You can now export any report as CSV.\u003C\u002Fp>',\n  text: 'You can now export any report as CSV.',\n  category: 'product',\n})\n",[182,524,525,539,545,556,561,571,581,591,601],{"__ignoreMap":387},[391,526,527,530,533,536],{"class":393,"line":12},[391,528,529],{"class":453},"import",[391,531,532],{"class":399}," { deliverEmail } ",[391,534,535],{"class":453},"from",[391,537,538],{"class":425}," '#server\u002Femail'\n",[391,540,541],{"class":393,"line":17},[391,542,544],{"emptyLinePlaceholder":543},true,"\n",[391,546,547,550,553],{"class":393,"line":22},[391,548,549],{"class":453},"await",[391,551,552],{"class":396}," deliverEmail",[391,554,555],{"class":399},"(event, {\n",[391,557,558],{"class":393,"line":27},[391,559,560],{"class":399},"  to: user.email,\n",[391,562,563,566,569],{"class":393,"line":32},[391,564,565],{"class":399},"  subject: ",[391,567,568],{"class":425},"'Exports are here'",[391,570,414],{"class":399},[391,572,573,576,579],{"class":393,"line":37},[391,574,575],{"class":399},"  html: ",[391,577,578],{"class":425},"'\u003Cp>You can now export any report as CSV.\u003C\u002Fp>'",[391,580,414],{"class":399},[391,582,583,586,589],{"class":393,"line":46},[391,584,585],{"class":399},"  text: ",[391,587,588],{"class":425},"'You can now export any report as CSV.'",[391,590,414],{"class":399},[391,592,593,596,599],{"class":393,"line":51},[391,594,595],{"class":399},"  category: ",[391,597,598],{"class":425},"'product'",[391,600,414],{"class":399},[391,602,603],{"class":393,"line":56},[391,604,605],{"class":399},"})\n",[308,607,608,624],{},[311,609,610],{},[314,611,612,615,618,621],{},[317,613,614],{},"Category",[317,616,617],{},"Use it for",[317,619,620],{},"Refusable",[317,622,623],{},"Unsubscribe link",[327,625,626,641,656],{},[314,627,628,633,636,639],{},[332,629,630],{},[182,631,632],{},"transactional",[332,634,635],{},"Mail the recipient's own action asked for",[332,637,638],{},"no",[332,640,638],{},[314,642,643,648,651,654],{},[332,644,645],{},[182,646,647],{},"product",[332,649,650],{},"News about the product the recipient already uses",[332,652,653],{},"yes",[332,655,653],{},[314,657,658,663,666,668],{},[332,659,660],{},[182,661,662],{},"marketing",[332,664,665],{},"Offers and announcements",[332,667,653],{},[332,669,653],{},[178,671,672,673,675],{},"Product and marketing mail, the elective kinds, is sent only when the address\nbelongs to an account that has not opted out of that category, is not banned\nand is not scheduled for deletion. Otherwise ",[182,674,184],{}," returns without sending and\nwithout an error. The queue asks the same question again before each retry, so\nan opt-out made while the mail waited still wins.",[178,677,678,679,681,682,684,685,687,688,347,691,694],{},"Don't build elective mail with ",[182,680,234],{},": it always marks its mail\n",[182,683,632],{},". For a layout of your own, add a template to ",[182,686,245],{}," and read\nits compiled HTML from ",[182,689,690],{},"emailTemplates",[182,692,693],{},"#email-templates",", keyed by file\nname.",[187,696,698],{"id":697},"unsubscribe-links","Unsubscribe links",[178,700,701,702,704,705,708,709,712,713,716],{},"Every elective mail carries ",[182,703,271],{}," and ",[182,706,707],{},"List-Unsubscribe-Post","\nheaders pointing at ",[182,710,711],{},"\u002Fapi\u002Femail\u002Funsubscribe?token=…",", which gives Gmail and\nOutlook a one-click unsubscribe. The token is one per account and category,\nkept in ",[182,714,715],{},"email_unsubscribe_tokens",", and never expires.",[718,719,720,723,734],"ul",{},[195,721,722],{},"A GET shows a page with one Unsubscribe button, so mail scanners that open\nlinks on delivery unsubscribe nobody. Only a POST records the opt-out.",[195,724,725,726,729,730,733],{},"The opt-out is a timestamp on the user row, ",[182,727,728],{},"email_product_opt_out_at"," or\n",[182,731,732],{},"email_marketing_opt_out_at",". A repeat keeps the first time.",[195,735,736,737,347,740,254],{},"The page copy is ",[182,738,739],{},"UNSUBSCRIBE_COPY",[182,741,253],{},[178,743,744,745,748,749,752,753,704,755,757,758,760],{},"Signed-in users switch both categories on ",[182,746,747],{},"\u002Fdashboard\u002Fsettings",", through\n",[182,750,751],{},"PATCH \u002Fapi\u002Faccount\u002Femail-preferences"," with ",[182,754,647],{},[182,756,662],{}," booleans\n(",[182,759,411],{}," is subscribed).",[187,762,764],{"id":763},"change-how-the-mails-look","Change how the mails look",[178,766,767,768,773,774,779,780,205,782,704,785,788],{},"The layouts are Vue files in ",[237,769,771],{"href":239,"rel":770,"title":242},[241],[182,772,245],{},", written with Maizzle's\ncomponents and Tailwind classes. ",[237,775,778],{"href":776,"rel":777},"https:\u002F\u002Fmaizzle.com",[241],"Maizzle"," compiles them\nto inlined HTML when Nuxt starts and at build, and the Worker ships only the\ncompiled strings. ",[182,781,245],{},[182,783,784],{},"copy.ts",[182,786,787],{},"templates.ts"," belong to the\ntemplate, so a restyle or a rewording conflicts when an update changes the\nsame lines.",[178,790,791,794,795,414,798,205,801,804,805,808,809,812],{},[182,792,793],{},"email-change-notice.vue"," carries the security notices (",[182,796,797],{},"emailChangeNotice",[182,799,800],{},"emailMovedNotice",[182,802,803],{},"passkeyAddedNotice","), ",[182,806,807],{},"org-invitation.vue"," the\ninvitation, and ",[182,810,811],{},"transactional.vue"," every other kind.",[178,814,815,816,818,819,205,822,205,825,704,828,831,832,254],{},"A template holds four single-brace placeholders that ",[182,817,234],{}," fills with\nescaped values: ",[182,820,821],{},"{body}",[182,823,824],{},"{cta}",[182,826,827],{},"{link}",[182,829,830],{},"{ignore}",". Write them exactly\nlike that, not as Vue's ",[182,833,834],{},"{{ }}",[178,836,837],{},"To add a template:",[192,839,840,850,863],{},[195,841,842,843,846,847,849],{},"Create ",[182,844,845],{},"emails\u002F\u003Cname>.vue",". It appears in ",[182,848,693],{}," under its file\nname, typed.",[195,851,852,853,260,856,254],{},"For one of the kit's mails to use it, point that kind at it in\n",[182,854,855],{},"TEMPLATE_FOR_KIND",[237,857,860],{"href":858,"rel":859,"title":242},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Femail\u002Ftemplates.ts",[241],[182,861,862],{},"server\u002Femail\u002Ftemplates.ts",[195,864,865,866,873],{},"Add the name to\n",[237,867,870],{"href":868,"rel":869,"title":242},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Ftest\u002Funit\u002Femail-templates.stub.ts",[241],[182,871,872],{},"test\u002Funit\u002Femail-templates.stub.ts",",\nwhich stands in for the virtual module in unit tests.",[178,875,876,877,880],{},"The build fails when a template compiles to under 512 bytes, and\n",[182,878,879],{},"test\u002Funit\u002Femail-templates.test.ts"," checks every compiled template for a\ndoctype, all four placeholders and inlined styles.",[187,882,884],{"id":883},"change-the-wording","Change the wording",[178,886,887,347,890,895,896,205,898,205,901,704,904,907,908,704,911,914,915,918],{},[182,888,889],{},"EMAIL_COPY",[237,891,893],{"href":249,"rel":892,"title":242},[241],[182,894,253],{}," holds a\n",[182,897,208],{},[182,899,900],{},"body",[182,902,903],{},"cta",[182,905,906],{},"ignore"," per kind and locale. Placeholders such\nas ",[182,909,910],{},"{minutes}",[182,912,913],{},"{date}"," use the same brace syntax as the locale files, and\none with no value is left as written. Mail goes out in the requester's locale;\ninvitations use ",[182,916,917],{},"i18n.defaultLocale",". An unknown locale falls back to English.",[178,920,921,922,925,926,929],{},"When you add a language to ",[182,923,924],{},"i18n.locales",", add it here too.\n",[182,927,928],{},"test\u002Funit\u002Femail-copy.test.ts"," fails until every locale has every kind with\nthe same placeholders as English.",[187,931,933],{"id":932},"add-a-provider","Add a provider",[178,935,936,937,940,941,944,945,948],{},"Drop a file in ",[182,938,939],{},"server\u002Femail\u002Fadapters\u002Fcustom\u002F",". The file name is the provider\nid and the default export is an ",[182,942,943],{},"EmailAdapter",".\n",[182,946,947],{},"server\u002Femail\u002Fadapters\u002Fcustom\u002Fpostmark.ts",":",[382,950,952],{"className":384,"code":951,"language":386,"meta":387,"style":387},"import type { EmailAdapter } from '#server\u002Femail\u002Ftypes'\n\nconst postmark: EmailAdapter = {\n  id: 'postmark',\n\n  async send(message, sender, config) {\n    let res: Response\n\n    try {\n      res = await fetch('https:\u002F\u002Fapi.postmarkapp.com\u002Femail', {\n        method: 'POST',\n        headers: {\n          'X-Postmark-Server-Token': config.key,\n          'Content-Type': 'application\u002Fjson',\n        },\n        body: JSON.stringify({\n          From: sender.fromName\n            ? `${sender.fromName} \u003C${sender.from}>`\n            : sender.from,\n          To: message.to,\n          Subject: message.subject,\n          HtmlBody: message.html,\n          TextBody: message.text,\n          ...(sender.replyTo ? { ReplyTo: sender.replyTo } : {}),\n          Headers: Object.entries(message.headers ?? {}).map(\n            ([Name, Value]) => ({ Name, Value }),\n          ),\n        }),\n      })\n    } catch (error) {\n      return { ok: false, reason: String(error), retryable: true }\n    }\n\n    if (res.ok) return { ok: true }\n\n    return {\n      ok: false,\n      reason: `${res.status} ${await res.text()}`,\n      retryable: res.status === 429 || res.status >= 500,\n    }\n  },\n}\n\nexport default postmark\n",[182,953,954,969,973,992,1002,1006,1034,1047,1051,1058,1079,1089,1094,1102,1114,1119,1135,1140,1167,1175,1180,1185,1190,1195,1214,1237,1259,1264,1270,1276,1288,1314,1320,1325,1343,1348,1356,1366,1402,1428,1433,1439,1445,1450],{"__ignoreMap":387},[391,955,956,958,961,964,966],{"class":393,"line":12},[391,957,529],{"class":453},[391,959,960],{"class":453}," type",[391,962,963],{"class":399}," { EmailAdapter } ",[391,965,535],{"class":453},[391,967,968],{"class":425}," '#server\u002Femail\u002Ftypes'\n",[391,970,971],{"class":393,"line":17},[391,972,544],{"emptyLinePlaceholder":543},[391,974,975,978,981,983,986,989],{"class":393,"line":22},[391,976,977],{"class":453},"const",[391,979,980],{"class":410}," postmark",[391,982,948],{"class":453},[391,984,985],{"class":396}," EmailAdapter",[391,987,988],{"class":453}," =",[391,990,991],{"class":399}," {\n",[391,993,994,997,1000],{"class":393,"line":27},[391,995,996],{"class":399},"  id: ",[391,998,999],{"class":425},"'postmark'",[391,1001,414],{"class":399},[391,1003,1004],{"class":393,"line":32},[391,1005,544],{"emptyLinePlaceholder":543},[391,1007,1008,1011,1014,1017,1021,1023,1026,1028,1031],{"class":393,"line":37},[391,1009,1010],{"class":453},"  async",[391,1012,1013],{"class":396}," send",[391,1015,1016],{"class":399},"(",[391,1018,1020],{"class":1019},"sP4rz","message",[391,1022,205],{"class":399},[391,1024,1025],{"class":1019},"sender",[391,1027,205],{"class":399},[391,1029,1030],{"class":1019},"config",[391,1032,1033],{"class":399},") {\n",[391,1035,1036,1039,1042,1044],{"class":393,"line":46},[391,1037,1038],{"class":453},"    let",[391,1040,1041],{"class":399}," res",[391,1043,948],{"class":453},[391,1045,1046],{"class":396}," Response\n",[391,1048,1049],{"class":393,"line":51},[391,1050,544],{"emptyLinePlaceholder":543},[391,1052,1053,1056],{"class":393,"line":56},[391,1054,1055],{"class":453},"    try",[391,1057,991],{"class":399},[391,1059,1060,1063,1065,1068,1071,1073,1076],{"class":393,"line":64},[391,1061,1062],{"class":399},"      res ",[391,1064,454],{"class":453},[391,1066,1067],{"class":453}," await",[391,1069,1070],{"class":396}," fetch",[391,1072,1016],{"class":399},[391,1074,1075],{"class":425},"'https:\u002F\u002Fapi.postmarkapp.com\u002Femail'",[391,1077,1078],{"class":399},", {\n",[391,1080,1081,1084,1087],{"class":393,"line":73},[391,1082,1083],{"class":399},"        method: ",[391,1085,1086],{"class":425},"'POST'",[391,1088,414],{"class":399},[391,1090,1091],{"class":393,"line":78},[391,1092,1093],{"class":399},"        headers: {\n",[391,1095,1096,1099],{"class":393,"line":83},[391,1097,1098],{"class":425},"          'X-Postmark-Server-Token'",[391,1100,1101],{"class":399},": config.key,\n",[391,1103,1104,1107,1109,1112],{"class":393,"line":88},[391,1105,1106],{"class":425},"          'Content-Type'",[391,1108,201],{"class":399},[391,1110,1111],{"class":425},"'application\u002Fjson'",[391,1113,414],{"class":399},[391,1115,1116],{"class":393,"line":93},[391,1117,1118],{"class":399},"        },\n",[391,1120,1121,1124,1127,1129,1132],{"class":393,"line":98},[391,1122,1123],{"class":399},"        body: ",[391,1125,1126],{"class":410},"JSON",[391,1128,254],{"class":399},[391,1130,1131],{"class":396},"stringify",[391,1133,1134],{"class":399},"({\n",[391,1136,1137],{"class":393,"line":103},[391,1138,1139],{"class":399},"          From: sender.fromName\n",[391,1141,1142,1145,1148,1150,1152,1155,1158,1160,1162,1164],{"class":393,"line":108},[391,1143,1144],{"class":453},"            ?",[391,1146,1147],{"class":425}," `${",[391,1149,1025],{"class":399},[391,1151,254],{"class":425},[391,1153,1154],{"class":399},"fromName",[391,1156,1157],{"class":425},"} \u003C${",[391,1159,1025],{"class":399},[391,1161,254],{"class":425},[391,1163,535],{"class":399},[391,1165,1166],{"class":425},"}>`\n",[391,1168,1169,1172],{"class":393,"line":113},[391,1170,1171],{"class":453},"            :",[391,1173,1174],{"class":399}," sender.from,\n",[391,1176,1177],{"class":393,"line":122},[391,1178,1179],{"class":399},"          To: message.to,\n",[391,1181,1182],{"class":393,"line":127},[391,1183,1184],{"class":399},"          Subject: message.subject,\n",[391,1186,1187],{"class":393,"line":132},[391,1188,1189],{"class":399},"          HtmlBody: message.html,\n",[391,1191,1192],{"class":393,"line":137},[391,1193,1194],{"class":399},"          TextBody: message.text,\n",[391,1196,1197,1200,1203,1206,1209,1211],{"class":393,"line":146},[391,1198,1199],{"class":453},"          ...",[391,1201,1202],{"class":399},"(sender.replyTo ",[391,1204,1205],{"class":453},"?",[391,1207,1208],{"class":399}," { ReplyTo: sender.replyTo } ",[391,1210,948],{"class":453},[391,1212,1213],{"class":399}," {}),\n",[391,1215,1216,1219,1222,1225,1228,1231,1234],{"class":393,"line":151},[391,1217,1218],{"class":399},"          Headers: Object.",[391,1220,1221],{"class":396},"entries",[391,1223,1224],{"class":399},"(message.headers ",[391,1226,1227],{"class":453},"??",[391,1229,1230],{"class":399}," {}).",[391,1232,1233],{"class":396},"map",[391,1235,1236],{"class":399},"(\n",[391,1238,1239,1242,1245,1247,1250,1253,1256],{"class":393,"line":156},[391,1240,1241],{"class":399},"            ([",[391,1243,1244],{"class":1019},"Name",[391,1246,205],{"class":399},[391,1248,1249],{"class":1019},"Value",[391,1251,1252],{"class":399},"]) ",[391,1254,1255],{"class":453},"=>",[391,1257,1258],{"class":399}," ({ Name, Value }),\n",[391,1260,1261],{"class":393,"line":165},[391,1262,1263],{"class":399},"          ),\n",[391,1265,1267],{"class":393,"line":1266},28,[391,1268,1269],{"class":399},"        }),\n",[391,1271,1273],{"class":393,"line":1272},29,[391,1274,1275],{"class":399},"      })\n",[391,1277,1279,1282,1285],{"class":393,"line":1278},30,[391,1280,1281],{"class":399},"    } ",[391,1283,1284],{"class":453},"catch",[391,1286,1287],{"class":399}," (error) {\n",[391,1289,1291,1294,1297,1300,1303,1306,1309,1311],{"class":393,"line":1290},31,[391,1292,1293],{"class":453},"      return",[391,1295,1296],{"class":399}," { ok: ",[391,1298,1299],{"class":410},"false",[391,1301,1302],{"class":399},", reason: ",[391,1304,1305],{"class":396},"String",[391,1307,1308],{"class":399},"(error), retryable: ",[391,1310,411],{"class":410},[391,1312,1313],{"class":399}," }\n",[391,1315,1317],{"class":393,"line":1316},32,[391,1318,1319],{"class":399},"    }\n",[391,1321,1323],{"class":393,"line":1322},33,[391,1324,544],{"emptyLinePlaceholder":543},[391,1326,1328,1331,1334,1337,1339,1341],{"class":393,"line":1327},34,[391,1329,1330],{"class":453},"    if",[391,1332,1333],{"class":399}," (res.ok) ",[391,1335,1336],{"class":453},"return",[391,1338,1296],{"class":399},[391,1340,411],{"class":410},[391,1342,1313],{"class":399},[391,1344,1346],{"class":393,"line":1345},35,[391,1347,544],{"emptyLinePlaceholder":543},[391,1349,1351,1354],{"class":393,"line":1350},36,[391,1352,1353],{"class":453},"    return",[391,1355,991],{"class":399},[391,1357,1359,1362,1364],{"class":393,"line":1358},37,[391,1360,1361],{"class":399},"      ok: ",[391,1363,1299],{"class":410},[391,1365,414],{"class":399},[391,1367,1369,1372,1375,1378,1380,1383,1386,1388,1390,1392,1394,1397,1400],{"class":393,"line":1368},38,[391,1370,1371],{"class":399},"      reason: ",[391,1373,1374],{"class":425},"`${",[391,1376,1377],{"class":399},"res",[391,1379,254],{"class":425},[391,1381,1382],{"class":399},"status",[391,1384,1385],{"class":425},"} ${",[391,1387,549],{"class":453},[391,1389,1041],{"class":399},[391,1391,254],{"class":425},[391,1393,214],{"class":396},[391,1395,1396],{"class":425},"()",[391,1398,1399],{"class":425},"}`",[391,1401,414],{"class":399},[391,1403,1405,1408,1411,1414,1417,1420,1423,1426],{"class":393,"line":1404},39,[391,1406,1407],{"class":399},"      retryable: res.status ",[391,1409,1410],{"class":453},"===",[391,1412,1413],{"class":410}," 429",[391,1415,1416],{"class":453}," ||",[391,1418,1419],{"class":399}," res.status ",[391,1421,1422],{"class":453},">=",[391,1424,1425],{"class":410}," 500",[391,1427,414],{"class":399},[391,1429,1431],{"class":393,"line":1430},40,[391,1432,1319],{"class":399},[391,1434,1436],{"class":393,"line":1435},41,[391,1437,1438],{"class":399},"  },\n",[391,1440,1442],{"class":393,"line":1441},42,[391,1443,1444],{"class":399},"}\n",[391,1446,1448],{"class":393,"line":1447},43,[391,1449,544],{"emptyLinePlaceholder":543},[391,1451,1453,1456,1459],{"class":393,"line":1452},44,[391,1454,1455],{"class":453},"export",[391,1457,1458],{"class":453}," default",[391,1460,1461],{"class":399}," postmark\n",[178,1463,1464],{},"Then list it and name its credential:",[382,1466,1468],{"className":384,"code":1467,"language":386,"meta":387,"style":387},"email: {\n  enabled: true,\n  providers: ['postmark'],\n  adapters: { postmark: { credentials: ['apiKey'] } },\n},\n",[182,1469,1470,1476,1486,1496,1520],{"__ignoreMap":387},[391,1471,1472,1474],{"class":393,"line":12},[391,1473,176],{"class":396},[391,1475,400],{"class":399},[391,1477,1478,1480,1482,1484],{"class":393,"line":17},[391,1479,405],{"class":396},[391,1481,201],{"class":399},[391,1483,411],{"class":410},[391,1485,414],{"class":399},[391,1487,1488,1490,1492,1494],{"class":393,"line":22},[391,1489,419],{"class":396},[391,1491,422],{"class":399},[391,1493,999],{"class":425},[391,1495,434],{"class":399},[391,1497,1498,1501,1504,1507,1509,1512,1514,1517],{"class":393,"line":27},[391,1499,1500],{"class":396},"  adapters",[391,1502,1503],{"class":399},": { ",[391,1505,1506],{"class":396},"postmark",[391,1508,1503],{"class":399},[391,1510,1511],{"class":396},"credentials",[391,1513,422],{"class":399},[391,1515,1516],{"class":425},"'apiKey'",[391,1518,1519],{"class":399},"] } },\n",[391,1521,1522],{"class":393,"line":32},[391,1523,439],{"class":399},[178,1525,1526,1527,1530,1531,1534,1535,1538,1539,1542,1543,1546,1547,254],{},"That reads ",[182,1528,1529],{},"NUXT_EMAIL_POSTMARK_API_KEY",". A credential named ",[182,1532,1533],{},"apiKey"," arrives\nas ",[182,1536,1537],{},"config.key","; every declared credential is also in ",[182,1540,1541],{},"config.credentials",".\nWithout an ",[182,1544,1545],{},"adapters"," entry, the whole block is one JSON variable, such as\n",[182,1548,1549],{},"NUXT_EMAIL_POSTMARK='{\"apiKey\":\"…\"}'",[718,1551,1552,1559,1570,1580],{},[195,1553,1554,1555,1558],{},"Return a result instead of throwing. ",[182,1556,1557],{},"retryable: true"," earns one repeat on\nthe same provider; never set it for a 4xx.",[195,1560,1561,1562,205,1564,1566,1567,1569],{},"The id must be one lowercase word, and not ",[182,1563,535],{},[182,1565,1154],{}," or ",[182,1568,222],{},".\nThe build refuses both.",[195,1571,1572,1573,1566,1576,1579],{},"A file named ",[182,1574,1575],{},"resend.ts",[182,1577,1578],{},"cloudflare.ts"," replaces the shipped adapter,\nand the build logs which file won.",[195,1581,1582,1583,1585],{},"Restart ",[182,1584,489],{}," after adding the file so the id typechecks.",[187,1587,1589],{"id":1588},"turn-email-off","Turn email off",[178,1591,1592,1593,1596,1597,1600,1601,729,1604,1607,1608,1611],{},"Set ",[182,1594,1595],{},"email.enabled: false",". No adapter or template is bundled, and the\npassword-reset routes are not registered. The build refuses it while\n",[182,1598,1599],{},"auth.emailVerification"," (with password sign-in), ",[182,1602,1603],{},"orgs.invitations",[182,1605,1606],{},"account.deletionGraceDays"," still needs mail. With ",[182,1609,1610],{},"auth.primary: 'passwordless'"," the build only warns, and magic-link sign-in then fails.",[187,1613,20],{"id":1614},"configuration",[308,1616,1617,1633],{},[311,1618,1619],{},[314,1620,1621,1624,1627,1630],{},[317,1622,1623],{},"Key or variable",[317,1625,1626],{},"Where",[317,1628,1629],{},"Default",[317,1631,1632],{},"What it does",[327,1634,1635,1654,1672,1689,1704,1717,1730,1743,1760],{},[314,1636,1637,1642,1647,1651],{},[332,1638,1639],{},[182,1640,1641],{},"email.enabled",[332,1643,1644],{},[182,1645,1646],{},"starter.config.ts",[332,1648,1649],{},[182,1650,411],{},[332,1652,1653],{},"Bundles the providers and compiles the templates.",[314,1655,1656,1660,1664,1669],{},[332,1657,1658],{},[182,1659,278],{},[332,1661,1662],{},[182,1663,1646],{},[332,1665,1666],{},[182,1667,1668],{},"['cloudflare']",[332,1670,1671],{},"Providers in delivery order. Later ones only see a failed send.",[314,1673,1674,1679,1683,1686],{},[332,1675,1676],{},[182,1677,1678],{},"email.adapters",[332,1680,1681],{},[182,1682,1646],{},[332,1684,1685],{},"none",[332,1687,1688],{},"Credential names for an adapter you added.",[314,1690,1691,1695,1698,1701],{},[332,1692,1693],{},[182,1694,305],{},[332,1696,1697],{},"env",[332,1699,1700],{},"unset",[332,1702,1703],{},"Sender address. Unset, no provider counts as configured.",[314,1705,1706,1710,1712,1714],{},[332,1707,1708],{},[182,1709,462],{},[332,1711,1697],{},[332,1713,1700],{},[332,1715,1716],{},"Display name inboxes show.",[314,1718,1719,1723,1725,1727],{},[332,1720,1721],{},[182,1722,226],{},[332,1724,1697],{},[332,1726,1700],{},[332,1728,1729],{},"Reply-to address.",[314,1731,1732,1736,1738,1740],{},[332,1733,1734],{},[182,1735,368],{},[332,1737,1697],{},[332,1739,1700],{},[332,1741,1742],{},"Resend API key.",[314,1744,1745,1753,1755,1757],{},[332,1746,1747,205,1750],{},[182,1748,1749],{},"NUXT_EMAIL_\u003CID>_\u003CKEY>",[182,1751,1752],{},"NUXT_EMAIL_\u003CID>",[332,1754,1697],{},[332,1756,1700],{},[332,1758,1759],{},"An added adapter's credentials, one per key or the block as JSON.",[314,1761,1762,1769,1773,1776],{},[332,1763,1764,1766,1767],{},[182,1765,342],{}," binding ",[182,1768,346],{},[332,1770,1771],{},[182,1772,350],{},[332,1774,1775],{},"declared",[332,1777,339,1778,1780],{},[182,1779,336],{}," provider. The build fails without it while listed.",[187,1782,1784],{"id":1783},"good-to-know","Good to know",[718,1786,1787,1802,1808,1813,1823,1837],{},[195,1788,1789,1790,704,1792,1794,1795,1798,1799,1801],{},"Keep ",[182,1791,305],{},[182,1793,226],{}," off ",[182,1796,1797],{},"NUXT_CONTACT_ADDRESS","\n(",[237,1800,91],{"href":90},"). Every mail carries both, sign-in links included,\nand anyone can ask for one. A deployed Worker warns at boot when either\nmatches.",[195,1803,1804,1805,1807],{},"On the Workers Free plan, Cloudflare Email Sending delivers only to\naddresses verified in your account, so sign-up and reset mail reaches nobody\nelse. List ",[182,1806,363],{}," with its key on Free.",[195,1809,1810,1812],{},[182,1811,234],{}," escapes the copy it fills in. HTML you build yourself goes out\nas written, so escape any value a user typed.",[195,1814,1815,1816,1819,1820,254],{},"Cloudflare refuses a header name that is not on its allowlist unless it\nstarts with ",[182,1817,1818],{},"X-",". Name custom headers ",[182,1821,1822],{},"X-…",[195,1824,1825,1826,1829,1830,1833,1834,1836],{},"Put padding on a ",[182,1827,1828],{},"Section"," inside ",[182,1831,1832],{},"\u003CBody>",", never on ",[182,1835,1832],{},": it is 100%\nwide, and padding it makes Gmail scroll sideways.",[195,1838,1839,1840,1842,1843,254],{},"Templates compile when Nuxt starts, so restart ",[182,1841,489],{}," to see an edit to\n",[182,1844,245],{},[1846,1847,1848],"style",{},"html pre.shiki code .snPdu, html code.shiki .snPdu{--shiki-light:#6F42C1;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sxrX7, html code.shiki .sxrX7{--shiki-light:#24292E;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sBjJW, html code.shiki .sBjJW{--shiki-light:#005CC5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sIIMD, html code.shiki .sIIMD{--shiki-light:#032F62;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .s8jYJ, html code.shiki .s8jYJ{--shiki-light:#D73A49;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sP4rz, html code.shiki .sP4rz{--shiki-light:#E36209;--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":387,"searchDepth":17,"depth":17,"links":1850},[1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861],{"id":189,"depth":17,"text":190},{"id":298,"depth":17,"text":299},{"id":493,"depth":17,"text":494},{"id":515,"depth":17,"text":516},{"id":697,"depth":17,"text":698},{"id":763,"depth":17,"text":764},{"id":883,"depth":17,"text":884},{"id":932,"depth":17,"text":933},{"id":1588,"depth":17,"text":1589},{"id":1614,"depth":17,"text":20},{"id":1783,"depth":17,"text":1784},[1863,1864],{"path":80,"title":81,"description":82,"navTitle":81,"group":67,"order":83},{"path":90,"title":91,"description":92,"navTitle":91,"group":67,"order":93}]