[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"docs-navigation":3,"docs-page-notifications":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":1135,"linksToKit":327},{"path":95,"title":96,"description":97,"navTitle":96,"group":67,"seo":168,"body":170},{"title":169,"description":97},"In-app notifications for a Nuxt dashboard",{"type":171,"value":172,"toc":1125},"minimark",[173,177,186,191,240,244,255,480,491,533,560,570,574,649,655,701,705,719,736,769,778,782,799,879,890,894,978,981,1017,1020,1023,1086,1096,1100,1121],[174,175,96],"h1",{"id":176},"notifications",[178,179,180,181,185],"p",{},"In-app notifications: a bell in the dashboard's top bar, a drawer with the\nnewest 20, and ",[182,183,184],"code",{},"notify()"," on the server to write one. The kit notifies buyers\nabout purchases, refunds and subscription changes. You add a type when your own\nfeature needs to tell someone something.",[187,188,190],"h2",{"id":189},"how-it-works","How it works",[192,193,194,215,222,229],"ol",{},[195,196,197,200,201,211,212,214],"li",{},[182,198,199],{},"notify(event, input)"," in\n",[202,203,208],"a",{"href":204,"rel":205,"title":207},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Fnotifications\u002Findex.ts",[206],"nofollow","Licensees only",[182,209,210],{},"server\u002Fnotifications\u002Findex.ts"," inserts\na row in ",[182,213,176],{},", unless the recipient switched off the type's\ncategory.",[195,216,217,218,221],{},"The drawer fetches ",[182,219,220],{},"GET \u002Fapi\u002Fnotifications",", which returns the newest 20 and\nthe unread count. It fetches again on every dashboard navigation.",[195,223,224,225,228],{},"Each row is worded from ",[182,226,227],{},"dashboard.notifications.types.\u003Ctype>"," in the\nlocale files. Opening a row marks it read and follows its link.",[195,230,231,232,235,236,239],{},"The daily ",[182,233,234],{},"notifications:prune"," task deletes notifications that were read\nmore than ",[182,237,238],{},"retentionDays"," ago. Unread ones stay.",[187,241,243],{"id":242},"add-a-notification-type","Add a notification type",[178,245,246,247,254],{},"Types live in\n",[202,248,251],{"href":249,"rel":250,"title":207},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fshared\u002Futils\u002Fnotifications.ts",[206],[182,252,253],{},"shared\u002Futils\u002Fnotifications.ts",". Add the\nname and its category:",[256,257,262],"pre",{"className":258,"code":259,"language":260,"meta":261,"style":261},"language-ts shiki shiki-themes github-light github-light github-dark","export const NOTIFICATION_TYPES = [\n  'billing.purchased',\n  'billing.refunded',\n  'billing.subscriptionStarted',\n  'billing.subscriptionCancelled',\n  'billing.subscriptionRefunded',\n\n  'product.announcement',\n\n  'reports.ready',\n] as const\n\nexport const NOTIFICATION_CATEGORY = {\n  'billing.purchased': 'transactional',\n  'billing.refunded': 'transactional',\n  'billing.subscriptionStarted': 'transactional',\n  'billing.subscriptionCancelled': 'transactional',\n  'billing.subscriptionRefunded': 'transactional',\n  'product.announcement': 'product',\n  'reports.ready': 'transactional',\n} as const satisfies Record\u003CNotificationType, MessageCategory>\n","ts","",[182,263,264,286,295,302,309,316,323,329,336,340,347,358,362,376,388,398,408,418,428,439,449],{"__ignoreMap":261},[265,266,268,272,275,279,282],"span",{"class":267,"line":12},"line",[265,269,271],{"class":270},"s8jYJ","export",[265,273,274],{"class":270}," const",[265,276,278],{"class":277},"sBjJW"," NOTIFICATION_TYPES",[265,280,281],{"class":270}," =",[265,283,285],{"class":284},"sxrX7"," [\n",[265,287,288,292],{"class":267,"line":17},[265,289,291],{"class":290},"sIIMD","  'billing.purchased'",[265,293,294],{"class":284},",\n",[265,296,297,300],{"class":267,"line":22},[265,298,299],{"class":290},"  'billing.refunded'",[265,301,294],{"class":284},[265,303,304,307],{"class":267,"line":27},[265,305,306],{"class":290},"  'billing.subscriptionStarted'",[265,308,294],{"class":284},[265,310,311,314],{"class":267,"line":32},[265,312,313],{"class":290},"  'billing.subscriptionCancelled'",[265,315,294],{"class":284},[265,317,318,321],{"class":267,"line":37},[265,319,320],{"class":290},"  'billing.subscriptionRefunded'",[265,322,294],{"class":284},[265,324,325],{"class":267,"line":46},[265,326,328],{"emptyLinePlaceholder":327},true,"\n",[265,330,331,334],{"class":267,"line":51},[265,332,333],{"class":290},"  'product.announcement'",[265,335,294],{"class":284},[265,337,338],{"class":267,"line":56},[265,339,328],{"emptyLinePlaceholder":327},[265,341,342,345],{"class":267,"line":64},[265,343,344],{"class":290},"  'reports.ready'",[265,346,294],{"class":284},[265,348,349,352,355],{"class":267,"line":73},[265,350,351],{"class":284},"] ",[265,353,354],{"class":270},"as",[265,356,357],{"class":270}," const\n",[265,359,360],{"class":267,"line":78},[265,361,328],{"emptyLinePlaceholder":327},[265,363,364,366,368,371,373],{"class":267,"line":83},[265,365,271],{"class":270},[265,367,274],{"class":270},[265,369,370],{"class":277}," NOTIFICATION_CATEGORY",[265,372,281],{"class":270},[265,374,375],{"class":284}," {\n",[265,377,378,380,383,386],{"class":267,"line":88},[265,379,291],{"class":290},[265,381,382],{"class":284},": ",[265,384,385],{"class":290},"'transactional'",[265,387,294],{"class":284},[265,389,390,392,394,396],{"class":267,"line":93},[265,391,299],{"class":290},[265,393,382],{"class":284},[265,395,385],{"class":290},[265,397,294],{"class":284},[265,399,400,402,404,406],{"class":267,"line":98},[265,401,306],{"class":290},[265,403,382],{"class":284},[265,405,385],{"class":290},[265,407,294],{"class":284},[265,409,410,412,414,416],{"class":267,"line":103},[265,411,313],{"class":290},[265,413,382],{"class":284},[265,415,385],{"class":290},[265,417,294],{"class":284},[265,419,420,422,424,426],{"class":267,"line":108},[265,421,320],{"class":290},[265,423,382],{"class":284},[265,425,385],{"class":290},[265,427,294],{"class":284},[265,429,430,432,434,437],{"class":267,"line":113},[265,431,333],{"class":290},[265,433,382],{"class":284},[265,435,436],{"class":290},"'product'",[265,438,294],{"class":284},[265,440,441,443,445,447],{"class":267,"line":122},[265,442,344],{"class":290},[265,444,382],{"class":284},[265,446,385],{"class":290},[265,448,294],{"class":284},[265,450,451,454,456,459,462,465,468,471,474,477],{"class":267,"line":127},[265,452,453],{"class":284},"} ",[265,455,354],{"class":270},[265,457,274],{"class":458},"snPdu",[265,460,461],{"class":270}," satisfies",[265,463,464],{"class":458}," Record",[265,466,467],{"class":284},"\u003C",[265,469,470],{"class":458},"NotificationType",[265,472,473],{"class":284},", ",[265,475,476],{"class":458},"MessageCategory",[265,478,479],{"class":284},">\n",[178,481,482,483,486,487,490],{},"Then word it in every locale under ",[182,484,485],{},"dashboard.notifications.types",". The dot in\nthe type is a level of nesting, so ",[182,488,489],{},"reports.ready"," is:",[256,492,496],{"className":493,"code":494,"language":495,"meta":261,"style":261},"language-json shiki shiki-themes github-light github-light github-dark","\"types\": {\n  \"reports\": {\n    \"ready\": \"Your report is ready to download.\"\n  }\n}\n","json",[182,497,498,506,513,523,528],{"__ignoreMap":261},[265,499,500,503],{"class":267,"line":12},[265,501,502],{"class":290},"\"types\"",[265,504,505],{"class":284},": {\n",[265,507,508,511],{"class":267,"line":17},[265,509,510],{"class":277},"  \"reports\"",[265,512,505],{"class":284},[265,514,515,518,520],{"class":267,"line":22},[265,516,517],{"class":277},"    \"ready\"",[265,519,382],{"class":284},[265,521,522],{"class":290},"\"Your report is ready to download.\"\n",[265,524,525],{"class":267,"line":27},[265,526,527],{"class":284},"  }\n",[265,529,530],{"class":267,"line":32},[265,531,532],{"class":284},"}\n",[178,534,535,536,539,540,547,548,551,552,555,556,559],{},"A flat ",[182,537,538],{},"\"reports.ready\""," key renders the raw key path in the drawer.\n",[202,541,544],{"href":542,"rel":543,"title":207},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Ftest\u002Funit\u002Fnotification-copy.test.ts",[206],[182,545,546],{},"test\u002Funit\u002Fnotification-copy.test.ts","\nfails when a locale misses a type or words one that doesn't exist, and when a\n",[182,549,550],{},"billing.*"," or ",[182,553,554],{},"security.*"," type is not ",[182,557,558],{},"transactional",".",[178,561,562,563,566,567,569],{},"The type is stored verbatim in ",[182,564,565],{},"notifications.type",", so renaming one later\nmeans updating the stored rows. ",[182,568,253],{}," belongs to the\ntemplate, so expect a small conflict there on updates.",[187,571,573],{"id":572},"send-a-notification","Send a notification",[256,575,577],{"className":258,"code":576,"language":260,"meta":261,"style":261},"import { notify } from '#server\u002Fnotifications'\n\nawait notify(event, {\n  userId,\n  type: 'reports.ready',\n  metadata: { reportId },\n  link: `\u002Freports\u002F${reportId}`,\n})\n",[182,578,579,593,597,608,613,623,628,644],{"__ignoreMap":261},[265,580,581,584,587,590],{"class":267,"line":12},[265,582,583],{"class":270},"import",[265,585,586],{"class":284}," { notify } ",[265,588,589],{"class":270},"from",[265,591,592],{"class":290}," '#server\u002Fnotifications'\n",[265,594,595],{"class":267,"line":17},[265,596,328],{"emptyLinePlaceholder":327},[265,598,599,602,605],{"class":267,"line":22},[265,600,601],{"class":270},"await",[265,603,604],{"class":458}," notify",[265,606,607],{"class":284},"(event, {\n",[265,609,610],{"class":267,"line":27},[265,611,612],{"class":284},"  userId,\n",[265,614,615,618,621],{"class":267,"line":32},[265,616,617],{"class":284},"  type: ",[265,619,620],{"class":290},"'reports.ready'",[265,622,294],{"class":284},[265,624,625],{"class":267,"line":37},[265,626,627],{"class":284},"  metadata: { reportId },\n",[265,629,630,633,636,639,642],{"class":267,"line":46},[265,631,632],{"class":284},"  link: ",[265,634,635],{"class":290},"`\u002Freports\u002F${",[265,637,638],{"class":284},"reportId",[265,640,641],{"class":290},"}`",[265,643,294],{"class":284},[265,645,646],{"class":267,"line":51},[265,647,648],{"class":284},"})\n",[178,650,651,654],{},[182,652,653],{},"notify"," never throws. A failed write is logged and dropped, so it's safe\ninside a webhook, a job or a sign-in path. It returns without writing when\nnotifications are off or the recipient refused the category. The event only\nneeds its bindings, so a job's or a listener's event works.",[656,657,658,672,695],"ul",{},[195,659,660,663,664,667,668,671],{},[182,661,662],{},"link"," must be a same-origin path. Anything else, ",[182,665,666],{},"\u002F\u002Fhost"," included, is\nlogged and stored as ",[182,669,670],{},"null",". Write it without a locale prefix: the drawer\nadds the reader's when it navigates.",[195,673,674,677,678,680,681,684,685,200,688,559],{},[182,675,676],{},"metadata"," is stored as JSON and returned by the API as ",[182,679,676],{},". The\nshipped panel renders the copy without it; to interpolate values, pass\n",[182,682,683],{},"row.metadata"," to ",[182,686,687],{},"t()",[202,689,692],{"href":690,"rel":691,"title":207},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fapp\u002Fcomponents\u002FNotificationsPanel.vue",[206],[182,693,694],{},"NotificationsPanel.vue",[195,696,697,700],{},[182,698,699],{},"userId"," must be an existing account.",[187,702,704],{"id":703},"categories-and-opt-outs","Categories and opt-outs",[178,706,707,708,710,711,718],{},"Notifications use the same three categories as ",[202,709,86],{"href":85},", from\n",[202,712,715],{"href":713,"rel":714,"title":207},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fshared\u002Fmessaging\u002Fcategories.ts",[206],[182,716,717],{},"shared\u002Fmessaging\u002Fcategories.ts",":",[656,720,721,726],{},[195,722,723,725],{},[182,724,558],{}," is always written. Use it for anything about the user's own\naccount, money or security.",[195,727,728,731,732,735],{},[182,729,730],{},"product"," and ",[182,733,734],{},"marketing"," can be switched off. Use them for things the user\nloses nothing by missing.",[178,737,738,739,731,742,745,746,749,750,753,754,757,758,731,761,764,765,768],{},"The switches are separate from the email ones: the in-app refusal lives in\n",[182,740,741],{},"users.notify_product_opt_out_at",[182,743,744],{},"users.notify_marketing_opt_out_at",".\nUsers flip them on ",[182,747,748],{},"\u002Fdashboard\u002Fsettings"," while ",[182,751,752],{},"notifications.enabled"," is on.\nThe route behind them is ",[182,755,756],{},"PATCH \u002Fapi\u002Faccount\u002Femail-preferences"," with\n",[182,759,760],{},"inAppProduct",[182,762,763],{},"inAppMarketing"," booleans (",[182,766,767],{},"true"," is subscribed).",[178,770,771,774,775,777],{},[182,772,773],{},"product.announcement"," is declared with the ",[182,776,730],{}," category and nothing\nsends it. Use it for product news, or delete it.",[187,779,781],{"id":780},"shipped-notifications","Shipped notifications",[178,783,784,791,792,795,796,798],{},[202,785,788],{"href":786,"rel":787,"title":207},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Fplugins\u002Fnotify-payments.ts",[206],[182,789,790],{},"server\u002Fplugins\u002Fnotify-payments.ts","\nlistens to ",[182,793,794],{},"payments:settled"," (",[202,797,81],{"href":80},") and writes one\nnotification per applied provider event:",[800,801,802,815],"table",{},[803,804,805],"thead",{},[806,807,808,812],"tr",{},[809,810,811],"th",{},"Payment event",[809,813,814],{},"Notification",[816,817,818,831,843,855,867],"tbody",{},[806,819,820,826],{},[821,822,823],"td",{},[182,824,825],{},"payment.succeeded",[821,827,828],{},[182,829,830],{},"billing.purchased",[806,832,833,838],{},[821,834,835],{},[182,836,837],{},"payment.refunded",[821,839,840],{},[182,841,842],{},"billing.refunded",[806,844,845,850],{},[821,846,847],{},[182,848,849],{},"subscription.created",[821,851,852],{},[182,853,854],{},"billing.subscriptionStarted",[806,856,857,862],{},[821,858,859],{},[182,860,861],{},"subscription.cancelled",[821,863,864],{},[182,865,866],{},"billing.subscriptionCancelled",[806,868,869,874],{},[821,870,871],{},[182,872,873],{},"subscription.refunded",[821,875,876],{},[182,877,878],{},"billing.subscriptionRefunded",[178,880,881,882,885,886,889],{},"Each links to the billing page of the organization the grant landed on,\n",[182,883,884],{},"\u002Fo\u002F\u003CpublicId>\u002Fbilling",", or ",[182,887,888],{},"\u002Fdashboard\u002Fbilling"," with organizations off. When\nthat organization is gone, the notification has no link. A purchase with no\nknown user writes nothing. Delete the plugin if you don't want these.",[187,891,893],{"id":892},"the-bell-and-the-drawer","The bell and the drawer",[800,895,896,906],{},[803,897,898],{},[806,899,900,903],{},[809,901,902],{},"File",[809,904,905],{},"What it does",[816,907,908,925,942,954],{},[806,909,910,919],{},[821,911,912],{},[202,913,916],{"href":914,"rel":915,"title":207},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fapp\u002Fcomponents\u002FNotificationBell.vue",[206],[182,917,918],{},"NotificationBell.vue",[821,920,921,922],{},"The trigger with the unread count, in ",[182,923,924],{},"DashboardTopBar.vue",[806,926,927,936],{},[821,928,929],{},[202,930,933],{"href":931,"rel":932,"title":207},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fapp\u002Fcomponents\u002FNotificationsDrawer.vue",[206],[182,934,935],{},"NotificationsDrawer.vue",[821,937,938,939],{},"Mounted once in ",[182,940,941],{},"app\u002Flayouts\u002Fdashboard.vue",[806,943,944,951],{},[821,945,946],{},[202,947,949],{"href":690,"rel":948,"title":207},[206],[182,950,694],{},[821,952,953],{},"The list, shared by both drawer surfaces",[806,955,956,965],{},[821,957,958],{},[202,959,962],{"href":960,"rel":961,"title":207},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fapp\u002Fcomposables\u002FuseNotifications.ts",[206],[182,963,964],{},"useNotifications.ts",[821,966,967,968,473,971,974,975],{},"The fetch, ",[182,969,970],{},"markAllRead",[182,972,973],{},"select",", and ",[182,976,977],{},"useNotificationsOpen()",[178,979,980],{},"From 1024px up the drawer is a 20rem region on the right that the dashboard\nshrinks to uncover. Below that it is a slideover. Both read one open flag, so\nyou can open the drawer from anywhere:",[256,982,984],{"className":258,"code":983,"language":260,"meta":261,"style":261},"const open = useNotificationsOpen()\n\nopen.value = true\n",[182,985,986,1002,1006],{"__ignoreMap":261},[265,987,988,991,994,996,999],{"class":267,"line":12},[265,989,990],{"class":270},"const",[265,992,993],{"class":277}," open",[265,995,281],{"class":270},[265,997,998],{"class":458}," useNotificationsOpen",[265,1000,1001],{"class":284},"()\n",[265,1003,1004],{"class":267,"line":17},[265,1005,328],{"emptyLinePlaceholder":327},[265,1007,1008,1011,1014],{"class":267,"line":22},[265,1009,1010],{"class":284},"open.value ",[265,1012,1013],{"class":270},"=",[265,1015,1016],{"class":277}," true\n",[178,1018,1019],{},"The drawer closes and refetches when the route path changes; a query-only\nchange does neither. Opening a row marks it read without waiting for the\nrequest, so a failed mark leaves it unread and still navigates. Mark all as\nread shows a toast when it fails.",[187,1021,20],{"id":1022},"configuration",[800,1024,1025,1040],{},[803,1026,1027],{},[806,1028,1029,1032,1035,1038],{},[809,1030,1031],{},"Key",[809,1033,1034],{},"Where",[809,1036,1037],{},"Default",[809,1039,905],{},[816,1041,1042,1063],{},[806,1043,1044,1048,1053,1057],{},[821,1045,1046],{},[182,1047,752],{},[821,1049,1050],{},[182,1051,1052],{},"starter.config.ts",[821,1054,1055],{},[182,1056,767],{},[821,1058,1059,1060,1062],{},"Off, ",[182,1061,184],{}," writes nothing and the bell, drawer and in-app switches don't render.",[806,1064,1065,1070,1074,1079],{},[821,1066,1067],{},[182,1068,1069],{},"notifications.retentionDays",[821,1071,1072],{},[182,1073,1052],{},[821,1075,1076],{},[182,1077,1078],{},"30",[821,1080,1081,1082,1085],{},"Days a read notification is kept. ",[182,1083,1084],{},"false"," keeps everything.",[178,1087,1088,1089,1092,1093,1095],{},"The build refuses notifications with ",[182,1090,1091],{},"auth.enabled: false",", since every\nnotification belongs to an account. The prune runs on the daily cron with the\nother scheduled tasks (",[202,1094,76],{"href":75},").",[187,1097,1099],{"id":1098},"good-to-know","Good to know",[656,1101,1102,1108,1111,1116],{},[195,1103,1104,1105,1107],{},"Add a type in the same change as the first ",[182,1106,184],{}," that sends it.\nSomeone will build a badge or a preference around a declared type, and if\nnothing sends it, the bug is silence.",[195,1109,1110],{},"Notifications and the activity log are separate. Write a notification only\nfor something worth interrupting the user over; the activity log keeps the\naudit trail.",[195,1112,1113,1114,559],{},"The API returns 20 rows and no pages. If your app needs a full history,\nadd your own route over ",[182,1115,176],{},[195,1117,1118,1120],{},[182,1119,220],{}," and the read routes need a verified session, and\nmarking a row read is scoped to the caller. Another user's id changes\nnothing and gets the same answer.",[1122,1123,1124],"style",{},"html pre.shiki code .s8jYJ, html code.shiki .s8jYJ{--shiki-light:#D73A49;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sBjJW, html code.shiki .sBjJW{--shiki-light:#005CC5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .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);}",{"title":261,"searchDepth":17,"depth":17,"links":1126},[1127,1128,1129,1130,1131,1132,1133,1134],{"id":189,"depth":17,"text":190},{"id":242,"depth":17,"text":243},{"id":572,"depth":17,"text":573},{"id":703,"depth":17,"text":704},{"id":780,"depth":17,"text":781},{"id":892,"depth":17,"text":893},{"id":1022,"depth":17,"text":20},{"id":1098,"depth":17,"text":1099},[1136,1137],{"path":90,"title":91,"description":92,"navTitle":91,"group":67,"order":93},{"path":100,"title":101,"description":102,"navTitle":101,"group":67,"order":103}]