[{"data":1,"prerenderedAt":881},["ShallowReactive",2],{"content-developer\u002Fadmin-dashboard":3,"surround-\u002Fdeveloper\u002Fadmin-dashboard":872},{"id":4,"title":5,"body":6,"description":864,"extension":865,"meta":866,"navigation":867,"path":868,"seo":869,"stem":870,"__hash__":871},"content\u002F3.developer\u002F11.admin-dashboard.md","Admin Dashboard",{"type":7,"value":8,"toc":848},"minimark",[9,18,30,35,112,118,122,126,131,138,148,152,159,176,180,423,426,429,458,462,545,549,622,629,633,640,696,700,706,748,752,844],[10,11,12,13,17],"p",{},"The admin dashboard (",[14,15,16],"code",{},"apps\u002Fadmin\u002F",") is a standalone Nuxt 3 application for platform-level administration. It is separate from the tenant-facing web app and provides tools for managing users, organizations, abuse detection, and delivery monitoring.",[19,20,23],"callout",{"title":21,"type":22},"Internal tool","info",[10,24,25,26,29],{},"The admin dashboard is an internal tool and is not exposed to end users. It requires the ",[14,27,28],{},"platform-admin"," role to access.",[31,32,34],"h2",{"id":33},"architecture","Architecture",[36,37,38,51],"table",{},[39,40,41],"thead",{},[42,43,44,48],"tr",{},[45,46,47],"th",{},"Aspect",[45,49,50],{},"Detail",[52,53,54,67,79,87,98],"tbody",{},[42,55,56,60],{},[57,58,59],"td",{},"Framework",[57,61,62,63,66],{},"Nuxt 3 (CSR-only, ",[14,64,65],{},"ssr: false",")",[42,68,69,72],{},[57,70,71],{},"UI Layer",[57,73,74,75,78],{},"Extends ",[14,76,77],{},"packages\u002Fui"," for shared components",[42,80,81,84],{},[57,82,83],{},"Backend",[57,85,86],{},"Convex (same deployment as main app)",[42,88,89,92],{},[57,90,91],{},"Auth",[57,93,94,95,97],{},"BetterAuth with ",[14,96,28],{}," role check",[42,99,100,103],{},[57,101,102],{},"Port",[57,104,105,108,109,66],{},[14,106,107],{},"3001"," (separate from web app on ",[14,110,111],{},"3000",[10,113,114,115,117],{},"The admin app shares the same Convex backend as the main web app. It uses platform-admin queries and mutations that are restricted to users with the ",[14,116,28],{}," role in the database.",[31,119,121],{"id":120},"authentication-flow","Authentication Flow",[123,124],"arch-flow",{":steps":125},"[{\"label\":\"Admin navigates to \u002Flogin\",\"detail\":\"Email + password form\",\"type\":\"action\"},{\"label\":\"BetterAuth\",\"detail\":\"Auth proxy forwards to Convex\",\"type\":\"gate\"},{\"label\":\"Session created\",\"detail\":\"JWT token issued\",\"type\":\"result\"},{\"label\":\"platform-admin role checked\",\"detail\":\"Query: isPlatformAdmin\",\"type\":\"gate\"},{\"label\":\"Dashboard access granted\",\"type\":\"final\"}]",[127,128,130],"h3",{"id":129},"auth-proxy-pattern","Auth Proxy Pattern",[10,132,133,134,137],{},"The admin app proxies authentication requests through a same-origin server route (",[14,135,136],{},"server\u002Fapi\u002Fauth\u002F[...].ts",") to avoid CORS issues. This route forwards requests to the Convex site URL, strips problematic headers (Host, Connection), and handles Set-Cookie domain adjustments for local development.",[139,140,145],"pre",{"className":141,"code":143,"language":144},[142],"language-text","Browser → \u002Fapi\u002Fauth\u002F* → Nuxt server route → Convex site URL → response proxied back\n","text",[14,146,143],{"__ignoreMap":147},"",[127,149,151],{"id":150},"token-management","Token Management",[10,153,154,155,158],{},"The ",[14,156,157],{},"lib\u002Fconvex-auth.ts"," module manages JWT tokens for Convex:",[160,161,162,166,169],"ul",{},[163,164,165],"li",{},"Caches the current token to avoid unnecessary auth calls",[163,167,168],{},"Implements a 60-second refresh buffer (refreshes before expiry)",[163,170,171,172,175],{},"Provides ",[14,173,174],{},"getConvexAuthToken()"," for the Convex client's auth callback",[31,177,179],{"id":178},"pages","Pages",[36,181,182,198],{},[39,183,184],{},[42,185,186,189,192,195],{},[45,187,188],{},"Route",[45,190,191],{},"Page",[45,193,194],{},"Middleware",[45,196,197],{},"Purpose",[52,199,200,218,236,253,270,287,304,321,338,355,372,389,406],{},[42,201,202,207,212,215],{},[57,203,204],{},[14,205,206],{},"\u002F",[57,208,209],{},[14,210,211],{},"index.vue",[57,213,214],{},"auth, platform-admin",[57,216,217],{},"Dashboard overview: flagged organizations, abuse metrics, pending reviews",[42,219,220,225,230,233],{},[57,221,222],{},[14,223,224],{},"\u002Flogin",[57,226,227],{},[14,228,229],{},"login.vue",[57,231,232],{},"—",[57,234,235],{},"Admin sign-in (email + password)",[42,237,238,243,248,250],{},[57,239,240],{},[14,241,242],{},"\u002Funauthorized",[57,244,245],{},[14,246,247],{},"unauthorized.vue",[57,249,232],{},[57,251,252],{},"Shown when user lacks platform-admin role",[42,254,255,260,265,267],{},[57,256,257],{},[14,258,259],{},"\u002Fusers",[57,261,262],{},[14,263,264],{},"users.vue",[57,266,214],{},[57,268,269],{},"User management",[42,271,272,277,282,284],{},[57,273,274],{},[14,275,276],{},"\u002Forganizations",[57,278,279],{},[14,280,281],{},"organizations\u002Findex.vue",[57,283,214],{},[57,285,286],{},"Organization listing with status, tier, risk level, bounce rates",[42,288,289,294,299,301],{},[57,290,291],{},[14,292,293],{},"\u002Forganizations\u002F[id]",[57,295,296],{},[14,297,298],{},"organizations\u002F[id].vue",[57,300,214],{},[57,302,303],{},"Organization detail and actions",[42,305,306,311,316,318],{},[57,307,308],{},[14,309,310],{},"\u002Fadmins",[57,312,313],{},[14,314,315],{},"admins.vue",[57,317,214],{},[57,319,320],{},"Platform admin management",[42,322,323,328,333,335],{},[57,324,325],{},[14,326,327],{},"\u002Fwaitlist",[57,329,330],{},[14,331,332],{},"waitlist.vue",[57,334,214],{},[57,336,337],{},"Waitlist user management (approve\u002Fdeny)",[42,339,340,345,350,352],{},[57,341,342],{},[14,343,344],{},"\u002Fanalytics",[57,346,347],{},[14,348,349],{},"analytics.vue",[57,351,214],{},[57,353,354],{},"Platform-wide analytics",[42,356,357,362,367,369],{},[57,358,359],{},[14,360,361],{},"\u002Faudit",[57,363,364],{},[14,365,366],{},"audit.vue",[57,368,214],{},[57,370,371],{},"Audit log viewer",[42,373,374,379,384,386],{},[57,375,376],{},[14,377,378],{},"\u002Fdelivery",[57,380,381],{},[14,382,383],{},"delivery.vue",[57,385,214],{},[57,387,388],{},"Delivery monitoring (bounce rates, blocked emails, active campaigns)",[42,390,391,396,401,403],{},[57,392,393],{},[14,394,395],{},"\u002Fdomains",[57,397,398],{},[14,399,400],{},"domains.vue",[57,402,214],{},[57,404,405],{},"Domain health across all organizations",[42,407,408,413,418,420],{},[57,409,410],{},[14,411,412],{},"\u002Freview",[57,414,415],{},[14,416,417],{},"review.vue",[57,419,214],{},[57,421,422],{},"Content review queue",[127,424,194],{"id":425},"middleware",[10,427,428],{},"Two middleware layers protect admin routes:",[430,431,432,444],"ol",{},[163,433,434,440,441,443],{},[435,436,437],"strong",{},[14,438,439],{},"auth.ts"," — redirects unauthenticated users to ",[14,442,224],{}," (with redirect query param)",[163,445,446,451,452,455,456],{},[435,447,448],{},[14,449,450],{},"platform-admin.ts"," — queries ",[14,453,454],{},"api.platformAdmin.isPlatformAdmin"," and redirects non-admins to ",[14,457,242],{},[31,459,461],{"id":460},"composables","Composables",[36,463,464,473],{},[39,465,466],{},[42,467,468,471],{},[45,469,470],{},"Composable",[45,472,197],{},[52,474,475,503,521,535],{},[42,476,477,482],{},[57,478,479],{},[14,480,481],{},"useAuth()",[57,483,484,485,488,489,488,492,488,495,498,499,502],{},"BetterAuth session management: ",[14,486,487],{},"signInWithEmail()",", ",[14,490,491],{},"signOut()",[14,493,494],{},"waitUntilReady()",[14,496,497],{},"refetch()",". Exposes reactive ",[14,500,501],{},"status"," (pending\u002Fauthenticated\u002Funauthenticated\u002Ferror).",[42,504,505,510],{},[57,506,507],{},[14,508,509],{},"useConvexQuery()",[57,511,512,513,516,517,520],{},"Reactive Convex query wrapper. Returns ",[14,514,515],{},"{ data, error, isLoading }",". Deep-watches args for reactivity. Supports ",[14,518,519],{},"'skip'"," pattern for conditional queries.",[42,522,523,528],{},[57,524,525],{},[14,526,527],{},"useConvexMutation()",[57,529,530,531,534],{},"Convex mutation executor. Returns ",[14,532,533],{},"{ mutate, isLoading, error }",".",[42,536,537,542],{},[57,538,539],{},[14,540,541],{},"useConvex()",[57,543,544],{},"Base composable returning the Convex client instance.",[127,546,548],{"id":547},"query-usage-pattern","Query Usage Pattern",[139,550,554],{"className":551,"code":552,"language":553,"meta":147,"style":147},"language-typescript shiki shiki-themes github-light github-dark-dimmed","const { data: organizations, isLoading } = useConvexQuery(\n  api.platformAdmin.listOrganizations,\n  () => ({ status: selectedStatus.value })\n)\n","typescript",[14,555,556,598,604,616],{"__ignoreMap":147},[557,558,561,565,569,573,576,580,582,585,588,591,595],"span",{"class":559,"line":560},"line",1,[557,562,564],{"class":563},"s7YZ4","const",[557,566,568],{"class":567},"sYgZi"," { ",[557,570,572],{"class":571},"stnAF","data",[557,574,575],{"class":567},": ",[557,577,579],{"class":578},"sviXB","organizations",[557,581,488],{"class":567},[557,583,584],{"class":578},"isLoading",[557,586,587],{"class":567}," } ",[557,589,590],{"class":563},"=",[557,592,594],{"class":593},"sPO5f"," useConvexQuery",[557,596,597],{"class":567},"(\n",[557,599,601],{"class":559,"line":600},2,[557,602,603],{"class":567},"  api.platformAdmin.listOrganizations,\n",[557,605,607,610,613],{"class":559,"line":606},3,[557,608,609],{"class":567},"  () ",[557,611,612],{"class":563},"=>",[557,614,615],{"class":567}," ({ status: selectedStatus.value })\n",[557,617,619],{"class":559,"line":618},4,[557,620,621],{"class":567},")\n",[10,623,624,625,628],{},"The args factory function is deep-watched — when ",[14,626,627],{},"selectedStatus"," changes, the query automatically re-subscribes with new arguments.",[31,630,632],{"id":631},"environment-variables","Environment Variables",[10,634,635,636,639],{},"Set in ",[14,637,638],{},"apps\u002Fadmin\u002F.env",":",[36,641,642,655],{},[39,643,644],{},[42,645,646,649,652],{},[45,647,648],{},"Variable",[45,650,651],{},"Description",[45,653,654],{},"Default",[52,656,657,669,681],{},[42,658,659,664,667],{},[57,660,661],{},[14,662,663],{},"NUXT_PUBLIC_CONVEX_URL",[57,665,666],{},"Convex deployment URL (same as web app)",[57,668,232],{},[42,670,671,676,679],{},[57,672,673],{},[14,674,675],{},"NUXT_PUBLIC_CONVEX_SITE_URL",[57,677,678],{},"Convex site URL (same as web app, used for auth proxy)",[57,680,232],{},[42,682,683,688,691],{},[57,684,685],{},[14,686,687],{},"NUXT_PUBLIC_SITE_URL",[57,689,690],{},"Admin dashboard URL",[57,692,693],{},[14,694,695],{},"http:\u002F\u002Flocalhost:3001",[31,697,699],{"id":698},"security","Security",[10,701,702,703,639],{},"The admin app is configured with strict security headers via ",[14,704,705],{},"nuxt-security",[160,707,708,724,730,740],{},[163,709,710,713,714,488,717,720,721],{},[435,711,712],{},"CSP"," — strict ",[14,715,716],{},"script-src",[14,718,719],{},"object-src: 'none'",", Convex URL in ",[14,722,723],{},"connect-src",[163,725,726,729],{},[435,727,728],{},"HSTS"," — enabled with long max-age",[163,731,732,735,736,739],{},[435,733,734],{},"X-Frame-Options"," — ",[14,737,738],{},"DENY"," (prevents embedding)",[163,741,742,735,745],{},[435,743,744],{},"Referrer-Policy",[14,746,747],{},"strict-origin-when-cross-origin",[31,749,751],{"id":750},"key-files","Key Files",[36,753,754,763],{},[39,755,756],{},[42,757,758,761],{},[45,759,760],{},"File",[45,762,197],{},[52,764,765,775,785,795,805,814,824,834],{},[42,766,767,772],{},[57,768,769],{},[14,770,771],{},"nuxt.config.ts",[57,773,774],{},"App configuration, security headers, runtime config",[42,776,777,782],{},[57,778,779],{},[14,780,781],{},"app\u002Flib\u002Fauth-client.ts",[57,783,784],{},"BetterAuth client initialization",[42,786,787,792],{},[57,788,789],{},[14,790,791],{},"app\u002Flib\u002Fconvex-auth.ts",[57,793,794],{},"JWT token caching and refresh",[42,796,797,802],{},[57,798,799],{},[14,800,801],{},"app\u002Fplugins\u002Fconvex.client.ts",[57,803,804],{},"Convex client initialization and auth wiring",[42,806,807,811],{},[57,808,809],{},[14,810,136],{},[57,812,813],{},"Auth proxy server route",[42,815,816,821],{},[57,817,818],{},[14,819,820],{},"app\u002Fmiddleware\u002Fauth.ts",[57,822,823],{},"Authentication middleware",[42,825,826,831],{},[57,827,828],{},[14,829,830],{},"app\u002Fmiddleware\u002Fplatform-admin.ts",[57,832,833],{},"Platform admin role middleware",[42,835,836,841],{},[57,837,838],{},[14,839,840],{},"app\u002Flayouts\u002Fdefault.vue",[57,842,843],{},"Sidebar layout with navigation",[845,846,847],"style",{},"html pre.shiki code .s7YZ4, html code.shiki .s7YZ4{--shiki-default:#D73A49;--shiki-dark:#F47067}html pre.shiki code .sYgZi, html code.shiki .sYgZi{--shiki-default:#24292E;--shiki-dark:#ADBAC7}html pre.shiki code .stnAF, html code.shiki .stnAF{--shiki-default:#E36209;--shiki-dark:#F69D50}html pre.shiki code .sviXB, html code.shiki .sviXB{--shiki-default:#005CC5;--shiki-dark:#6CB6FF}html pre.shiki code .sPO5f, html code.shiki .sPO5f{--shiki-default:#6F42C1;--shiki-dark:#DCBDFB}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":147,"searchDepth":600,"depth":600,"links":849},[850,851,855,858,861,862,863],{"id":33,"depth":600,"text":34},{"id":120,"depth":600,"text":121,"children":852},[853,854],{"id":129,"depth":606,"text":130},{"id":150,"depth":606,"text":151},{"id":178,"depth":600,"text":179,"children":856},[857],{"id":425,"depth":606,"text":194},{"id":460,"depth":600,"text":461,"children":859},[860],{"id":547,"depth":606,"text":548},{"id":631,"depth":600,"text":632},{"id":698,"depth":600,"text":699},{"id":750,"depth":600,"text":751},"Platform administration dashboard for managing users, organizations, abuse detection, and delivery monitoring.","md",{},true,"\u002Fdeveloper\u002Fadmin-dashboard",{"title":5,"description":864},"3.developer\u002F11.admin-dashboard","6K41CCfs_92jjMGH9xTVrsLbsKfIB4j3DCLO-j4Nrp0",[873,877],{"title":874,"path":875,"stem":876,"children":-1},"MTA System","\u002Fdeveloper\u002Fmta-system","3.developer\u002F10.mta-system",{"title":878,"path":879,"stem":880,"children":-1},"How Email Works","\u002Fdeveloper\u002Fhow-email-works","3.developer\u002F12.how-email-works",1774391042010]