[{"data":1,"prerenderedAt":912},["ShallowReactive",2],{"navigation:en":3,"\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fplatform-admin:en":311},[4],{"title":5,"path":6,"stem":7,"children":8,"icon":10},"Nuxfire Docs","\u002Fdocs","docs\u002F0.index",[9,11,152],{"title":5,"path":6,"stem":7,"icon":10},"i-lucide-layout-grid",{"title":12,"path":13,"stem":14,"children":15,"icon":151},"SaaS Starter Kit","\u002Fdocs\u002Fsaas-starter-kit","docs\u002F1.saas-starter-kit\u002F0.index",[16,19,63,129],{"title":17,"path":13,"stem":14,"icon":18},"What is the SaaS Starter Kit?","i-lucide-info",{"title":20,"icon":21,"hide":22,"path":23,"stem":24,"children":25,"page":62},"Getting Started","i-lucide-play",true,"\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started","docs\u002F1.saas-starter-kit\u002F1.getting-started",[26,30,34,38,42,46,50,54,58],{"title":27,"path":28,"stem":29},"Quick Start","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fquick-start","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F0.quick-start",{"title":31,"path":32,"stem":33},"Installation","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Finstallation","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F1.installation",{"title":35,"path":36,"stem":37},"Cloudflare Access","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fcloudflare-access","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F2.cloudflare-access",{"title":39,"path":40,"stem":41},"Configuration","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fconfiguration","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F3.configuration",{"title":43,"path":44,"stem":45},"Secrets","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fsecrets","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F4.secrets",{"title":47,"path":48,"stem":49},"Environment Variables","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fenvironment-variables","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F5.environment-variables",{"title":51,"path":52,"stem":53},"Deploy","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fdeploy","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F6.deploy",{"title":55,"path":56,"stem":57},"Local Development","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Flocal-development","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F7.local-development",{"title":59,"path":60,"stem":61},"Project Structure","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fproject-structure","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F8.project-structure",false,{"title":64,"icon":65,"hide":22,"path":66,"stem":67,"children":68,"page":62},"Features","i-lucide-box","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures","docs\u002F1.saas-starter-kit\u002F2.features",[69,73,77,81,85,89,93,97,101,105,109,113,117,121,125],{"title":70,"path":71,"stem":72},"Authentication","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fauth","docs\u002F1.saas-starter-kit\u002F2.features\u002F1.auth",{"title":74,"path":75,"stem":76},"Rate Limiting","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Frate-limiting","docs\u002F1.saas-starter-kit\u002F2.features\u002F10.rate-limiting",{"title":78,"path":79,"stem":80},"WebSockets","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fwebsockets","docs\u002F1.saas-starter-kit\u002F2.features\u002F11.websockets",{"title":82,"path":83,"stem":84},"Blog","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fblog","docs\u002F1.saas-starter-kit\u002F2.features\u002F14.blog",{"title":86,"path":87,"stem":88},"Documentation","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fdocumentation","docs\u002F1.saas-starter-kit\u002F2.features\u002F15.documentation",{"title":90,"path":91,"stem":92},"Analytics","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fanalytics","docs\u002F1.saas-starter-kit\u002F2.features\u002F16.analytics",{"title":94,"path":95,"stem":96},"Platform Admin Console","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fplatform-admin","docs\u002F1.saas-starter-kit\u002F2.features\u002F17.platform-admin",{"title":98,"path":99,"stem":100},"Database","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fdatabase","docs\u002F1.saas-starter-kit\u002F2.features\u002F2.database",{"title":102,"path":103,"stem":104},"Payments & Billing","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fpayments","docs\u002F1.saas-starter-kit\u002F2.features\u002F3.payments",{"title":106,"path":107,"stem":108},"Emails","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Femails","docs\u002F1.saas-starter-kit\u002F2.features\u002F4.emails",{"title":110,"path":111,"stem":112},"Notifications","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fnotifications","docs\u002F1.saas-starter-kit\u002F2.features\u002F5.notifications",{"title":114,"path":115,"stem":116},"Role-Based Access Control","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Frbac","docs\u002F1.saas-starter-kit\u002F2.features\u002F6.rbac",{"title":118,"path":119,"stem":120},"Async Jobs","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fasync-jobs","docs\u002F1.saas-starter-kit\u002F2.features\u002F7.async-jobs",{"title":122,"path":123,"stem":124},"Storage","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fstorage","docs\u002F1.saas-starter-kit\u002F2.features\u002F8.storage",{"title":126,"path":127,"stem":128},"Internationalization","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Finternationalization","docs\u002F1.saas-starter-kit\u002F2.features\u002F9.internationalization",{"title":130,"icon":131,"path":132,"stem":133,"children":134,"page":62},"Concepts","i-lucide-graduation-cap","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts","docs\u002F1.saas-starter-kit\u002F3.concepts",[135,139,143,147],{"title":136,"path":137,"stem":138},"Technology Stack","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts\u002Ftech-stack","docs\u002F1.saas-starter-kit\u002F3.concepts\u002F0.tech-stack",{"title":140,"path":141,"stem":142},"SST.dev","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts\u002Fsst","docs\u002F1.saas-starter-kit\u002F3.concepts\u002F1.sst",{"title":144,"path":145,"stem":146},"NuxtHub","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts\u002Fnuxt-hub","docs\u002F1.saas-starter-kit\u002F3.concepts\u002F2.nuxt-hub",{"title":148,"path":149,"stem":150},"Design System & Branding","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts\u002Fdesign-system","docs\u002F1.saas-starter-kit\u002F3.concepts\u002F3.design-system","i-lucide-zap",{"title":153,"path":154,"stem":155,"children":156,"icon":159},"Fireskills","\u002Fdocs\u002Ffireskills","docs\u002F2.fireskills\u002F0.index",[157,160,219,249,275,293],{"title":158,"path":154,"stem":155,"icon":159},"What is Fireskills?","i-lucide-flame",{"title":20,"icon":21,"hide":22,"path":161,"stem":162,"children":163,"page":62},"\u002Fdocs\u002Ffireskills\u002Fgetting-started","docs\u002F2.fireskills\u002F1.getting-started",[164,167,171,175,179,183,187,191,195,199,203,207,211,215],{"title":27,"path":165,"stem":166},"\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fquick-start","docs\u002F2.fireskills\u002F1.getting-started\u002F00.quick-start",{"title":168,"path":169,"stem":170},"Workflow & Governance","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fworkflow","docs\u002F2.fireskills\u002F1.getting-started\u002F01.workflow",{"title":172,"path":173,"stem":174},"Project Context","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fproject-context","docs\u002F2.fireskills\u002F1.getting-started\u002F02.project-context",{"title":176,"path":177,"stem":178},"Step 0: Setup","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-setup","docs\u002F2.fireskills\u002F1.getting-started\u002F03.step-setup",{"title":180,"path":181,"stem":182},"Step 1: Inbox","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-inbox","docs\u002F2.fireskills\u002F1.getting-started\u002F04.step-inbox",{"title":184,"path":185,"stem":186},"Step 2: Backlog","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-backlog","docs\u002F2.fireskills\u002F1.getting-started\u002F05.step-backlog",{"title":188,"path":189,"stem":190},"Step 3: Specify","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-specify","docs\u002F2.fireskills\u002F1.getting-started\u002F06.step-specify",{"title":192,"path":193,"stem":194},"Step 4: Validate","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-validate","docs\u002F2.fireskills\u002F1.getting-started\u002F07.step-validate",{"title":196,"path":197,"stem":198},"Step 5: Tasks","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-tasks","docs\u002F2.fireskills\u002F1.getting-started\u002F08.step-tasks",{"title":200,"path":201,"stem":202},"Step 6: TDD\u002FBDD","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-tdd-bdd","docs\u002F2.fireskills\u002F1.getting-started\u002F09.step-tdd-bdd",{"title":204,"path":205,"stem":206},"Step 7: Implement","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-implement","docs\u002F2.fireskills\u002F1.getting-started\u002F10.step-implement",{"title":208,"path":209,"stem":210},"Step 8: Documentator","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-documentator","docs\u002F2.fireskills\u002F1.getting-started\u002F11.step-documentator",{"title":212,"path":213,"stem":214},"Late Changes","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Flate-changes","docs\u002F2.fireskills\u002F1.getting-started\u002F12.late-changes",{"title":216,"path":217,"stem":218},"Other Entry Points","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fother-entry-points","docs\u002F2.fireskills\u002F1.getting-started\u002F13.other-entry-points",{"title":220,"icon":221,"hide":22,"path":222,"stem":223,"children":224,"page":62},"Methodology","i-lucide-git-branch","\u002Fdocs\u002Ffireskills\u002Fmethodology","docs\u002F2.fireskills\u002F2.methodology",[225,229,233,237,241,245],{"title":226,"path":227,"stem":228},"The Three Acts","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Fthree-acts","docs\u002F2.fireskills\u002F2.methodology\u002F0.three-acts",{"title":230,"path":231,"stem":232},"The Three Gates","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Fthree-gates","docs\u002F2.fireskills\u002F2.methodology\u002F1.three-gates",{"title":234,"path":235,"stem":236},"The Canonical Spec","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Fcanonical-spec","docs\u002F2.fireskills\u002F2.methodology\u002F2.canonical-spec",{"title":238,"path":239,"stem":240},"Lifecycle States","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Flifecycle-states","docs\u002F2.fireskills\u002F2.methodology\u002F3.lifecycle-states",{"title":242,"path":243,"stem":244},"Effort Scale","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Feffort-scale","docs\u002F2.fireskills\u002F2.methodology\u002F4.effort-scale",{"title":246,"path":247,"stem":248},"Milestones & Governance","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Fgovernance-and-milestones","docs\u002F2.fireskills\u002F2.methodology\u002F5.governance-and-milestones",{"title":250,"icon":251,"hide":22,"path":252,"stem":253,"children":254,"page":62},"Skills","i-lucide-layers","\u002Fdocs\u002Ffireskills\u002Fskills","docs\u002F2.fireskills\u002F3.skills",[255,259,263,267,271],{"title":256,"path":257,"stem":258},"Catalog","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fcatalog","docs\u002F2.fireskills\u002F3.skills\u002F0.catalog",{"title":260,"path":261,"stem":262},"Act I: Define","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fact-1-define","docs\u002F2.fireskills\u002F3.skills\u002F1.act-1-define",{"title":264,"path":265,"stem":266},"Act II: Design & Prove","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fact-2-design-prove","docs\u002F2.fireskills\u002F3.skills\u002F2.act-2-design-prove",{"title":268,"path":269,"stem":270},"Act III: Deliver","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fact-3-deliver","docs\u002F2.fireskills\u002F3.skills\u002F3.act-3-deliver",{"title":272,"path":273,"stem":274},"Governance & Decisions","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fgovernance-and-decisions","docs\u002F2.fireskills\u002F3.skills\u002F4.governance-and-decisions",{"title":276,"icon":277,"hide":22,"path":278,"stem":279,"children":280,"page":62},"Specialists","i-lucide-sparkles","\u002Fdocs\u002Ffireskills\u002Fspecialists","docs\u002F2.fireskills\u002F4.specialists",[281,285,289],{"title":282,"path":283,"stem":284},"Overview","\u002Fdocs\u002Ffireskills\u002Fspecialists\u002Foverview","docs\u002F2.fireskills\u002F4.specialists\u002F0.overview",{"title":286,"path":287,"stem":288},"Nuxfire Stack","\u002Fdocs\u002Ffireskills\u002Fspecialists\u002Fnuxfire-stack","docs\u002F2.fireskills\u002F4.specialists\u002F1.nuxfire-stack",{"title":290,"path":291,"stem":292},"Engineering Specialists","\u002Fdocs\u002Ffireskills\u002Fspecialists\u002Fengineering-specialists","docs\u002F2.fireskills\u002F4.specialists\u002F2.engineering-specialists",{"title":294,"icon":295,"hide":22,"path":296,"stem":297,"children":298,"page":62},"CLI & Tooling","i-lucide-terminal","\u002Fdocs\u002Ffireskills\u002Fcli","docs\u002F2.fireskills\u002F5.cli",[299,303,307],{"title":300,"path":301,"stem":302},"CLI Overview","\u002Fdocs\u002Ffireskills\u002Fcli\u002Foverview","docs\u002F2.fireskills\u002F5.cli\u002F0.overview",{"title":304,"path":305,"stem":306},"Commands","\u002Fdocs\u002Ffireskills\u002Fcli\u002Fcommands","docs\u002F2.fireskills\u002F5.cli\u002F1.commands",{"title":308,"path":309,"stem":310},"Safety Scripts","\u002Fdocs\u002Ffireskills\u002Fcli\u002Fgovernance-scripts","docs\u002F2.fireskills\u002F5.cli\u002F2.governance-scripts",{"id":312,"title":94,"body":313,"description":907,"extension":908,"meta":909,"navigation":22,"path":95,"seo":910,"stem":96,"__hash__":911},"docs\u002Fdocs\u002F1.saas-starter-kit\u002F2.features\u002F17.platform-admin.md",{"type":314,"value":315,"toc":897},"minimark",[316,333,338,346,361,371,456,466,480,585,596,615,619,622,626,636,691,720,724,733,736,761,765,833,837,851,855,865,880,893],[317,318,319,323,324,328,329,332],"p",{},[320,321,322],"code",{},"\u002Fadmin"," is a separate authority system for the SaaS owner\u002Foperator, not for tenants. It does ",[325,326,327],"strong",{},"not"," use the per-tenant RBAC the rest of the app uses (",[330,331,114],"a",{"href":115},") — nothing here is scoped by team.",[334,335,337],"h2",{"id":336},"step-by-step-get-your-first-platform-admin-account","Step-by-step: get your first platform-admin account",[317,339,340,341,345],{},"There is no signup form for platform admins. On purpose — see ",[330,342,344],{"href":343},"#why-theres-no-signup-form","Why there's no signup form"," below. Instead: a real user signs up first, then gets promoted by someone with database access.",[317,347,348,349,352,353,356,357,360],{},"Do this once ",[325,350,351],{},"per environment"," — once for ",[320,354,355],{},"stage",", once for ",[320,358,359],{},"production"," — not once per project.",[317,362,363,366,367,370],{},[325,364,365],{},"1. Deploy with the two required secrets set."," Every ",[320,368,369],{},"\u002Fadmin\u002F*"," route refuses to work without both — it fails closed, never degrades to an insecure mode:",[372,373,378],"pre",{"className":374,"code":375,"language":376,"meta":377,"style":377},"language-bash shiki shiki-themes material-theme-lighter github-light github-dark monokai","sst secret set PLATFORM_MFA_SESSION_SECRET \"$(openssl rand -base64 32)\"\nsst secret set PLATFORM_ADMIN_ENCRYPTION_KEY \"$(openssl rand -base64 32)\"\nbun run deploy\n","bash","",[320,379,380,420,444],{"__ignoreMap":377},[381,382,385,389,393,396,399,403,406,409,413,417],"span",{"class":383,"line":384},"line",1,[381,386,388],{"class":387},"sR7ES","sst",[381,390,392],{"class":391},"sLACW"," secret",[381,394,395],{"class":391}," set",[381,397,398],{"class":391}," PLATFORM_MFA_SESSION_SECRET",[381,400,402],{"class":401},"siCPE"," \"$(",[381,404,405],{"class":387},"openssl",[381,407,408],{"class":391}," rand ",[381,410,412],{"class":411},"sFhLe","-base64",[381,414,416],{"class":415},"sYThS"," 32",[381,418,419],{"class":401},")\"\n",[381,421,423,425,427,429,432,434,436,438,440,442],{"class":383,"line":422},2,[381,424,388],{"class":387},[381,426,392],{"class":391},[381,428,395],{"class":391},[381,430,431],{"class":391}," PLATFORM_ADMIN_ENCRYPTION_KEY",[381,433,402],{"class":401},[381,435,405],{"class":387},[381,437,408],{"class":391},[381,439,412],{"class":411},[381,441,416],{"class":415},[381,443,419],{"class":401},[381,445,447,450,453],{"class":383,"line":446},3,[381,448,449],{"class":387},"bun",[381,451,452],{"class":391}," run",[381,454,455],{"class":391}," deploy\n",[317,457,458,461,462,465],{},[325,459,460],{},"2. The future owner signs up like any normal user"," — ",[320,463,464],{},"\u002Fsignup"," or an OAuth provider, choosing their own password. Nobody else ever needs to know this password.",[317,467,468,471,472,475,476,479],{},[325,469,470],{},"3. Promote that account to platform admin."," Run this from ",[320,473,474],{},"apps\u002Ffunctions"," (not the repo root — that's the #1 mistake here), with the stage's real ",[320,477,478],{},"DATABASE_*"," values as environment variables:",[372,481,483],{"className":374,"code":482,"language":376,"meta":377,"style":377},"cd apps\u002Ffunctions\n$env:DATABASE_HOST=\"...\"\n$env:DATABASE_PORT=\"5432\"\n$env:DATABASE_NAME=\"...\"\n$env:DATABASE_USER=\"...\"\n$env:DATABASE_PASSWORD=\"...\"\nbun run src\u002Fdatabase\u002Fgrant-platform-admin.ts --email=owner@yourdomain.com --role=SUPER_ADMIN\n",[320,484,485,494,513,527,541,555,569],{"__ignoreMap":377},[381,486,487,491],{"class":383,"line":384},[381,488,490],{"class":489},"sMLJd","cd",[381,492,493],{"class":391}," apps\u002Ffunctions\n",[381,495,496,500,504,507,510],{"class":383,"line":422},[381,497,499],{"class":498},"ss--_","$env:DATABASE_HOST",[381,501,503],{"class":502},"sGXK2","=",[381,505,506],{"class":401},"\"",[381,508,509],{"class":391},"...",[381,511,512],{"class":401},"\"\n",[381,514,515,518,520,522,525],{"class":383,"line":446},[381,516,517],{"class":498},"$env:DATABASE_PORT",[381,519,503],{"class":502},[381,521,506],{"class":401},[381,523,524],{"class":391},"5432",[381,526,512],{"class":401},[381,528,530,533,535,537,539],{"class":383,"line":529},4,[381,531,532],{"class":498},"$env:DATABASE_NAME",[381,534,503],{"class":502},[381,536,506],{"class":401},[381,538,509],{"class":391},[381,540,512],{"class":401},[381,542,544,547,549,551,553],{"class":383,"line":543},5,[381,545,546],{"class":498},"$env:DATABASE_USER",[381,548,503],{"class":502},[381,550,506],{"class":401},[381,552,509],{"class":391},[381,554,512],{"class":401},[381,556,558,561,563,565,567],{"class":383,"line":557},6,[381,559,560],{"class":498},"$env:DATABASE_PASSWORD",[381,562,503],{"class":502},[381,564,506],{"class":401},[381,566,509],{"class":391},[381,568,512],{"class":401},[381,570,572,574,576,579,582],{"class":383,"line":571},7,[381,573,449],{"class":387},[381,575,452],{"class":391},[381,577,578],{"class":391}," src\u002Fdatabase\u002Fgrant-platform-admin.ts",[381,580,581],{"class":411}," --email=owner@yourdomain.com",[381,583,584],{"class":411}," --role=SUPER_ADMIN\n",[317,586,587,588,591,592,595],{},"This only works if that email already signed up in step 2 — the script errors with ",[320,589,590],{},"no user found"," otherwise. It never touches passwords, only grants a row in the ",[320,593,594],{},"PlatformAdmin"," table. It's a manual script by design, never run automatically on deploy: an automated grant is exactly how \"one forgotten env var\" turns into a permanent super-admin nobody remembers creating.",[317,597,598,604,605,608,609,612,613,603],{},[325,599,600,601,603],{},"4. The owner logs in normally, then visits ",[320,602,322],{},"."," The app takes it from there: it detects MFA isn't set up yet and opens the enrollment dialog automatically — a centered, blurred-backdrop modal (the same ",[320,606,607],{},"AccountMfaSetupModal"," component used for a regular user's own 2FA setup, just pointed at the ",[320,610,611],{},"\u002Fapi\u002Fadmin\u002Fmfa\u002F*"," endpoints instead). Click \"Generate code\", scan the QR with any authenticator app (Google Authenticator, 1Password, Authy...), and enter the 6-digit code it shows. That's it — the account now has full access to ",[320,614,322],{},[616,617,344],"h3",{"id":618},"why-theres-no-signup-form",[317,620,621],{},"A self-serve \"become the admin\" form would have to sit at a public URL before anyone owns the platform — meaning whoever finds that URL first gets platform authority. There's no way to lock it down at that point, since locking it down is the whole point of having an admin in the first place. Promoting an existing, already-authenticated account sidesteps the problem entirely: authority is only ever handed to an account that's already provably owned by one person.",[616,623,625],{"id":624},"local-development-and-demos-only-seed-both-accounts-at-once","Local development and demos only: seed both accounts at once",[317,627,628,631,632,635],{},[320,629,630],{},"seed\u002Fdemo-accounts.ts"," creates ",[325,633,634],{},"both"," a platform owner and a demo tenant client in one call, with known default credentials — convenient for trying the product end to end, wrong for anything a stranger could reach:",[372,637,639],{"className":374,"code":638,"language":376,"meta":377,"style":377},"AUTH_URL=... AUTH_ADMIN_SECRET=... \\\n  bun run src\u002Fdatabase\u002Fseed\u002Fdemo-accounts.ts \\\n  --owner-email=owner@yourdomain.com --client-email=client@yourdomain.com \\\n  --owner-password=... --client-password=...\n",[320,640,641,660,673,683],{"__ignoreMap":377},[381,642,643,646,648,650,653,655,657],{"class":383,"line":384},[381,644,645],{"class":498},"AUTH_URL",[381,647,503],{"class":502},[381,649,509],{"class":391},[381,651,652],{"class":498}," AUTH_ADMIN_SECRET",[381,654,503],{"class":502},[381,656,509],{"class":391},[381,658,659],{"class":387}," \\\n",[381,661,662,665,667,670],{"class":383,"line":422},[381,663,664],{"class":391},"  bun",[381,666,452],{"class":391},[381,668,669],{"class":391}," src\u002Fdatabase\u002Fseed\u002Fdemo-accounts.ts",[381,671,659],{"class":672},"sQeA1",[381,674,675,678,681],{"class":383,"line":446},[381,676,677],{"class":411},"  --owner-email=owner@yourdomain.com",[381,679,680],{"class":411}," --client-email=client@yourdomain.com",[381,682,659],{"class":672},[381,684,685,688],{"class":383,"line":529},[381,686,687],{"class":411},"  --owner-password=...",[381,689,690],{"class":411}," --client-password=...\n",[317,692,693,694,696,697,700,701,704,705,708,709,696,712,715,716,719],{},"(",[320,695,645],{},"\u002F",[320,698,699],{},"AUTH_ADMIN_SECRET"," come from the shell environment, not the vault — see the script's own header comment for exactly where to read them from a deployed stage.) ",[320,702,703],{},"--owner-email"," and ",[320,706,707],{},"--client-email"," are required — there is no default address, the accounts are created on real addresses of yours. ",[320,710,711],{},"--owner-password",[320,713,714],{},"--client-password"," override the throwaway defaults defined in the script's ",[320,717,718],{},"parseArgs()"," — if you ever run this against an environment anyone outside your team can reach, always pass your own passwords, never the defaults. The defaults exist purely for local\u002Fdemo convenience, precisely because they're meant to be throwaway; left unset on a reachable environment, a known owner password is a live credential-stuffing target, not a placeholder.",[334,721,723],{"id":722},"mfa-after-the-first-time-what-to-expect","MFA after the first time: what to expect",[317,725,726,727,729,730,732],{},"Once enrolled, ",[320,728,322],{}," stays open for 30 minutes at a time. After that, visiting any ",[320,731,322],{}," page re-opens the same blurred modal — but it now just asks for a fresh 6-digit code instead of showing a QR again. Nothing to re-scan, no re-enrolling.",[317,734,735],{},"Wrong codes get rate-limited (10 attempts per 5 minutes, per account) — if you're locked out, wait a few minutes rather than retrying immediately.",[317,737,738,741,742,745,746,749,750,753,754,757,758,760],{},[325,739,740],{},"Recovery codes:"," enrolling issues ten single-use recovery codes, shown once. If you lose your authenticator, choose ",[325,743,744],{},"Lost your authenticator?"," at the prompt and enter one in place of the six-digit code. ",[325,747,748],{},"Generate new codes"," on the console overview replaces the set after confirming an authenticator code. ",[325,751,752],{},"Known limit:"," if both are lost, an operator resets the factor with ",[320,755,756],{},"apps\u002Ffunctions\u002Fsrc\u002Fdatabase\u002Freset-mfa.ts --scope=platform"," (manual and audited), then you enroll again from scratch — visit ",[320,759,322],{}," and follow the enrollment modal.",[334,762,764],{"id":763},"what-the-console-does","What the console does",[766,767,768,779,795,804,821],"ul",{},[769,770,771,774,775,778],"li",{},[325,772,773],{},"Companies"," (",[320,776,777],{},"\u002Fadmin\u002Fcompanies",") — cross-tenant list of every team, with search, plan and status filters; suspend and reactivate any team. Every action is scoped explicitly by team id, never by the caller's own memberships.",[769,780,781,774,784,787,788,704,791,794],{},[325,782,783],{},"Plans",[320,785,786],{},"\u002Fadmin\u002Fplans",") — create, edit, archive\u002Funarchive plans (archiving is a soft delete: ",[320,789,790],{},"Team.planId",[320,792,793],{},"Subscription.planId"," are real foreign keys, so a plan row is never hard-deleted while teams reference it). Also runs the one-off Stripe cancel-configuration setup.",[769,796,797,774,800,803],{},[325,798,799],{},"Queue",[320,801,802],{},"\u002Fadmin\u002Fqueue",") — job status counts, dead-letter messages with replay, and the last 20 cron log lines. Job processing is treated as infrastructure of the SaaS itself, not a tenant feature.",[769,805,806,774,809,812,813,816,817,820],{},[325,807,808],{},"Audit log",[320,810,811],{},"\u002Fadmin\u002Faudit-log",") — read-only trail of every platform-admin mutation: ",[320,814,815],{},"actorUserId · action · target · ip · createdAt",". Writing is centralized in one function (",[320,818,819],{},"recordAuditLog","), never a direct insert from a router, so no future admin route can forget to log. The actor is looked up by a best-effort join, not a foreign key — a line survives the actor's own account being deleted later, which is exactly the line an operator most needs to see.",[769,822,823,774,825,828,829,832],{},[325,824,110],{},[320,826,827],{},"\u002Fadmin\u002Fnotifications",") — a live demo of the real-time notification pipeline aimed at an arbitrary user: pick a team, pick a member, send them a notification, watch their bell update instantly. See ",[330,830,110],{"href":831},"\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fnotifications#sending-to-another-user-the-admin-demo"," for the procedure behind it.",[334,834,836],{"id":835},"other-known-limits","Other known limits",[766,838,839,842,848],{},[769,840,841],{},"No tenant impersonation by the platform owner.",[769,843,844,845,847],{},"No IP allowlisting for ",[320,846,322],{}," yet.",[769,849,850],{},"No self-service action to move an existing team onto a different plan — only creation and status changes.",[334,852,854],{"id":853},"reference-how-access-is-actually-checked","Reference: how access is actually checked",[317,856,857,858,860,861,864],{},"Every ",[320,859,369],{}," server procedure requires all three of these — you don't need to manage this yourself, it's just useful when debugging a ",[320,862,863],{},"FORBIDDEN"," error:",[866,867,868,874,877],"ol",{},[769,869,870,871,873],{},"An active (non-revoked) row for the user in the ",[320,872,594],{}," table (from step 3 above).",[769,875,876],{},"MFA enrolled on that row.",[769,878,879],{},"A currently valid elevated session (a code entered in the last 30 minutes).",[317,881,882,883,885,886,704,889,892],{},"Any of the three missing returns the same generic ",[320,884,863],{}," — the response never says which one failed. There are two roles, ",[320,887,888],{},"SUPER_ADMIN",[320,890,891],{},"SUPPORT","; today both pass the same gate with no difference in what they can do.",[894,895,896],"style",{},"html pre.shiki code .sR7ES, html code.shiki .sR7ES{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0;--shiki-sepia:#A6E22E}html pre.shiki code .sLACW, html code.shiki .sLACW{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html pre.shiki code .siCPE, html code.shiki .siCPE{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html pre.shiki code .sFhLe, html code.shiki .sFhLe{--shiki-light:#91B859;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}html pre.shiki code .sYThS, html code.shiki .sYThS{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html pre.shiki code .sMLJd, html code.shiki .sMLJd{--shiki-light:#6182B8;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#66D9EF}html pre.shiki code .ss--_, html code.shiki .ss--_{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .sGXK2, html code.shiki .sGXK2{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .sQeA1, html code.shiki .sQeA1{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}",{"title":377,"searchDepth":422,"depth":422,"links":898},[899,903,904,905,906],{"id":336,"depth":422,"text":337,"children":900},[901,902],{"id":618,"depth":446,"text":344},{"id":624,"depth":446,"text":625},{"id":722,"depth":422,"text":723},{"id":763,"depth":422,"text":764},{"id":835,"depth":422,"text":836},{"id":853,"depth":422,"text":854},"The MFA-gated \u002Fadmin console: platform-owner authority, company and plan management, the job queue, and the audit trail.","md",{},{"title":94,"description":907},"fNK6IglvQDKTH-b_RbAPQhlS5wg73PDfLvuDDZryDuE",1790707485288]