[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"docs-navigation":3,"docs-page-auth":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":2459,"linksToKit":842},{"path":43,"title":44,"description":45,"navTitle":44,"group":40,"seo":168,"body":170},{"title":169,"description":45},"Nuxt auth: magic links, passkeys, OAuth and 2FA",{"type":171,"value":172,"toc":2440},"minimark",[173,177,200,205,270,274,375,445,490,509,513,569,576,593,596,601,604,618,677,681,684,734,771,775,780,857,923,930,939,1029,1073,1076,1079,1100,1106,1129,1148,1173,1177,1180,1201,1204,1223,1226,1238,1241,1257,1260,1284,1302,1306,1319,1446,1476,1490,1509,1513,1524,1550,1562,1588,1746,1760,1764,1770,1798,1820,1824,1933,1956,2086,2094,2098,2107,2110,2279,2367,2371,2436],[174,175,44],"h1",{"id":176},"auth",[178,179,180,181,185,186,188,189,199],"p",{},"Auth covers sign-in, sessions, second factors, email verification and the\naccount pages under ",[182,183,184],"code",{},"\u002Fdashboard\u002Fsettings",". It is switched from the ",[182,187,176],{}," block\nin ",[190,191,196],"a",{"href":192,"rel":193,"title":195},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fstarter.config.ts",[194],"nofollow","Licensees only",[182,197,198],{},"starter.config.ts",". You work here when you pick the\nsign-in methods, add an OAuth provider or protect a route of your own.",[201,202,204],"h2",{"id":203},"how-it-works","How it works",[206,207,208,230,257,267],"ol",{},[209,210,211,218,219,221,222,229],"li",{},[190,212,215],{"href":213,"rel":214,"title":195},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fmodules\u002Fauth-routes.ts",[194],[182,216,217],{},"modules\u002Fauth-routes.ts"," reads ",[182,220,176],{}," at build\ntime and mounts only the routes it asks for, from\n",[190,223,226],{"href":224,"rel":225,"title":195},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Ftree\u002Fmain\u002Fserver\u002Fauth\u002Froutes",[194],[182,227,228],{},"server\u002Fauth\u002Froutes\u002F",". A method you turn off has\nno endpoint.",[209,231,232,233,236,237,244,245,248,249,252,253,256],{},"Every sign-in goes through ",[182,234,235],{},"openSession"," in\n",[190,238,241],{"href":239,"rel":240,"title":195},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Futils\u002Fauth.ts",[194],[182,242,243],{},"server\u002Futils\u002Fauth.ts",". It refuses a suspended or\ndeletion-scheduled account, works out whether a second factor is still owed,\nwrites a row in ",[182,246,247],{},"sessions",", and seals the nuxt-auth-utils cookie with that\nrow's id (",[182,250,251],{},"sid",") and the account's ",[182,254,255],{},"session_version",".",[209,258,259,262,263,266],{},[182,260,261],{},"requireSession"," reads the row back on every call. A session ends when its\nrow is revoked, when the account's version is bumped, or after 30 days\nwithout a request. The daily ",[182,264,265],{},"sessions:prune"," task deletes dead rows.",[209,268,269],{},"Mailed links (verification, magic link, password reset, restore) open a page\nthat posts the token when the person clicks. A mail scanner that prefetches\nthe link spends nothing.",[201,271,273],{"id":272},"choose-how-people-sign-in","Choose how people sign in",[275,276,281],"pre",{"className":277,"code":278,"language":279,"meta":280,"style":280},"language-ts shiki shiki-themes github-light github-light github-dark","auth: {\n  enabled: true,\n  primary: 'password',\n  oauth: ['github'],\n  totp: true,\n  passkey: 'login',\n  emailVerification: true,\n},\n","ts","",[182,282,283,294,309,322,336,347,359,370],{"__ignoreMap":280},[284,285,287,290],"span",{"class":286,"line":12},"line",[284,288,176],{"class":289},"snPdu",[284,291,293],{"class":292},"sxrX7",": {\n",[284,295,296,299,302,306],{"class":286,"line":17},[284,297,298],{"class":289},"  enabled",[284,300,301],{"class":292},": ",[284,303,305],{"class":304},"sBjJW","true",[284,307,308],{"class":292},",\n",[284,310,311,314,316,320],{"class":286,"line":22},[284,312,313],{"class":289},"  primary",[284,315,301],{"class":292},[284,317,319],{"class":318},"sIIMD","'password'",[284,321,308],{"class":292},[284,323,324,327,330,333],{"class":286,"line":27},[284,325,326],{"class":289},"  oauth",[284,328,329],{"class":292},": [",[284,331,332],{"class":318},"'github'",[284,334,335],{"class":292},"],\n",[284,337,338,341,343,345],{"class":286,"line":32},[284,339,340],{"class":289},"  totp",[284,342,301],{"class":292},[284,344,305],{"class":304},[284,346,308],{"class":292},[284,348,349,352,354,357],{"class":286,"line":37},[284,350,351],{"class":289},"  passkey",[284,353,301],{"class":292},[284,355,356],{"class":318},"'login'",[284,358,308],{"class":292},[284,360,361,364,366,368],{"class":286,"line":46},[284,362,363],{"class":289},"  emailVerification",[284,365,301],{"class":292},[284,367,305],{"class":304},[284,369,308],{"class":292},[284,371,372],{"class":286,"line":51},[284,373,374],{"class":292},"},\n",[376,377,378,396],"table",{},[379,380,381],"thead",{},[382,383,384,390,393],"tr",{},[385,386,387],"th",{},[182,388,389],{},"primary",[385,391,392],{},"Sign-in",[385,394,395],{},"New accounts",[397,398,399,417,432],"tbody",{},[382,400,401,406,412],{},[402,403,404],"td",{},[182,405,319],{},[402,407,408,409],{},"Email and password on ",[182,410,411],{},"\u002Flogin",[402,413,414],{},[182,415,416],{},"\u002Fregister",[382,418,419,424,429],{},[402,420,421],{},[182,422,423],{},"'passwordless'",[402,425,426,427],{},"A magic link mailed from ",[182,428,411],{},[402,430,431],{},"The first magic link sent to a new address",[382,433,434,439,442],{},[402,435,436],{},[182,437,438],{},"'none'",[402,440,441],{},"OAuth only",[402,443,444],{},"The first OAuth sign-in with a trusted address",[446,447,448,456,462,472],"ul",{},[209,449,450,453,454,256],{},[182,451,452],{},"oauth"," works with any ",[182,455,389],{},[209,457,458,461],{},[182,459,460],{},"totp"," adds authenticator codes as a second factor. It is never a way in on\nits own.",[209,463,464,467,468,471],{},[182,465,466],{},"passkey: 'login'"," lets a passkey sign in alone and skips TOTP.\n",[182,469,470],{},"'second-factor'"," only confirms a session the primary method opened.",[209,473,474,477,478,481,482,484,485,489],{},[182,475,476],{},"emailVerification"," applies under ",[182,479,480],{},"primary: 'password'",". Under\n",[182,483,423],{}," an account whose address nobody has proven always waits for\nits first sign-in by email link, whatever the flag says\n(",[190,486,488],{"href":487},"#verify-email-addresses","Verify email addresses",").",[178,491,492,494,495,497,498,500,501,504,505,508],{},[182,493,416],{}," exists only under ",[182,496,319],{},". On other builds it answers 404 and\nevery sign-up link points at ",[182,499,411],{},". Password reset needs mail, so with\n",[182,502,503],{},"email.enabled: false"," its routes are not mounted and the \"Forgot password?\"\nlink disappears. The build fails on ",[182,506,507],{},"primary: 'none'"," when nothing could ever\nsign in, and warns when existing users can sign in but nobody new can register,\nfor example with Microsoft as the only provider.",[201,510,512],{"id":511},"add-github-sign-in","Add GitHub sign-in",[206,514,515,521,527,562],{},[209,516,517,518,256],{},"Set ",[182,519,520],{},"oauth: ['github']",[209,522,523,524,256],{},"On GitHub, create an OAuth app with the callback URL\n",[182,525,526],{},"https:\u002F\u002F\u003Cyour-domain>\u002Fapi\u002Fauth\u002Foauth\u002Fgithub",[209,528,529,530,533,534],{},"Put its credentials in ",[182,531,532],{},".env",", and in the Worker's secrets for a deploy:",[275,535,539],{"className":536,"code":537,"language":538,"meta":280,"style":280},"language-sh shiki shiki-themes github-light github-light github-dark","NUXT_OAUTH_GITHUB_CLIENT_ID=...\nNUXT_OAUTH_GITHUB_CLIENT_SECRET=...\n","sh",[182,540,541,553],{"__ignoreMap":280},[284,542,543,546,550],{"class":286,"line":12},[284,544,545],{"class":292},"NUXT_OAUTH_GITHUB_CLIENT_ID",[284,547,549],{"class":548},"s8jYJ","=",[284,551,552],{"class":318},"...\n",[284,554,555,558,560],{"class":286,"line":17},[284,556,557],{"class":292},"NUXT_OAUTH_GITHUB_CLIENT_SECRET",[284,559,549],{"class":548},[284,561,552],{"class":318},[209,563,564,565,568],{},"Restart ",[182,566,567],{},"pnpm dev",". The login page shows a GitHub button.",[178,570,571,572,575],{},"A GitHub OAuth app takes one callback URL, so register a second app for local\ndevelopment with ",[182,573,574],{},"http:\u002F\u002Flocalhost:3000\u002Fapi\u002Fauth\u002Foauth\u002Fgithub",". The button\nerrors until its client id and secret are set.",[178,577,578,581,582,585,586,589,590,256],{},[182,579,580],{},"\u002Fapi\u002Fauth\u002Foauth\u002F\u003Cprovider>"," is both the leg that leaves for the provider and\nthe callback. A ",[182,583,584],{},"?redirect="," on the first leg is kept in a cookie for ten\nminutes and applied after sign-in, so an invitation link survives the round\ntrip. Google works the same way with ",[182,587,588],{},"NUXT_OAUTH_GOOGLE_*",". Microsoft also\nneeds ",[182,591,592],{},"NUXT_OAUTH_MICROSOFT_TENANT",[178,594,595],{},"Only the social sign-in started last can finish. nuxt-auth-utils keeps one\npending sign-in for all providers, so with two sign-in tabs open the older one\nreturns to the login page with an error.",[597,598,600],"h3",{"id":599},"which-providers-can-create-accounts","Which providers can create accounts",[178,602,603],{},"An OAuth identity never joins an existing account because the email matches.\nIt signs in only once it is attached to an account: either it created the\naccount, or a signed-in user connected it from Settings → Security. Signing in\nwith a provider whose address already has an account is refused, and the login\npage explains how to connect it instead.",[178,605,606,607,236,610,617],{},"Whether a provider's address may open a new account is ",[182,608,609],{},"EMAIL_TRUST",[190,611,614],{"href":612,"rel":613,"title":195},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fshared\u002Fstarter\u002Foauth-trust.ts",[194],[182,615,616],{},"shared\u002Fstarter\u002Foauth-trust.ts",":",[376,619,620,633],{},[379,621,622],{},[382,623,624,627,630],{},[385,625,626],{},"Provider",[385,628,629],{},"Rule",[385,631,632],{},"Opens accounts",[397,634,635,648,664],{},[382,636,637,640,645],{},[402,638,639],{},"GitHub",[402,641,642],{},[182,643,644],{},"'provider-confirmed'",[402,646,647],{},"Yes",[382,649,650,653,658],{},[402,651,652],{},"Google",[402,654,655],{},[182,656,657],{},"'verified-flag'",[402,659,660,661],{},"Only when Google sends ",[182,662,663],{},"email_verified: true",[382,665,666,669,674],{},[402,667,668],{},"Microsoft",[402,670,671],{},[182,672,673],{},"false",[402,675,676],{},"Never. It can only be connected from Settings",[597,678,680],{"id":679},"add-a-provider-the-kit-doesnt-wire","Add a provider the kit doesn't wire",[178,682,683],{},"nuxt-auth-utils ships many more handlers than the three the kit wires. To add\none, say Discord:",[206,685,686,703,717],{},[209,687,688,689,692,693,236,696,256],{},"Add ",[182,690,691],{},"'discord'"," to ",[182,694,695],{},"OAUTH_PROVIDERS",[190,697,700],{"href":698,"rel":699,"title":195},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fshared\u002Fstarter\u002Fproviders.ts",[194],[182,701,702],{},"shared\u002Fstarter\u002Fproviders.ts",[209,704,705,706,709,710,712,713,716],{},"Give it an entry in ",[182,707,708],{},"OAUTH_HANDLERS"," in ",[182,711,217],{},": the\nnuxt-auth-utils factory, and whether to add the email scope. Every\nprovider's route is wrapped in ",[182,714,715],{},"withOAuthState",", so the new one is too.",[209,718,719,720,722,723,726,727,256],{},"Add its trust rule to ",[182,721,609],{},", and its label and icon to ",[182,724,725],{},"OAUTH_META","\nin ",[190,728,731],{"href":729,"rel":730,"title":195},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fapp\u002Fcomposables\u002FuseStarterAuth.ts",[194],[182,732,733],{},"app\u002Fcomposables\u002FuseStarterAuth.ts",[178,735,736,737,740,741,744,745,740,748,751,752,755,756,759,760,236,763,770],{},"All three are records over the provider type, so typecheck names what you\nmissed. The callback reads ",[182,738,739],{},"id"," or ",[182,742,743],{},"sub",", ",[182,746,747],{},"email",[182,749,750],{},"mail",", and ",[182,753,754],{},"name"," or\n",[182,757,758],{},"displayName"," from the provider's user payload, in ",[182,761,762],{},"oauthCallbackHandlers",[190,764,767],{"href":765,"rel":766,"title":195},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Futils\u002Foauth-shim.ts",[194],[182,768,769],{},"server\u002Futils\u002Foauth-shim.ts",". A provider that\nnames them differently needs a case there.",[201,772,774],{"id":773},"protect-a-route","Protect a route",[178,776,777,778,617],{},"On the server, call a session helper first. They are auto-imported from\n",[182,779,243],{},[275,781,783],{"className":277,"code":782,"language":279,"meta":280,"style":280},"export default defineEventHandler(async (event) => {\n  const session = await requireVerifiedSession(event)\n\n  return { id: session.user.id, email: session.user.email }\n})\n",[182,784,785,818,838,844,852],{"__ignoreMap":280},[284,786,787,790,793,796,799,802,805,809,812,815],{"class":286,"line":12},[284,788,789],{"class":548},"export",[284,791,792],{"class":548}," default",[284,794,795],{"class":289}," defineEventHandler",[284,797,798],{"class":292},"(",[284,800,801],{"class":548},"async",[284,803,804],{"class":292}," (",[284,806,808],{"class":807},"sP4rz","event",[284,810,811],{"class":292},") ",[284,813,814],{"class":548},"=>",[284,816,817],{"class":292}," {\n",[284,819,820,823,826,829,832,835],{"class":286,"line":17},[284,821,822],{"class":548},"  const",[284,824,825],{"class":304}," session",[284,827,828],{"class":548}," =",[284,830,831],{"class":548}," await",[284,833,834],{"class":289}," requireVerifiedSession",[284,836,837],{"class":292},"(event)\n",[284,839,840],{"class":286,"line":22},[284,841,843],{"emptyLinePlaceholder":842},true,"\n",[284,845,846,849],{"class":286,"line":27},[284,847,848],{"class":548},"  return",[284,850,851],{"class":292}," { id: session.user.id, email: session.user.email }\n",[284,853,854],{"class":286,"line":32},[284,855,856],{"class":292},"})\n",[376,858,859,869],{},[379,860,861],{},[382,862,863,866],{},[385,864,865],{},"Helper",[385,867,868],{},"Answers",[397,870,871,885,895,909],{},[382,872,873,878],{},[402,874,875],{},[182,876,877],{},"requireVerifiedSession(event)",[402,879,880,881,884],{},"401 when signed out, 403 ",[182,882,883],{},"second-factor-required"," while a factor is owed",[382,886,887,892],{},[402,888,889],{},[182,890,891],{},"requireSession(event)",[402,893,894],{},"401 when signed out. Lets a session that still owes a factor through",[382,896,897,902],{},[402,898,899],{},[182,900,901],{},"verifySession(event)",[402,903,904,905,908],{},"The session, or ",[182,906,907],{},"null",". Never throws",[382,910,911,916],{},[402,912,913],{},[182,914,915],{},"requirePermission(event, 'users:manage')",[402,917,918,919,922],{},"Also checks a role or plan permission (",[190,920,921],{"href":53},"Permissions",")",[178,924,925,926,929],{},"Each one reads the session row, so a revoked, banned or version-bumped session\nis refused on its next request. ",[182,927,928],{},"getUserSession"," from nuxt-auth-utils only\nunseals the cookie and checks none of that, so don't use it to decide who\nsomeone is.",[178,931,932,933,935,936,617],{},"In a page, the ",[182,934,176],{}," middleware sends a signed-out visitor to\n",[182,937,938],{},"\u002Flogin?redirect=\u003Cpath>",[275,940,944],{"className":941,"code":942,"language":943,"meta":280,"style":280},"language-vue shiki shiki-themes github-light github-light github-dark","\u003Cscript setup lang=\"ts\">\ndefinePageMeta({ layout: 'dashboard', middleware: 'auth' })\n\nconst { user, session } = useUserSession()\n\u003C\u002Fscript>\n","vue",[182,945,946,969,989,993,1020],{"__ignoreMap":280},[284,947,948,951,955,958,961,963,966],{"class":286,"line":12},[284,949,950],{"class":292},"\u003C",[284,952,954],{"class":953},"sovSZ","script",[284,956,957],{"class":289}," setup",[284,959,960],{"class":289}," lang",[284,962,549],{"class":292},[284,964,965],{"class":318},"\"ts\"",[284,967,968],{"class":292},">\n",[284,970,971,974,977,980,983,986],{"class":286,"line":17},[284,972,973],{"class":289},"definePageMeta",[284,975,976],{"class":292},"({ layout: ",[284,978,979],{"class":318},"'dashboard'",[284,981,982],{"class":292},", middleware: ",[284,984,985],{"class":318},"'auth'",[284,987,988],{"class":292}," })\n",[284,990,991],{"class":286,"line":22},[284,992,843],{"emptyLinePlaceholder":842},[284,994,995,998,1001,1004,1006,1009,1012,1014,1017],{"class":286,"line":27},[284,996,997],{"class":548},"const",[284,999,1000],{"class":292}," { ",[284,1002,1003],{"class":304},"user",[284,1005,744],{"class":292},[284,1007,1008],{"class":304},"session",[284,1010,1011],{"class":292}," } ",[284,1013,549],{"class":548},[284,1015,1016],{"class":289}," useUserSession",[284,1018,1019],{"class":292},"()\n",[284,1021,1022,1025,1027],{"class":286,"line":32},[284,1023,1024],{"class":292},"\u003C\u002F",[284,1026,954],{"class":953},[284,1028,968],{"class":292},[178,1030,1031,1033,1034,1037,1038,301,1045,744,1047,744,1049,308,1051,744,1054,744,1057,1060,1061,1064,1065,1068,1069,1072],{},[182,1032,1003],{}," is a ",[182,1035,1036],{},"SessionUser"," from\n",[190,1039,1042],{"href":1040,"rel":1041,"title":195},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fshared\u002Ftypes\u002Fsession.ts",[194],[182,1043,1044],{},"shared\u002Ftypes\u002Fsession.ts",[182,1046,739],{},[182,1048,747],{},[182,1050,754],{},[182,1052,1053],{},"role",[182,1055,1056],{},"avatar",[182,1058,1059],{},"emailVerified"," and ",[182,1062,1063],{},"termsCurrent",". ",[182,1066,1067],{},"loggedIn"," is already\ntrue for a session that still owes its second factor, so code that means\n\"signed in\" also checks ",[182,1070,1071],{},"session.value?.pendingMfa",", as the middleware does.",[201,1074,488],{"id":1075},"verify-email-addresses",[178,1077,1078],{},"An account whose address nobody has proven yet waits for proof:",[446,1080,1081,1094],{},[209,1082,1083,1084,1060,1086,1089,1090,1093],{},"With ",[182,1085,480],{},[182,1087,1088],{},"emailVerification: true",", signup mails a\nconfirmation link that lasts 24 hours. The link counts only when it is\nopened in a browser signed in to that account; signed out, ",[182,1091,1092],{},"\u002Fverify-email","\nasks the person to sign in and comes back.",[209,1095,1083,1096,1099],{},[182,1097,1098],{},"primary: 'passwordless'",", the proof is one sign-in by email link. A\nmagic-link signup is proven from the start, so an unproven account exists\nonly when something opened a session without that link: an app signing in\nthe account a guest purchase created, or staff marking an address unverified.",[178,1101,1102,1103,617],{},"Until then the account can sign in and use the dashboard, but these answer 403\nwith ",[182,1104,1105],{},"data.reason: 'address-unverified'",[446,1107,1108,1113,1116],{},[209,1109,1110],{},[182,1111,1112],{},"POST \u002Fapi\u002Fpayments\u002Fcheckout",[209,1114,1115],{},"adding a passkey, starting TOTP enrolment and connecting an OAuth provider",[209,1117,1118,744,1121,1124,1125,922],{},[182,1119,1120],{},"POST \u002Fapi\u002Forgs\u002F:org\u002Finvitations",[182,1122,1123],{},"PATCH \u002Fapi\u002Forgs\u002F:org\u002Fmembers\u002F:userId",",\nand accepting an invitation sent to another address\n(",[190,1126,1128],{"href":1127},"\u002Fdocs\u002Ftenancy#invitations","Tenancy",[178,1130,1131,1132,1135,1136,1139,1140,1143,1144,1147],{},"The dashboard keeps a warning toast up on every page of the ",[182,1133,1134],{},"dashboard"," layout.\nUnder ",[182,1137,1138],{},"password"," its button resends the confirmation link. Under\n",[182,1141,1142],{},"passwordless"," it reads Email me a link and posts to\n",[182,1145,1146],{},"POST \u002Fapi\u002Fauth\u002Fmagic\u002Fconfirm",", which mails the session's own address a sign-in\nlink that lands back on the page; the button comes back as Send another link a\nminute later. Both are paced at one mail a minute. OAuth accounts start\nverified.",[178,1149,1150,1151,1154,1155,1158,1159,1162,1163,1166,1167,1158,1170,256],{},"When an account that has never proven an address proves one, by the verify\nlink, a completed password reset or a sign-in by email link, every passkey,\nTOTP factor and connected provider it holds is deleted if it has a password.\nThe first sign-in by email link to such an account also signs every other\nsession out, records ",[182,1152,1153],{},"auth.otherSessionsRevoked"," in its activity, and cancels\na pending address change. To refuse a new way in on a route of your own, start\nit with ",[182,1156,1157],{},"requireProvenAddress(event)"," from ",[182,1160,1161],{},"#server\u002Fauth\u002Faddress-proof",". It is\n",[182,1164,1165],{},"requireVerifiedSession"," plus the same 403. Outside auth, call\n",[182,1168,1169],{},"awaitsAddressProof(session.user)",[182,1171,1172],{},"#shared\u002Fauth\u002Faddress-proof",[201,1174,1176],{"id":1175},"second-factors-and-passkeys","Second factors and passkeys",[178,1178,1179],{},"TOTP enrolment is on Settings → Security. Confirming the first code issues 10\nrecovery codes of 16 characters, shown once. A recovery code signs in when the\nauthenticator is lost, and generating a new set replaces the old one.",[178,1181,1182,1183,1186,1187,308,1190,1193,1194,1037,1197,1200],{},"Adding or removing a passkey needs a fresh proof on the same device: a sign-in\nor a re-authentication within the last 10 minutes. Without one the route\nanswers 403 with ",[182,1184,1185],{},"data.reason: 'proof-required'",", and the settings card asks\nfor the password or a passkey (",[182,1188,1189],{},"POST \u002Fapi\u002Fauth\u002Freauth",[182,1191,1192],{},"POST \u002Fapi\u002Fauth\u002Freauth\u002Fpasskey",") and retries. Put a route of yours behind the\nsame check with ",[182,1195,1196],{},"requireRecentProof(event, session)",[182,1198,1199],{},"#server\u002Fauth\u002Freauth",". Every new passkey mails the owner.",[178,1202,1203],{},"Changing the password, changing the email address and deleting the account ask\nfor the current password when the account has one.",[178,1205,1206,1207,1210,1211,1214,1215,1218,1219,1222],{},"An account can't remove its last way in. The ",[182,1208,1209],{},"DELETE"," routes for passkeys and\nconnected providers answer 409 with ",[182,1212,1213],{},"data.reason: 'last-sign-in-method'"," when\nnothing would be left. A removal route of your own calls\n",[182,1216,1217],{},"assertStillReachableIn(tx, userId, { kind: 'passkey', id })"," inside a\n",[182,1220,1221],{},"serializable"," transaction before it deletes, as those routes do.",[201,1224,1225],{"id":247},"Sessions",[178,1227,1228,1229,744,1232,308,1235,489],{},"A session lives until it goes 30 days without a request or something revokes\nit. Settings → Security lists the account's devices and ends one or all the\nothers (",[182,1230,1231],{},"GET \u002Fapi\u002Fauth\u002Fsessions",[182,1233,1234],{},"DELETE \u002Fapi\u002Fauth\u002Fsessions\u002F:id",[182,1236,1237],{},"DELETE \u002Fapi\u002Fauth\u002Fsessions",[178,1239,1240],{},"To sign an account out everywhere, bump its version:",[275,1242,1244],{"className":277,"code":1243,"language":279,"meta":280,"style":280},"await revokeSessions(event, userId)\n",[182,1245,1246],{"__ignoreMap":280},[284,1247,1248,1251,1254],{"class":286,"line":12},[284,1249,1250],{"class":548},"await",[284,1252,1253],{"class":289}," revokeSessions",[284,1255,1256],{"class":292},"(event, userId)\n",[178,1258,1259],{},"To keep the device making the request, keep its row and reseal its cookie:",[275,1261,1263],{"className":277,"code":1262,"language":279,"meta":280,"style":280},"await revokeSessions(event, session.user.id, { keep: session.sid })\nawait issueVerifiedSession(event, session.user.id)\n",[182,1264,1265,1274],{"__ignoreMap":280},[284,1266,1267,1269,1271],{"class":286,"line":12},[284,1268,1250],{"class":548},[284,1270,1253],{"class":289},[284,1272,1273],{"class":292},"(event, session.user.id, { keep: session.sid })\n",[284,1275,1276,1278,1281],{"class":286,"line":17},[284,1277,1250],{"class":548},[284,1279,1280],{"class":289}," issueVerifiedSession",[284,1282,1283],{"class":292},"(event, session.user.id)\n",[178,1285,1286,1287,1290,1291,1293,1294,1297,1298,1301],{},"Password changes and resets, confirmed email changes and account deletion\nalready revoke sessions. A version bump also voids a pending address change:\nits confirm link answers 400 ",[182,1288,1289],{},"link-invalid"," once the account's\n",[182,1292,255],{}," has moved past the one it was asked under, or once the\nsession that asked is signed out. When you change a ",[182,1295,1296],{},"users"," column the session\ncarries, such as the name or role, call ",[182,1299,1300],{},"issueVerifiedSession"," too, so the copy\nsealed in the cookie matches.",[201,1303,1305],{"id":1304},"rate-limits-and-turnstile","Rate limits and Turnstile",[178,1307,1308,1309,709,1312,617],{},"Every credential check reserves an attempt before it runs. The limits are\n",[182,1310,1311],{},"POLICY",[190,1313,1316],{"href":1314,"rel":1315,"title":195},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Fauth\u002Fthrottle.ts",[194],[182,1317,1318],{},"server\u002Fauth\u002Fthrottle.ts",[376,1320,1321,1334],{},[379,1322,1323],{},[382,1324,1325,1328,1331],{},[385,1326,1327],{},"Scope",[385,1329,1330],{},"Keyed by",[385,1332,1333],{},"Allows",[397,1335,1336,1348,1366,1378,1391,1404,1422,1434],{},[382,1337,1338,1342,1345],{},[402,1339,1340],{},[182,1341,1138],{},[402,1343,1344],{},"IP and address",[402,1346,1347],{},"10 per 15 minutes",[382,1349,1350,1360,1363],{},[402,1351,1352,744,1354,744,1357],{},[182,1353,460],{},[182,1355,1356],{},"totp-recovery",[182,1358,1359],{},"reauth",[402,1361,1362],{},"Account",[402,1364,1365],{},"5 per 15 minutes each",[382,1367,1368,1373,1375],{},[402,1369,1370],{},[182,1371,1372],{},"totp-daily",[402,1374,1362],{},[402,1376,1377],{},"20 per day",[382,1379,1380,1385,1388],{},[402,1381,1382],{},[182,1383,1384],{},"passkey",[402,1386,1387],{},"IP",[402,1389,1390],{},"30 per 15 minutes",[382,1392,1393,1398,1401],{},[402,1394,1395],{},[182,1396,1397],{},"register",[402,1399,1400],{},"Address",[402,1402,1403],{},"10 per hour",[382,1405,1406,1417,1419],{},[402,1407,1408,744,1411,744,1414],{},[182,1409,1410],{},"magic-link",[182,1412,1413],{},"password-reset",[182,1415,1416],{},"email-verify",[402,1418,1400],{},[402,1420,1421],{},"10 per hour, 1 per minute",[382,1423,1424,1429,1431],{},[402,1425,1426],{},[182,1427,1428],{},"magic-confirm",[402,1430,1362],{},[402,1432,1433],{},"3 per hour, 1 per minute",[382,1435,1436,1441,1444],{},[402,1437,1438],{},[182,1439,1440],{},"email-change",[402,1442,1443],{},"New address",[402,1445,1421],{},[178,1447,1448,1449,1451,1452,1454,1455,1457,1458,1460,1461,1464,1465,1468,1469,256],{},"A spent scope locks for 15 minutes, ",[182,1450,1428],{}," for an hour.\n",[182,1453,1146],{}," spends ",[182,1456,1428],{}," and then the address's\n",[182,1459,1410],{},", and hands the account's send back when the address refuses. A\npasskey sign-in that succeeds hands its attempts back, so only abandoned and\nfailed ceremonies count. The counters live in the ",[182,1462,1463],{},"AUTH_THROTTLE"," Durable\nObject, and the ",[182,1466,1467],{},"AUTH_RATE_LIMIT"," binding (20 requests a minute) sheds bursts\nin front of them. Both are declared in ",[190,1470,1473],{"href":1471,"rel":1472,"title":195},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fwrangler.jsonc",[194],[182,1474,1475],{},"wrangler.jsonc",[178,1477,1478,1479,1482,1483,804,1486,1489],{},"Staff holding ",[182,1480,1481],{},"users:manage"," can lift a customer's lockout from\n",[182,1484,1485],{},"\u002Fdashboard\u002Fcustomers",[182,1487,1488],{},"POST \u002Fapi\u002Fadmin\u002Fusers\u002Funlock","). It clears the\ncounters keyed to the account and its address, and the password counters of\nIPs that failed against it in the last 15 minutes. The per-IP passkey counter\nand the email-change counter stay.",[178,1491,1492,1493,1060,1496,1499,1500,1502,1503,1505,1506,1508],{},"Turnstile guards password sign-in, registration, reset and magic-link requests,\nguest checkout and the contact page. The server checks it only when both\n",[182,1494,1495],{},"NUXT_PUBLIC_TURNSTILE_SITE_KEY",[182,1497,1498],{},"NUXT_TURNSTILE_SECRET_KEY"," are set, and\noutside ",[182,1501,567],{}," the contact page refuses until they are\n(",[190,1504,91],{"href":90},").\n",[182,1507,567],{}," runs on Cloudflare's always-pass test keys.",[201,1510,1512],{"id":1511},"delete-an-account","Delete an account",[178,1514,1515,1516,1519,1520,1523],{},"Settings → Profile → Delete account calls ",[182,1517,1518],{},"DELETE \u002Fapi\u002Fauth\u002Faccount"," with the\ntyped email address, plus the current password when the account has one.\n",[182,1521,1522],{},"account.deletionGraceDays"," decides what happens next:",[446,1525,1526,1545],{},[209,1527,1528,1529,1532,1533,1536,1537,1540,1541,1544],{},"A number (30 as shipped): the kit mails a restore link, deletes every session\nrow and refuses sign-in with 403 ",[182,1530,1531],{},"sign-in-deletion-scheduled",". Until the\nwindow closes the link opens ",[182,1534,1535],{},"\u002Frestore-account",", which calls\n",[182,1538,1539],{},"POST \u002Fapi\u002Faccount\u002Frestore",". After it, the daily ",[182,1542,1543],{},"account:purge"," task erases\nthe account.",[209,1546,1547,1549],{},[182,1548,673],{},": the account is erased in the request.",[178,1551,1552,1553,1555,1556,1559,1560,489],{},"The restore mail goes out before anything changes, so a failed send fails the\nrequest and leaves the account as it was. The build refuses a grace period\nwith ",[182,1554,503],{},". With organizations on, the request answers 409\n",[182,1557,1558],{},"sole-owner"," while the account is the only owner of an organization other\npeople still belong to (",[190,1561,1128],{"href":48},[178,1563,1564,709,1567,1574,1575,1577,1578,1581,1582,1584,1585,617],{},[182,1565,1566],{},"purgeAccount",[190,1568,1571],{"href":1569,"rel":1570,"title":195},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Fauth\u002Fdeletion.ts",[194],[182,1572,1573],{},"server\u002Fauth\u002Fdeletion.ts","\nerases. Deleting the ",[182,1576,1296],{}," row cascades to credentials, sessions, tokens,\nmemberships and notifications. Payment records stay with ",[182,1579,1580],{},"user_id"," set to null\nand the stored provider payload redacted. Any table of yours with a user column\nreferences ",[182,1583,1296],{}," with a cascade, in ",[182,1586,1587],{},"server\u002Fdatabase\u002Fcustom\u002Fschema.ts",[275,1589,1591],{"className":277,"code":1590,"language":279,"meta":280,"style":280},"import { integer, pgTable, text } from 'drizzle-orm\u002Fpg-core'\n\nimport { users } from '..\u002Fschema'\n\nexport const notes = pgTable('notes', {\n  id: integer('id').primaryKey().generatedByDefaultAsIdentity(),\n  userId: integer('user_id')\n    .notNull()\n    .references(() => users.id, { onDelete: 'cascade' }),\n  body: text('body').notNull(),\n})\n",[182,1592,1593,1607,1611,1623,1627,1650,1677,1692,1702,1723,1742],{"__ignoreMap":280},[284,1594,1595,1598,1601,1604],{"class":286,"line":12},[284,1596,1597],{"class":548},"import",[284,1599,1600],{"class":292}," { integer, pgTable, text } ",[284,1602,1603],{"class":548},"from",[284,1605,1606],{"class":318}," 'drizzle-orm\u002Fpg-core'\n",[284,1608,1609],{"class":286,"line":17},[284,1610,843],{"emptyLinePlaceholder":842},[284,1612,1613,1615,1618,1620],{"class":286,"line":22},[284,1614,1597],{"class":548},[284,1616,1617],{"class":292}," { users } ",[284,1619,1603],{"class":548},[284,1621,1622],{"class":318}," '..\u002Fschema'\n",[284,1624,1625],{"class":286,"line":27},[284,1626,843],{"emptyLinePlaceholder":842},[284,1628,1629,1631,1634,1637,1639,1642,1644,1647],{"class":286,"line":32},[284,1630,789],{"class":548},[284,1632,1633],{"class":548}," const",[284,1635,1636],{"class":304}," notes",[284,1638,828],{"class":548},[284,1640,1641],{"class":289}," pgTable",[284,1643,798],{"class":292},[284,1645,1646],{"class":318},"'notes'",[284,1648,1649],{"class":292},", {\n",[284,1651,1652,1655,1658,1660,1663,1665,1668,1671,1674],{"class":286,"line":37},[284,1653,1654],{"class":292},"  id: ",[284,1656,1657],{"class":289},"integer",[284,1659,798],{"class":292},[284,1661,1662],{"class":318},"'id'",[284,1664,489],{"class":292},[284,1666,1667],{"class":289},"primaryKey",[284,1669,1670],{"class":292},"().",[284,1672,1673],{"class":289},"generatedByDefaultAsIdentity",[284,1675,1676],{"class":292},"(),\n",[284,1678,1679,1682,1684,1686,1689],{"class":286,"line":46},[284,1680,1681],{"class":292},"  userId: ",[284,1683,1657],{"class":289},[284,1685,798],{"class":292},[284,1687,1688],{"class":318},"'user_id'",[284,1690,1691],{"class":292},")\n",[284,1693,1694,1697,1700],{"class":286,"line":51},[284,1695,1696],{"class":292},"    .",[284,1698,1699],{"class":289},"notNull",[284,1701,1019],{"class":292},[284,1703,1704,1706,1709,1712,1714,1717,1720],{"class":286,"line":56},[284,1705,1696],{"class":292},[284,1707,1708],{"class":289},"references",[284,1710,1711],{"class":292},"(() ",[284,1713,814],{"class":548},[284,1715,1716],{"class":292}," users.id, { onDelete: ",[284,1718,1719],{"class":318},"'cascade'",[284,1721,1722],{"class":292}," }),\n",[284,1724,1725,1728,1731,1733,1736,1738,1740],{"class":286,"line":64},[284,1726,1727],{"class":292},"  body: ",[284,1729,1730],{"class":289},"text",[284,1732,798],{"class":292},[284,1734,1735],{"class":318},"'body'",[284,1737,489],{"class":292},[284,1739,1699],{"class":289},[284,1741,1676],{"class":292},[284,1743,1744],{"class":286,"line":73},[284,1745,856],{"class":292},[178,1747,1748,1749,1752,1753,256],{},"Settings → Profile also has Download my data (",[182,1750,1751],{},"GET \u002Fapi\u002Fauth\u002Fexport","), a JSON\nfile of the profile, connected providers, entitlements, sessions, activity and\npasskey labels. Your own tables are not in it until you add them to\n",[190,1754,1757],{"href":1755,"rel":1756,"title":195},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Fauth\u002Froutes\u002Fdata-export.get.ts",[194],[182,1758,1759],{},"server\u002Fauth\u002Froutes\u002Fdata-export.get.ts",[201,1761,1763],{"id":1762},"track-terms-acceptance","Track terms acceptance",[178,1765,517,1766,1769],{},[182,1767,1768],{},"legal.termsVersion"," to any string. A date reads best:",[275,1771,1773],{"className":277,"code":1772,"language":279,"meta":280,"style":280},"legal: {\n  termsVersion: '2026-09-01',\n},\n",[182,1774,1775,1782,1794],{"__ignoreMap":280},[284,1776,1777,1780],{"class":286,"line":12},[284,1778,1779],{"class":289},"legal",[284,1781,293],{"class":292},[284,1783,1784,1787,1789,1792],{"class":286,"line":17},[284,1785,1786],{"class":289},"  termsVersion",[284,1788,301],{"class":292},[284,1790,1791],{"class":318},"'2026-09-01'",[284,1793,308],{"class":292},[284,1795,1796],{"class":286,"line":22},[284,1797,374],{"class":292},[178,1799,1800,1801,1804,1805,1808,1809,1812,1813,1816,1817,1819],{},"Submitting the register form then records acceptance, and every signed-in user\nwhose recorded version differs gets a dialog to accept the current terms\n(",[182,1802,1803],{},"POST \u002Fapi\u002Fauth\u002Fterms\u002Faccept","). Magic-link and OAuth signups meet the dialog on\ntheir first page. Any change to the string prompts everyone again, so change it\nonly for material edits. Acceptances land in ",[182,1806,1807],{},"users.tos_accepted_version"," and\none ",[182,1810,1811],{},"tos_acceptances"," row per version. The pages themselves are\n",[182,1814,1815],{},"content\u002F\u003Clocale>\u002Flegal\u002F",". With ",[182,1818,673],{},", the shipped value, none of this is\nbuilt.",[201,1821,1823],{"id":1822},"react-to-auth-events","React to auth events",[376,1825,1826,1839],{},[379,1827,1828],{},[382,1829,1830,1833,1836],{},[385,1831,1832],{},"Event",[385,1834,1835],{},"Fires when",[385,1837,1838],{},"Payload",[397,1840,1841,1864,1880,1899,1919],{},[382,1842,1843,1848,1851],{},[402,1844,1845],{},[182,1846,1847],{},"auth:registered",[402,1849,1850],{},"An account is created",[402,1852,1853,744,1856,744,1858,744,1861],{},[182,1854,1855],{},"userId",[182,1857,747],{},[182,1859,1860],{},"method",[182,1862,1863],{},"verified",[382,1865,1866,1871,1874],{},[402,1867,1868],{},[182,1869,1870],{},"auth:emailVerified",[402,1872,1873],{},"An account proves its address",[402,1875,1876,744,1878],{},[182,1877,1855],{},[182,1879,747],{},[382,1881,1882,1887,1890],{},[402,1883,1884],{},[182,1885,1886],{},"auth:emailChanged",[402,1888,1889],{},"A new address lands on the account",[402,1891,1892,744,1894,744,1896],{},[182,1893,1855],{},[182,1895,747],{},[182,1897,1898],{},"previousEmail",[382,1900,1901,1906,1909],{},[402,1902,1903],{},[182,1904,1905],{},"account:deletionScheduled",[402,1907,1908],{},"Deletion is requested",[402,1910,1911,744,1913,744,1916],{},[182,1912,1855],{},[182,1914,1915],{},"purgeAt",[182,1917,1918],{},"subscriptions",[382,1920,1921,1926,1929],{},[402,1922,1923],{},[182,1924,1925],{},"account:purged",[402,1927,1928],{},"The account has been erased",[402,1930,1931],{},[182,1932,1855],{},[178,1934,1935,1936,1938,1939,1942,1943,1945,1946,1949,1950,1953,1954,617],{},"A password signup fires ",[182,1937,1847],{}," with ",[182,1940,1941],{},"verified: false",", then\n",[182,1944,1870],{}," once the link is opened or a password reset completes.\nMagic-link and OAuth signups arrive verified. Provision anything real only for\na proven address. This\n",[182,1947,1948],{},"server\u002Flisteners\u002Fwelcome.ts"," queues the ",[182,1951,1952],{},"crm-sync"," job from\n",[190,1955,76],{"href":75},[275,1957,1959],{"className":277,"code":1958,"language":279,"meta":280,"style":280},"import { defineListener } from '#server\u002Fevents\u002Ftypes'\nimport { enqueue } from '#server\u002Futils\u002Fqueue'\n\nexport default defineListener({\n  event: 'auth:registered',\n\n  async handle({ userId, verified }, { event }) {\n    if (!verified) return\n\n    await enqueue(event, { kind: 'crm-sync', payload: { userId } })\n  },\n})\n",[182,1960,1961,1973,1985,1989,2001,2011,2015,2040,2056,2060,2077,2082],{"__ignoreMap":280},[284,1962,1963,1965,1968,1970],{"class":286,"line":12},[284,1964,1597],{"class":548},[284,1966,1967],{"class":292}," { defineListener } ",[284,1969,1603],{"class":548},[284,1971,1972],{"class":318}," '#server\u002Fevents\u002Ftypes'\n",[284,1974,1975,1977,1980,1982],{"class":286,"line":17},[284,1976,1597],{"class":548},[284,1978,1979],{"class":292}," { enqueue } ",[284,1981,1603],{"class":548},[284,1983,1984],{"class":318}," '#server\u002Futils\u002Fqueue'\n",[284,1986,1987],{"class":286,"line":22},[284,1988,843],{"emptyLinePlaceholder":842},[284,1990,1991,1993,1995,1998],{"class":286,"line":27},[284,1992,789],{"class":548},[284,1994,792],{"class":548},[284,1996,1997],{"class":289}," defineListener",[284,1999,2000],{"class":292},"({\n",[284,2002,2003,2006,2009],{"class":286,"line":32},[284,2004,2005],{"class":292},"  event: ",[284,2007,2008],{"class":318},"'auth:registered'",[284,2010,308],{"class":292},[284,2012,2013],{"class":286,"line":37},[284,2014,843],{"emptyLinePlaceholder":842},[284,2016,2017,2020,2023,2026,2028,2030,2032,2035,2037],{"class":286,"line":46},[284,2018,2019],{"class":548},"  async",[284,2021,2022],{"class":289}," handle",[284,2024,2025],{"class":292},"({ ",[284,2027,1855],{"class":807},[284,2029,744],{"class":292},[284,2031,1863],{"class":807},[284,2033,2034],{"class":292}," }, { ",[284,2036,808],{"class":807},[284,2038,2039],{"class":292}," }) {\n",[284,2041,2042,2045,2047,2050,2053],{"class":286,"line":51},[284,2043,2044],{"class":548},"    if",[284,2046,804],{"class":292},[284,2048,2049],{"class":548},"!",[284,2051,2052],{"class":292},"verified) ",[284,2054,2055],{"class":548},"return\n",[284,2057,2058],{"class":286,"line":56},[284,2059,843],{"emptyLinePlaceholder":842},[284,2061,2062,2065,2068,2071,2074],{"class":286,"line":64},[284,2063,2064],{"class":548},"    await",[284,2066,2067],{"class":289}," enqueue",[284,2069,2070],{"class":292},"(event, { kind: ",[284,2072,2073],{"class":318},"'crm-sync'",[284,2075,2076],{"class":292},", payload: { userId } })\n",[284,2078,2079],{"class":286,"line":73},[284,2080,2081],{"class":292},"  },\n",[284,2083,2084],{"class":286,"line":78},[284,2085,856],{"class":292},[178,2087,2088,2089,2091,2092,489],{},"Add a second listener on ",[182,2090,1870],{}," wherever an account can exist\nunproven: password signups and, under passwordless, guest-checkout accounts and\naccounts staff marked unverified (",[190,2093,81],{"href":80},[201,2095,2097],{"id":2096},"turn-auth-off","Turn auth off",[178,2099,2100,2103,2104,2106],{},[182,2101,2102],{},"auth.enabled: false"," mounts no auth route, and the ",[182,2105,176],{}," page middleware lets\neveryone through, so the dashboard is open and has no account menu. The build\nrefuses payments, organizations and notifications while auth is off, since each\nneeds a signed-in user.",[201,2108,20],{"id":2109},"configuration",[376,2111,2112,2125],{},[379,2113,2114],{},[382,2115,2116,2119,2122],{},[385,2117,2118],{},"Key",[385,2120,2121],{},"Default",[385,2123,2124],{},"What it does",[397,2126,2127,2141,2160,2183,2197,2216,2232,2249,2262],{},[382,2128,2129,2134,2138],{},[402,2130,2131],{},[182,2132,2133],{},"auth.enabled",[402,2135,2136],{},[182,2137,305],{},[402,2139,2140],{},"Off mounts no auth route and opens the dashboard.",[382,2142,2143,2148,2152],{},[402,2144,2145],{},[182,2146,2147],{},"auth.primary",[402,2149,2150],{},[182,2151,319],{},[402,2153,2154,744,2156,740,2158,256],{},[182,2155,319],{},[182,2157,423],{},[182,2159,438],{},[382,2161,2162,2167,2172],{},[402,2163,2164],{},[182,2165,2166],{},"auth.oauth",[402,2168,2169],{},[182,2170,2171],{},"[]",[402,2173,2174,2175,744,2177,744,2180,256],{},"Any of ",[182,2176,332],{},[182,2178,2179],{},"'google'",[182,2181,2182],{},"'microsoft'",[382,2184,2185,2190,2194],{},[402,2186,2187],{},[182,2188,2189],{},"auth.totp",[402,2191,2192],{},[182,2193,305],{},[402,2195,2196],{},"Authenticator codes as a second factor.",[382,2198,2199,2204,2208],{},[402,2200,2201],{},[182,2202,2203],{},"auth.passkey",[402,2205,2206],{},[182,2207,356],{},[402,2209,2210,744,2212,740,2214,256],{},[182,2211,356],{},[182,2213,470],{},[182,2215,673],{},[382,2217,2218,2223,2227],{},[402,2219,2220],{},[182,2221,2222],{},"auth.emailVerification",[402,2224,2225],{},[182,2226,305],{},[402,2228,2229,2230,256],{},"Password signups confirm their address first. Ignored under ",[182,2231,423],{},[382,2233,2234,2238,2243],{},[402,2235,2236],{},[182,2237,1522],{},[402,2239,2240],{},[182,2241,2242],{},"30",[402,2244,2245,2246,2248],{},"Days a deletion can be undone. ",[182,2247,673],{}," erases at once.",[382,2250,2251,2255,2259],{},[402,2252,2253],{},[182,2254,1768],{},[402,2256,2257],{},[182,2258,673],{},[402,2260,2261],{},"The current terms version. Changing it prompts everyone again.",[382,2263,2264,2269,2274],{},[402,2265,2266],{},[182,2267,2268],{},"access.firstAccountRole",[402,2270,2271],{},[182,2272,2273],{},"'superadmin'",[402,2275,2276,2277,489],{},"Role of the first account to prove its address (",[190,2278,921],{"href":53},[376,2280,2281,2290],{},[379,2282,2283],{},[382,2284,2285,2288],{},[385,2286,2287],{},"Variable",[385,2289,2124],{},[397,2291,2292,2307,2319,2332,2347,2356],{},[382,2293,2294,2299],{},[402,2295,2296],{},[182,2297,2298],{},"NUXT_SESSION_PASSWORD",[402,2300,2301,2302,2304,2305,256],{},"Cookie encryption key, 32+ characters. ",[182,2303,567],{}," writes one to ",[182,2306,532],{},[382,2308,2309,2314],{},[402,2310,2311],{},[182,2312,2313],{},"NUXT_SESSION_COOKIE_SECURE",[402,2315,517,2316,2318],{},[182,2317,673],{}," only for a production build served over plain http.",[382,2320,2321,2326],{},[402,2322,2323],{},[182,2324,2325],{},"NUXT_SITE_URL",[402,2327,2328,2329,2331],{},"Origin of every mailed link. Outside ",[182,2330,567],{},", sending a link fails without it.",[382,2333,2334,2342],{},[402,2335,2336,744,2339],{},[182,2337,2338],{},"NUXT_OAUTH_\u003CPROVIDER>_CLIENT_ID",[182,2340,2341],{},"NUXT_OAUTH_\u003CPROVIDER>_CLIENT_SECRET",[402,2343,2344,2345,256],{},"One pair per provider in ",[182,2346,2166],{},[382,2348,2349,2353],{},[402,2350,2351],{},[182,2352,592],{},[402,2354,2355],{},"Required with Microsoft.",[382,2357,2358,2364],{},[402,2359,2360,744,2362],{},[182,2361,1495],{},[182,2363,1498],{},[402,2365,2366],{},"Turn on the Turnstile check. Both or neither.",[201,2368,2370],{"id":2369},"good-to-know","Good to know",[446,2372,2373,2385,2392,2401,2411],{},[209,2374,2375,2377,2378,2381,2382,256],{},[182,2376,567],{}," has no Durable Objects, so every auth limit is off there and a\n",[182,2379,2380],{},"[rate-limit]"," warning is logged. Try lockouts under ",[182,2383,2384],{},"pnpm preview",[209,2386,1083,2387,2389,2390,256],{},[182,2388,476],{}," off, someone can register a victim's address first,\nadd a passkey or TOTP, and keep that way in after the owner resets the\npassword. Leave it on under ",[182,2391,480],{},[209,2393,2394,2395,2397,2398,2400],{},"A password reset never creates a first password. Before you drop a provider\nfrom ",[182,2396,2166],{},", check for accounts that only ever signed in through it:\nunder ",[182,2399,319],{}," they are locked out.",[209,2402,2403,2404,2406,2407,2410],{},"Mailed links use ",[182,2405,2325],{},", never the request's host. Set it at build,\nor ",[182,2408,2409],{},"NUXT_PUBLIC_SITE_URL"," on the Worker.",[209,2412,2413,2414,2416,2417,2419,2420,2423,2424,2431,2432,2435],{},"A column of yours that references ",[182,2415,1296],{},", or is named ",[182,2418,1580],{},", without\n",[182,2421,2422],{},"onDelete: 'cascade'"," fails\n",[190,2425,2428],{"href":2426,"rel":2427,"title":195},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Ftest\u002Funit\u002Fschema-cascades.test.ts",[194],[182,2429,2430],{},"test\u002Funit\u002Fschema-cascades.test.ts",",\nwhich reads ",[182,2433,2434],{},"custom\u002Fschema.ts"," too.",[2437,2438,2439],"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}html pre.shiki code .sovSZ, html code.shiki .sovSZ{--shiki-light:#22863A;--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":280,"searchDepth":17,"depth":17,"links":2441},[2442,2443,2444,2448,2449,2450,2451,2452,2453,2454,2455,2456,2457,2458],{"id":203,"depth":17,"text":204},{"id":272,"depth":17,"text":273},{"id":511,"depth":17,"text":512,"children":2445},[2446,2447],{"id":599,"depth":22,"text":600},{"id":679,"depth":22,"text":680},{"id":773,"depth":17,"text":774},{"id":1075,"depth":17,"text":488},{"id":1175,"depth":17,"text":1176},{"id":247,"depth":17,"text":1225},{"id":1304,"depth":17,"text":1305},{"id":1511,"depth":17,"text":1512},{"id":1762,"depth":17,"text":1763},{"id":1822,"depth":17,"text":1823},{"id":2096,"depth":17,"text":2097},{"id":2109,"depth":17,"text":20},{"id":2369,"depth":17,"text":2370},[2460,2461],{"path":34,"title":35,"description":36,"navTitle":35,"group":6,"order":37},{"path":48,"title":49,"description":50,"navTitle":49,"group":40,"order":51}]