[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"docs-navigation":3,"docs-page-events":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":1506,"linksToKit":314},{"path":80,"title":81,"description":82,"navTitle":81,"group":67,"seo":168,"body":170},{"title":169,"description":82},"Server events and listeners in Nuxt",{"type":171,"value":172,"toc":1495},"minimark",[173,177,193,198,264,267,271,282,417,432,440,450,454,471,475,490,599,609,627,631,642,723,737,740,840,864,868,877,999,1024,1028,1038,1286,1316,1319,1356,1360,1365,1424,1428,1491],[174,175,81],"h1",{"id":176},"events",[178,179,180,181,192],"p",{},"The kit fires typed events when something happens: a payment settles, an\naccount is purged, someone joins an organization. You react to one by adding a\nfile to ",[182,183,188],"a",{"href":184,"rel":185,"title":187},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Ftree\u002Fmain\u002Fserver\u002Flisteners",[186],"nofollow","Licensees only",[189,190,191],"code",{},"server\u002Flisteners\u002F",", without editing the code\nthat fired it. You can declare and fire events of your own the same way.",[194,195,197],"h2",{"id":196},"how-it-works","How it works",[199,200,201,209,231,246],"ol",{},[202,203,204,205,208],"li",{},"Kit code calls ",[189,206,207],{},"emit(fromRequest(event), name, payload)"," after the write the\nevent describes has landed.",[202,210,211,212,219,220,222,223,230],{},"When Nuxt starts,\n",[182,213,216],{"href":214,"rel":215,"title":187},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fmodules\u002Flisteners-registry.ts",[186],[189,217,218],{},"modules\u002Flisteners-registry.ts"," collects\nevery file in ",[189,221,191],{},", and\n",[182,224,227],{"href":225,"rel":226,"title":187},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Fplugins\u002Fevents.ts",[186],[189,228,229],{},"server\u002Fplugins\u002Fevents.ts"," registers each on\nNitro's hooks.",[202,232,233,234,237,238,241,242,245],{},"Each listener runs in its own ",[189,235,236],{},"try","\u002F",[189,239,240],{},"catch",". A throw is logged with the\nevent and the listener's file name, then swallowed. It can't stop another\nlistener or fail the request, and ",[189,243,244],{},"emit"," never rejects.",[202,247,248,249,252,253,256,257,260,261,263],{},"A ",[189,250,251],{},"deferred"," listener, the default, runs after the response through\n",[189,254,255],{},"waitUntil",". A ",[189,258,259],{},"sync"," listener is awaited inside ",[189,262,244],{},".",[178,265,266],{},"Nothing on the bus is stored or retried, and the bus never logs a payload.",[194,268,270],{"id":269},"add-a-listener","Add a listener",[178,272,273,274,277,278,281],{},"Create ",[189,275,276],{},"server\u002Flisteners\u002Fcrm-contact.ts"," and default-export ",[189,279,280],{},"defineListener",":",[283,284,289],"pre",{"className":285,"code":286,"language":287,"meta":288,"style":288},"language-ts shiki shiki-themes github-light github-light github-dark","import { defineListener } from '#server\u002Fevents\u002Ftypes'\n\nexport default defineListener({\n  event: 'auth:emailVerified',\n\n  async handle({ userId, email }) {\n    await $fetch('https:\u002F\u002Fcrm.example.com\u002Fcontacts', {\n      method: 'POST',\n      body: { externalId: userId, email },\n    })\n  },\n})\n","ts","",[189,290,291,310,316,331,342,346,370,387,397,402,407,412],{"__ignoreMap":288},[292,293,295,299,303,306],"span",{"class":294,"line":12},"line",[292,296,298],{"class":297},"s8jYJ","import",[292,300,302],{"class":301},"sxrX7"," { defineListener } ",[292,304,305],{"class":297},"from",[292,307,309],{"class":308},"sIIMD"," '#server\u002Fevents\u002Ftypes'\n",[292,311,312],{"class":294,"line":17},[292,313,315],{"emptyLinePlaceholder":314},true,"\n",[292,317,318,321,324,328],{"class":294,"line":22},[292,319,320],{"class":297},"export",[292,322,323],{"class":297}," default",[292,325,327],{"class":326},"snPdu"," defineListener",[292,329,330],{"class":301},"({\n",[292,332,333,336,339],{"class":294,"line":27},[292,334,335],{"class":301},"  event: ",[292,337,338],{"class":308},"'auth:emailVerified'",[292,340,341],{"class":301},",\n",[292,343,344],{"class":294,"line":32},[292,345,315],{"emptyLinePlaceholder":314},[292,347,348,351,354,357,361,364,367],{"class":294,"line":37},[292,349,350],{"class":297},"  async",[292,352,353],{"class":326}," handle",[292,355,356],{"class":301},"({ ",[292,358,360],{"class":359},"sP4rz","userId",[292,362,363],{"class":301},", ",[292,365,366],{"class":359},"email",[292,368,369],{"class":301}," }) {\n",[292,371,372,375,378,381,384],{"class":294,"line":46},[292,373,374],{"class":297},"    await",[292,376,377],{"class":326}," $fetch",[292,379,380],{"class":301},"(",[292,382,383],{"class":308},"'https:\u002F\u002Fcrm.example.com\u002Fcontacts'",[292,385,386],{"class":301},", {\n",[292,388,389,392,395],{"class":294,"line":51},[292,390,391],{"class":301},"      method: ",[292,393,394],{"class":308},"'POST'",[292,396,341],{"class":301},[292,398,399],{"class":294,"line":56},[292,400,401],{"class":301},"      body: { externalId: userId, email },\n",[292,403,404],{"class":294,"line":64},[292,405,406],{"class":301},"    })\n",[292,408,409],{"class":294,"line":73},[292,410,411],{"class":301},"  },\n",[292,413,414],{"class":294,"line":78},[292,415,416],{"class":301},"})\n",[178,418,419,420,423,424,427,428,431],{},"The payload type follows from ",[189,421,422],{},"event",", so there is no type argument and no\nlist to register the file in. The file name is the listener's id and must be\nlowercase kebab-case with an optional numeric prefix (",[189,425,426],{},"00-billing-email","), or\nthe build fails. The registry is generated when Nuxt starts, so restart\n",[189,429,430],{},"pnpm dev"," after you add, rename or delete a listener. In development the\nserver prints each one as it registers:",[283,433,438],{"className":434,"code":436,"language":437,"meta":288},[435],"language-text","[events] crm-contact → auth:emailVerified (deferred)\n","text",[189,439,436],{"__ignoreMap":288},[178,441,442,443,446,447,449],{},"A listener's failure is never retried. When the work has to survive an outage,\nhave the listener ",[189,444,445],{},"enqueue()"," a job instead (",[182,448,76],{"href":75},").",[194,451,453],{"id":452},"what-a-listener-gets","What a listener gets",[178,455,456,457,459,460,363,463,466,467,470],{},"The second argument's ",[189,458,422],{}," is built from the Worker's bindings and carries\nnothing else. ",[189,461,462],{},"useDrizzle(event)",[189,464,465],{},"useRuntimeConfig(event)"," and\n",[189,468,469],{},"enqueue(event, …)"," work; headers, cookies and the session don't exist on it.\nAnything a listener needs from the original request has to be in the payload.",[194,472,474],{"id":473},"choose-a-mode","Choose a mode",[178,476,477,478,480,481,483,484,486,487,489],{},"A listener is ",[189,479,251],{}," unless it says otherwise. Deferred work is handed to\n",[189,482,255],{},", so the response doesn't wait for it, and it is lost if the isolate\nis evicted or runs out of time after the response. A ",[189,485,259],{}," listener is awaited\ninside ",[189,488,244],{},", so the request that fired the event waits for it:",[283,491,493],{"className":285,"code":492,"language":287,"meta":288,"style":288},"export default defineListener({\n  event: 'org:member:added',\n  mode: 'sync',\n\n  async handle({ orgId, userId }, { event }) {\n    await useDrizzle(event)\n      .insert(tables.memberProfiles)\n      .values({ orgId, userId })\n      .onConflictDoNothing()\n  },\n})\n",[189,494,495,505,514,524,528,550,560,571,581,591,595],{"__ignoreMap":288},[292,496,497,499,501,503],{"class":294,"line":12},[292,498,320],{"class":297},[292,500,323],{"class":297},[292,502,327],{"class":326},[292,504,330],{"class":301},[292,506,507,509,512],{"class":294,"line":17},[292,508,335],{"class":301},[292,510,511],{"class":308},"'org:member:added'",[292,513,341],{"class":301},[292,515,516,519,522],{"class":294,"line":22},[292,517,518],{"class":301},"  mode: ",[292,520,521],{"class":308},"'sync'",[292,523,341],{"class":301},[292,525,526],{"class":294,"line":27},[292,527,315],{"emptyLinePlaceholder":314},[292,529,530,532,534,536,539,541,543,546,548],{"class":294,"line":32},[292,531,350],{"class":297},[292,533,353],{"class":326},[292,535,356],{"class":301},[292,537,538],{"class":359},"orgId",[292,540,363],{"class":301},[292,542,360],{"class":359},[292,544,545],{"class":301}," }, { ",[292,547,422],{"class":359},[292,549,369],{"class":301},[292,551,552,554,557],{"class":294,"line":37},[292,553,374],{"class":297},[292,555,556],{"class":326}," useDrizzle",[292,558,559],{"class":301},"(event)\n",[292,561,562,565,568],{"class":294,"line":46},[292,563,564],{"class":301},"      .",[292,566,567],{"class":326},"insert",[292,569,570],{"class":301},"(tables.memberProfiles)\n",[292,572,573,575,578],{"class":294,"line":51},[292,574,564],{"class":301},[292,576,577],{"class":326},"values",[292,579,580],{"class":301},"({ orgId, userId })\n",[292,582,583,585,588],{"class":294,"line":56},[292,584,564],{"class":301},[292,586,587],{"class":326},"onConflictDoNothing",[292,589,590],{"class":301},"()\n",[292,592,593],{"class":294,"line":64},[292,594,411],{"class":301},[292,596,597],{"class":294,"line":73},[292,598,416],{"class":301},[178,600,601,602,604,605,608],{},"Use ",[189,603,259],{}," only when the request that fired the event reads the listener's\nresult back, as the members page would read ",[189,606,607],{},"memberProfiles"," here.",[178,610,611,612,615,616,619,620,623,624,626],{},"Listeners for one event run one after another in file-name order, sorted\nnumerically, so ",[189,613,614],{},"00-"," runs first and ",[189,617,618],{},"10-"," sorts after ",[189,621,622],{},"2-",". That order holds\nwithin a mode only: the deferred batch can start before a ",[189,625,259],{}," listener whose\nname sorts earlier. Nothing orders listeners across different events.",[194,628,630],{"id":629},"fire-your-own-event","Fire your own event",[178,632,633,634,637,638,641],{},"Declare it by augmenting ",[189,635,636],{},"BusEvents"," in ",[189,639,640],{},"server\u002Fevents\u002Fcustom.d.ts",", a file the\ntemplate never writes:",[283,643,645],{"className":285,"code":644,"language":287,"meta":288,"style":288},"declare module '#server\u002Fevents\u002Ftypes' {\n  interface BusEvents {\n    'app:project:archived': { projectId: number; userId: number }\n  }\n}\n\nexport {}\n",[189,646,647,661,671,702,707,712,716],{"__ignoreMap":288},[292,648,649,652,655,658],{"class":294,"line":12},[292,650,651],{"class":297},"declare",[292,653,654],{"class":297}," module",[292,656,657],{"class":308}," '#server\u002Fevents\u002Ftypes'",[292,659,660],{"class":301}," {\n",[292,662,663,666,669],{"class":294,"line":17},[292,664,665],{"class":297},"  interface",[292,667,668],{"class":326}," BusEvents",[292,670,660],{"class":301},[292,672,673,676,678,681,684,686,690,693,695,697,699],{"class":294,"line":22},[292,674,675],{"class":308},"    'app:project:archived'",[292,677,281],{"class":297},[292,679,680],{"class":301}," { ",[292,682,683],{"class":359},"projectId",[292,685,281],{"class":297},[292,687,689],{"class":688},"sBjJW"," number",[292,691,692],{"class":301},"; ",[292,694,360],{"class":359},[292,696,281],{"class":297},[292,698,689],{"class":688},[292,700,701],{"class":301}," }\n",[292,703,704],{"class":294,"line":27},[292,705,706],{"class":301},"  }\n",[292,708,709],{"class":294,"line":32},[292,710,711],{"class":301},"}\n",[292,713,714],{"class":294,"line":37},[292,715,315],{"emptyLinePlaceholder":314},[292,717,718,720],{"class":294,"line":46},[292,719,320],{"class":297},[292,721,722],{"class":301}," {}\n",[178,724,725,726,729,730,733,734,736],{},"The ",[189,727,728],{},"export {}"," makes the file a module. Without it the ",[189,731,732],{},"declare module","\nreplaces the kit's types instead of extending them, and every ",[189,735,244],{}," and\nlistener loses its types.",[178,738,739],{},"Then fire it after the write lands:",[283,741,743],{"className":285,"code":742,"language":287,"meta":288,"style":288},"import { emit, fromRequest } from '#server\u002Fevents\u002Femit'\n\nawait useDrizzle(event)\n  .update(tables.projects)\n  .set({ archivedAt: new Date() })\n  .where(eq(tables.projects.id, projectId))\n\nawait emit(fromRequest(event), 'app:project:archived', { projectId, userId })\n",[189,744,745,757,761,770,781,800,815,819],{"__ignoreMap":288},[292,746,747,749,752,754],{"class":294,"line":12},[292,748,298],{"class":297},[292,750,751],{"class":301}," { emit, fromRequest } ",[292,753,305],{"class":297},[292,755,756],{"class":308}," '#server\u002Fevents\u002Femit'\n",[292,758,759],{"class":294,"line":17},[292,760,315],{"emptyLinePlaceholder":314},[292,762,763,766,768],{"class":294,"line":22},[292,764,765],{"class":297},"await",[292,767,556],{"class":326},[292,769,559],{"class":301},[292,771,772,775,778],{"class":294,"line":27},[292,773,774],{"class":301},"  .",[292,776,777],{"class":326},"update",[292,779,780],{"class":301},"(tables.projects)\n",[292,782,783,785,788,791,794,797],{"class":294,"line":32},[292,784,774],{"class":301},[292,786,787],{"class":326},"set",[292,789,790],{"class":301},"({ archivedAt: ",[292,792,793],{"class":297},"new",[292,795,796],{"class":326}," Date",[292,798,799],{"class":301},"() })\n",[292,801,802,804,807,809,812],{"class":294,"line":37},[292,803,774],{"class":301},[292,805,806],{"class":326},"where",[292,808,380],{"class":301},[292,810,811],{"class":326},"eq",[292,813,814],{"class":301},"(tables.projects.id, projectId))\n",[292,816,817],{"class":294,"line":46},[292,818,315],{"emptyLinePlaceholder":314},[292,820,821,823,826,828,831,834,837],{"class":294,"line":51},[292,822,765],{"class":297},[292,824,825],{"class":326}," emit",[292,827,380],{"class":301},[292,829,830],{"class":326},"fromRequest",[292,832,833],{"class":301},"(event), ",[292,835,836],{"class":308},"'app:project:archived'",[292,838,839],{"class":301},", { projectId, userId })\n",[841,842,843,846,853],"ul",{},[202,844,845],{},"Emit after a transaction resolves, never inside its callback. A listener\ncould act on a write that rolls back, and a retried transaction would fire\nthe event twice.",[202,847,848,849,852],{},"Keep payloads plain JSON: ids, strings, numbers. No ",[189,850,851],{},"H3Event",", binding or\nfunction.",[202,854,855,856,859,860,863],{},"Name events in the past tense (",[189,857,858],{},"archived",", not ",[189,861,862],{},"archive","). A listener can't\nrefuse an event, and an imperative name suggests it can.",[194,865,867],{"id":866},"register-a-listener-in-code","Register a listener in code",[178,869,870,871,874,875,281],{},"For a listener that needs a closure, register it from a Nitro plugin with\n",[189,872,873],{},"onEvent",". It always runs ",[189,876,251],{},[283,878,880],{"className":285,"code":879,"language":287,"meta":288,"style":288},"import { onEvent } from '#server\u002Fevents\u002Femit'\n\nexport default defineNitroPlugin(() => {\n  const endpoint = 'https:\u002F\u002Fcrm.example.com\u002Fcontacts'\n\n  onEvent('account:purged', async ({ userId }) => {\n    await $fetch(`${endpoint}\u002F${userId}`, { method: 'DELETE' })\n  })\n})\n",[189,881,882,893,897,914,928,932,959,990,995],{"__ignoreMap":288},[292,883,884,886,889,891],{"class":294,"line":12},[292,885,298],{"class":297},[292,887,888],{"class":301}," { onEvent } ",[292,890,305],{"class":297},[292,892,756],{"class":308},[292,894,895],{"class":294,"line":17},[292,896,315],{"emptyLinePlaceholder":314},[292,898,899,901,903,906,909,912],{"class":294,"line":22},[292,900,320],{"class":297},[292,902,323],{"class":297},[292,904,905],{"class":326}," defineNitroPlugin",[292,907,908],{"class":301},"(() ",[292,910,911],{"class":297},"=>",[292,913,660],{"class":301},[292,915,916,919,922,925],{"class":294,"line":27},[292,917,918],{"class":297},"  const",[292,920,921],{"class":688}," endpoint",[292,923,924],{"class":297}," =",[292,926,927],{"class":308}," 'https:\u002F\u002Fcrm.example.com\u002Fcontacts'\n",[292,929,930],{"class":294,"line":32},[292,931,315],{"emptyLinePlaceholder":314},[292,933,934,937,939,942,944,947,950,952,955,957],{"class":294,"line":37},[292,935,936],{"class":326},"  onEvent",[292,938,380],{"class":301},[292,940,941],{"class":308},"'account:purged'",[292,943,363],{"class":301},[292,945,946],{"class":297},"async",[292,948,949],{"class":301}," ({ ",[292,951,360],{"class":359},[292,953,954],{"class":301}," }) ",[292,956,911],{"class":297},[292,958,660],{"class":301},[292,960,961,963,965,967,970,973,976,978,981,984,987],{"class":294,"line":46},[292,962,374],{"class":297},[292,964,377],{"class":326},[292,966,380],{"class":301},[292,968,969],{"class":308},"`${",[292,971,972],{"class":301},"endpoint",[292,974,975],{"class":308},"}\u002F${",[292,977,360],{"class":301},[292,979,980],{"class":308},"}`",[292,982,983],{"class":301},", { method: ",[292,985,986],{"class":308},"'DELETE'",[292,988,989],{"class":301}," })\n",[292,991,992],{"class":294,"line":51},[292,993,994],{"class":301},"  })\n",[292,996,997],{"class":294,"line":56},[292,998,416],{"class":301},[178,1000,1001,1004,1005,1008,1009,1012,1013,1015,1016,1023],{},[189,1002,1003],{},"onPaymentSettled(nitro, handler)"," from ",[189,1006,1007],{},"#server\u002Fpayments\u002Fsettled"," does the same\nfor ",[189,1010,1011],{},"payments:settled"," and hands the handler the payload with ",[189,1014,422],{}," merged\nin. ",[182,1017,1020],{"href":1018,"rel":1019,"title":187},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Fplugins\u002Fnotify-payments.ts",[186],[189,1021,1022],{},"server\u002Fplugins\u002Fnotify-payments.ts","\nuses it.",[194,1025,1027],{"id":1026},"the-catalogue","The catalogue",[178,1029,1030,1031,263],{},"The payload types are in ",[182,1032,1035],{"href":1033,"rel":1034,"title":187},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Fevents\u002Fcatalogue.ts",[186],[189,1036,1037],{},"server\u002Fevents\u002Fcatalogue.ts",[1039,1040,1041,1057],"table",{},[1042,1043,1044],"thead",{},[1045,1046,1047,1051,1054],"tr",{},[1048,1049,1050],"th",{},"Event",[1048,1052,1053],{},"Fired",[1048,1055,1056],{},"Payload",[1058,1059,1060,1094,1121,1144,1162,1184,1200,1219,1249,1270],"tbody",{},[1045,1061,1062,1067,1070],{},[1063,1064,1065],"td",{},[189,1066,1011],{},[1063,1068,1069],{},"The payments webhook, once per provider event it applies",[1063,1071,1072,363,1075,363,1078,363,1081,363,1083,363,1085,363,1088,363,1091],{},[189,1073,1074],{},"provider",[189,1076,1077],{},"normalized",[189,1079,1080],{},"rawBody",[189,1082,360],{},[189,1084,538],{},[189,1086,1087],{},"eventRowId",[189,1089,1090],{},"country",[189,1092,1093],{},"retried",[1045,1095,1096,1101,1104],{},[1063,1097,1098],{},[189,1099,1100],{},"payments:refunded",[1063,1102,1103],{},"The payments webhook, when a refund is applied",[1063,1105,1106,363,1108,363,1110,363,1112,363,1115,363,1118],{},[189,1107,1074],{},[189,1109,360],{},[189,1111,1087],{},[189,1113,1114],{},"plan",[189,1116,1117],{},"amountMinor",[189,1119,1120],{},"currency",[1045,1122,1123,1128,1134],{},[1063,1124,1125],{},[189,1126,1127],{},"account:deletionScheduled",[1063,1129,1130,1133],{},[189,1131,1132],{},"DELETE \u002Fapi\u002Fauth\u002Faccount",", with a grace period or without",[1063,1135,1136,363,1138,363,1141],{},[189,1137,360],{},[189,1139,1140],{},"purgeAt",[189,1142,1143],{},"subscriptions",[1045,1145,1146,1151,1158],{},[1063,1147,1148],{},[189,1149,1150],{},"account:purged",[1063,1152,1153,1154,1157],{},"The end of ",[189,1155,1156],{},"purgeAccount",", after the user row is gone",[1063,1159,1160],{},[189,1161,360],{},[1045,1163,1164,1169,1172],{},[1063,1165,1166],{},[189,1167,1168],{},"auth:registered",[1063,1170,1171],{},"A new account from a password, magic-link or OAuth signup",[1063,1173,1174,363,1176,363,1178,363,1181],{},[189,1175,360],{},[189,1177,366],{},[189,1179,1180],{},"method",[189,1182,1183],{},"verified",[1045,1185,1186,1191,1194],{},[1063,1187,1188],{},[189,1189,1190],{},"auth:emailVerified",[1063,1192,1193],{},"An unproven account proves an address, or staff mark it as verified",[1063,1195,1196,363,1198],{},[189,1197,360],{},[189,1199,366],{},[1045,1201,1202,1207,1210],{},[1063,1203,1204],{},[189,1205,1206],{},"auth:emailChanged",[1063,1208,1209],{},"An account moves to a new address, by its link or by a staff edit",[1063,1211,1212,363,1214,363,1216],{},[189,1213,360],{},[189,1215,366],{},[189,1217,1218],{},"previousEmail",[1045,1220,1221,1226,1232],{},[1063,1222,1223],{},[189,1224,1225],{},"storage:uploaded",[1063,1227,1228,1231],{},[189,1229,1230],{},"putObject",", after the object is stored",[1063,1233,1234,363,1237,363,1240,363,1243,363,1246],{},[189,1235,1236],{},"purpose",[189,1238,1239],{},"key",[189,1241,1242],{},"ownerId",[189,1244,1245],{},"bytes",[189,1247,1248],{},"format",[1045,1250,1251,1256,1262],{},[1063,1252,1253],{},[189,1254,1255],{},"storage:deleted",[1063,1257,1258,1261],{},[189,1259,1260],{},"deleteObject",", only when the delete succeeded",[1063,1263,1264,363,1266,363,1268],{},[189,1265,1236],{},[189,1267,1239],{},[189,1269,1242],{},[1045,1271,1272,1277,1283],{},[1063,1273,1274],{},[189,1275,1276],{},"org:*",[1063,1278,1279,1280,1282],{},"The tenancy services (",[182,1281,49],{"href":48},")",[1063,1284,1285],{},"See the catalogue",[178,1287,1288,1289,1291,1292,363,1295,341,1298,363,1301,363,1304,341,1307,363,1310,466,1313,263],{},"The eight ",[189,1290,1276],{}," events are ",[189,1293,1294],{},"org:created",[189,1296,1297],{},"org:deleted",[189,1299,1300],{},"org:member:added",[189,1302,1303],{},"org:member:removed",[189,1305,1306],{},"org:member:roleChanged",[189,1308,1309],{},"org:invitation:created",[189,1311,1312],{},"org:invitation:accepted",[189,1314,1315],{},"org:invitation:revoked",[178,1317,1318],{},"A few payloads need care:",[841,1320,1321,1338,1346,1351],{},[202,1322,1323,1325,1326,1328,1329,1331,1332,1334,1335,1337],{},[189,1324,1168],{}," fires while a password signup's address is still unproven;\n",[189,1327,1183],{}," says which case you have. Provision anything real on\n",[189,1330,1190],{},", which fires once per proof, whether a verify link, a\nsign-in link or a completed password reset made it. An account created by\nguest checkout fires no ",[189,1333,1168],{},"; it fires ",[189,1336,1190],{}," the\nfirst time its owner signs in by email link.",[202,1339,1340,1342,1343,1345],{},[189,1341,1100],{}," sits next to ",[189,1344,1011],{}," on purpose: take back\nthere whatever you grant on settle.",[202,1347,1348,1350],{},[189,1349,1150],{}," carries only the id, because the address is erased. Use it\nfor data the database cascades can't reach, such as a record in an external\nCRM.",[202,1352,1353,1355],{},[189,1354,1309],{}," carries the invitation's plaintext token, the only\nplace it exists. Don't log it or store it anywhere readable.",[194,1357,1359],{"id":1358},"shipped-listeners","Shipped listeners",[178,1361,1362,1364],{},[189,1363,191],{}," belongs to your app, and an empty directory is fine. The\nkit ships three files there:",[1039,1366,1367,1380],{},[1042,1368,1369],{},[1045,1370,1371,1374,1377],{},[1048,1372,1373],{},"Listener",[1048,1375,1376],{},"On",[1048,1378,1379],{},"Delete it and",[1058,1381,1382,1396,1410],{},[1045,1383,1384,1389,1393],{},[1063,1385,1386],{},[189,1387,1388],{},"00-org-invitation-mail.ts",[1063,1390,1391],{},[189,1392,1309],{},[1063,1394,1395],{},"Invitations are still created, but no mail is sent",[1045,1397,1398,1403,1407],{},[1063,1399,1400],{},[189,1401,1402],{},"00-billing-email.ts",[1063,1404,1405],{},[189,1406,1206],{},[1063,1408,1409],{},"A new address no longer reaches the payment provider's customer",[1045,1411,1412,1417,1421],{},[1063,1413,1414],{},[189,1415,1416],{},"00-cancel-billing.ts",[1063,1418,1419],{},[189,1420,1127],{},[1063,1422,1423],{},"A deleted account's subscriptions keep billing at the provider",[194,1425,1427],{"id":1426},"good-to-know","Good to know",[841,1429,1430,1440,1461,1480],{},[202,1431,1432,1433,263],{},"Events have no config keys. What fires depends on which parts of the kit are\non in ",[182,1434,1437],{"href":1435,"rel":1436,"title":187},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fstarter.config.ts",[186],[189,1438,1439],{},"starter.config.ts",[202,1441,1442,1449,1450,1453,1454,1456,1457,1460],{},[182,1443,1446],{"href":1444,"rel":1445,"title":187},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Ftest\u002Funit\u002Fevents-catalogue.test.ts",[186],[189,1447,1448],{},"test\u002Funit\u002Fevents-catalogue.test.ts","\nreads declared events from ",[189,1451,1452],{},"catalogue.ts"," only, so its check for undeclared\nemits fails on your first ",[189,1455,244],{}," of an event from ",[189,1458,1459],{},"custom.d.ts",". Add your\nevent names to that check.",[202,1462,1463,1464,1467,1468,1475,1476,1479],{},"In unit tests, ",[189,1465,1466],{},"installEvents()"," from\n",[182,1469,1472],{"href":1470,"rel":1471,"title":187},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Ftest\u002Fhelpers\u002Fevents.ts",[186],[189,1473,1474],{},"test\u002Fhelpers\u002Fevents.ts"," records every emit and\nholds deferred work until you ",[189,1477,1478],{},"await settled()",". Assert after that, or the\nlistener hasn't run yet.",[202,1481,1482,1484,1485,1487,1488,1490],{},[189,1483,1225],{}," fires inside ",[189,1486,1230],{},", before the upload route writes\nits row. When that write fails, the route deletes the object and\n",[189,1489,1255],{}," follows, so undo on the second whatever you did on the\nfirst.",[1492,1493,1494],"style",{},"html pre.shiki code .s8jYJ, html code.shiki .s8jYJ{--shiki-light:#D73A49;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sxrX7, html code.shiki .sxrX7{--shiki-light:#24292E;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sIIMD, html code.shiki .sIIMD{--shiki-light:#032F62;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .snPdu, html code.shiki .snPdu{--shiki-light:#6F42C1;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sP4rz, html code.shiki .sP4rz{--shiki-light:#E36209;--shiki-default:#E36209;--shiki-dark:#FFAB70}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 .sBjJW, html code.shiki .sBjJW{--shiki-light:#005CC5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":288,"searchDepth":17,"depth":17,"links":1496},[1497,1498,1499,1500,1501,1502,1503,1504,1505],{"id":196,"depth":17,"text":197},{"id":269,"depth":17,"text":270},{"id":452,"depth":17,"text":453},{"id":473,"depth":17,"text":474},{"id":629,"depth":17,"text":630},{"id":866,"depth":17,"text":867},{"id":1026,"depth":17,"text":1027},{"id":1358,"depth":17,"text":1359},{"id":1426,"depth":17,"text":1427},[1507,1508],{"path":75,"title":76,"description":77,"navTitle":76,"group":67,"order":78},{"path":85,"title":86,"description":87,"navTitle":86,"group":67,"order":88}]