[{"data":1,"prerenderedAt":851},["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-ph-squares-four",{"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-ph-info",{"title":20,"icon":21,"hide":22,"path":23,"stem":24,"children":25,"page":62},"Getting Started","i-ph-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-ph-cube","\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-ph-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-ph-lightning",{"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-ph-fire",{"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-ph-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-ph-stack","\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-ph-star-four","\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-ph-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":846,"extension":847,"meta":848,"navigation":22,"path":95,"seo":849,"stem":96,"__hash__":850},"docs\u002Fdocs\u002F1.saas-starter-kit\u002F2.features\u002F17.platform-admin.md",{"type":314,"value":315,"toc":836},"minimark",[316,333,338,346,361,375,400,410,424,533,544,556,560,563,567,577,632,661,665,674,677,699,703,771,775,789,793,804,819,832],[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,371,374],{},[325,364,365],{},"1. Deploy."," The console needs no secret of its own: it uses the same authenticator as the app, so the two-factor secrets of the app (",[320,368,369],{},"USER_MFA_SESSION_SECRET",", ",[320,372,373],{},"USER_MFA_ENCRYPTION_KEY",") must be set. It fails closed without them, never degrades to an insecure mode:",[376,377,382],"pre",{"className":378,"code":379,"language":380,"meta":381,"style":381},"language-bash shiki shiki-themes material-theme-lighter github-light github-dark monokai","bun run deploy\n","bash","",[320,383,384],{"__ignoreMap":381},[385,386,389,393,397],"span",{"class":387,"line":388},"line",1,[385,390,392],{"class":391},"sR7ES","bun",[385,394,396],{"class":395},"sLACW"," run",[385,398,399],{"class":395}," deploy\n",[317,401,402,405,406,409],{},[325,403,404],{},"2. The future owner signs up like any normal user"," — ",[320,407,408],{},"\u002Fsignup"," or an OAuth provider, choosing their own password. Nobody else ever needs to know this password.",[317,411,412,415,416,419,420,423],{},[325,413,414],{},"3. Promote that account to platform admin."," Run this from ",[320,417,418],{},"apps\u002Ffunctions"," (not the repo root — that's the #1 mistake here), with the stage's real ",[320,421,422],{},"DATABASE_*"," values as environment variables:",[376,425,427],{"className":378,"code":426,"language":380,"meta":381,"style":381},"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,428,429,438,459,474,488,502,516],{"__ignoreMap":381},[385,430,431,435],{"class":387,"line":388},[385,432,434],{"class":433},"sMLJd","cd",[385,436,437],{"class":395}," apps\u002Ffunctions\n",[385,439,441,445,449,453,456],{"class":387,"line":440},2,[385,442,444],{"class":443},"ss--_","$env:DATABASE_HOST",[385,446,448],{"class":447},"sGXK2","=",[385,450,452],{"class":451},"siCPE","\"",[385,454,455],{"class":395},"...",[385,457,458],{"class":451},"\"\n",[385,460,462,465,467,469,472],{"class":387,"line":461},3,[385,463,464],{"class":443},"$env:DATABASE_PORT",[385,466,448],{"class":447},[385,468,452],{"class":451},[385,470,471],{"class":395},"5432",[385,473,458],{"class":451},[385,475,477,480,482,484,486],{"class":387,"line":476},4,[385,478,479],{"class":443},"$env:DATABASE_NAME",[385,481,448],{"class":447},[385,483,452],{"class":451},[385,485,455],{"class":395},[385,487,458],{"class":451},[385,489,491,494,496,498,500],{"class":387,"line":490},5,[385,492,493],{"class":443},"$env:DATABASE_USER",[385,495,448],{"class":447},[385,497,452],{"class":451},[385,499,455],{"class":395},[385,501,458],{"class":451},[385,503,505,508,510,512,514],{"class":387,"line":504},6,[385,506,507],{"class":443},"$env:DATABASE_PASSWORD",[385,509,448],{"class":447},[385,511,452],{"class":451},[385,513,455],{"class":395},[385,515,458],{"class":451},[385,517,519,521,523,526,530],{"class":387,"line":518},7,[385,520,392],{"class":391},[385,522,396],{"class":395},[385,524,525],{"class":395}," src\u002Fdatabase\u002Fgrant-platform-admin.ts",[385,527,529],{"class":528},"sFhLe"," --email=owner@yourdomain.com",[385,531,532],{"class":528}," --role=SUPER_ADMIN\n",[317,534,535,536,539,540,543],{},"This only works if that email already signed up in step 2 — the script errors with ",[320,537,538],{},"no user found"," otherwise. It never touches passwords, only grants a row in the ",[320,541,542],{},"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,545,546,552,553,555],{},[325,547,548,549,551],{},"4. The owner turns on two-factor authentication, then visits ",[320,550,322],{},"."," The owner enables 2FA once, in Account settings > Security (or in the prompt the app shows), by scanning the QR with any authenticator app (Google Authenticator, 1Password, Authy...) and entering the 6-digit code it shows. The same authenticator opens ",[320,554,322],{},": if the last code is older than 30 minutes, a blurred modal asks for a fresh one. There is no second enrollment for the console.",[557,558,344],"h3",{"id":559},"why-theres-no-signup-form",[317,561,562],{},"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.",[557,564,566],{"id":565},"local-development-and-demos-only-seed-both-accounts-at-once","Local development and demos only: seed both accounts at once",[317,568,569,572,573,576],{},[320,570,571],{},"seed\u002Fdemo-accounts.ts"," creates ",[325,574,575],{},"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:",[376,578,580],{"className":378,"code":579,"language":380,"meta":381,"style":381},"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,581,582,601,614,624],{"__ignoreMap":381},[385,583,584,587,589,591,594,596,598],{"class":387,"line":388},[385,585,586],{"class":443},"AUTH_URL",[385,588,448],{"class":447},[385,590,455],{"class":395},[385,592,593],{"class":443}," AUTH_ADMIN_SECRET",[385,595,448],{"class":447},[385,597,455],{"class":395},[385,599,600],{"class":391}," \\\n",[385,602,603,606,608,611],{"class":387,"line":440},[385,604,605],{"class":395},"  bun",[385,607,396],{"class":395},[385,609,610],{"class":395}," src\u002Fdatabase\u002Fseed\u002Fdemo-accounts.ts",[385,612,600],{"class":613},"sQeA1",[385,615,616,619,622],{"class":387,"line":461},[385,617,618],{"class":528},"  --owner-email=owner@yourdomain.com",[385,620,621],{"class":528}," --client-email=client@yourdomain.com",[385,623,600],{"class":613},[385,625,626,629],{"class":387,"line":476},[385,627,628],{"class":528},"  --owner-password=...",[385,630,631],{"class":528}," --client-password=...\n",[317,633,634,635,637,638,641,642,645,646,649,650,637,653,656,657,660],{},"(",[320,636,586],{},"\u002F",[320,639,640],{},"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,643,644],{},"--owner-email"," and ",[320,647,648],{},"--client-email"," are required — there is no default address, the accounts are created on real addresses of yours. ",[320,651,652],{},"--owner-password",[320,654,655],{},"--client-password"," override the throwaway defaults defined in the script's ",[320,658,659],{},"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,662,664],{"id":663},"mfa-after-the-first-time-what-to-expect","MFA after the first time: what to expect",[317,666,667,668,670,671,673],{},"Once 2FA is on, ",[320,669,322],{}," stays open for 30 minutes after each code. After that, visiting any ",[320,672,322],{}," page opens a blurred modal that asks for a fresh 6-digit code from the same authenticator. Nothing to re-scan, no re-enrolling.",[317,675,676],{},"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,678,679,682,683,686,687,690,691,694,695,698],{},[325,680,681],{},"Recovery codes:"," turning on 2FA issues ten single-use recovery codes, shown once. If you lose your authenticator, choose ",[325,684,685],{},"Lost your authenticator?"," at the prompt and enter one in place of the six-digit code. ",[325,688,689],{},"Generate new codes"," in Account settings > Security replaces the set after confirming an authenticator code. ",[325,692,693],{},"Known limit:"," if both are lost, an operator resets the factor with ",[320,696,697],{},"apps\u002Ffunctions\u002Fsrc\u002Fdatabase\u002Freset-mfa.ts --email=\u003Cemail>"," (manual and audited), then you turn 2FA on again from scratch.",[334,700,702],{"id":701},"what-the-console-does","What the console does",[704,705,706,717,733,742,759],"ul",{},[707,708,709,712,713,716],"li",{},[325,710,711],{},"Companies"," (",[320,714,715],{},"\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.",[707,718,719,712,722,725,726,645,729,732],{},[325,720,721],{},"Plans",[320,723,724],{},"\u002Fadmin\u002Fplans",") — create, edit, archive\u002Funarchive plans (archiving is a soft delete: ",[320,727,728],{},"Team.planId",[320,730,731],{},"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.",[707,734,735,712,738,741],{},[325,736,737],{},"Queue",[320,739,740],{},"\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.",[707,743,744,712,747,750,751,754,755,758],{},[325,745,746],{},"Audit log",[320,748,749],{},"\u002Fadmin\u002Faudit-log",") — read-only trail of every platform-admin mutation: ",[320,752,753],{},"actorUserId · action · target · ip · createdAt",". Writing is centralized in one function (",[320,756,757],{},"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.",[707,760,761,712,763,766,767,770],{},[325,762,110],{},[320,764,765],{},"\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,768,110],{"href":769},"\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fnotifications#sending-to-another-user-the-admin-demo"," for the procedure behind it.",[334,772,774],{"id":773},"other-known-limits","Other known limits",[704,776,777,780,786],{},[707,778,779],{},"No tenant impersonation by the platform owner.",[707,781,782,783,785],{},"No IP allowlisting for ",[320,784,322],{}," yet.",[707,787,788],{},"No self-service action to move an existing team onto a different plan — only creation and status changes.",[334,790,792],{"id":791},"reference-how-access-is-actually-checked","Reference: how access is actually checked",[317,794,795,796,799,800,803],{},"Every ",[320,797,798],{},"\u002Fadmin\u002F*"," server procedure requires all of these — you don't need to manage this yourself, it's just useful when debugging a ",[320,801,802],{},"FORBIDDEN"," error:",[805,806,807,813,816],"ol",{},[707,808,809,810,812],{},"An active (non-revoked) row for the user in the ",[320,811,542],{}," table (from step 3 above).",[707,814,815],{},"Two-factor authentication turned on for the account.",[707,817,818],{},"A code from that authenticator entered in the last 30 minutes.",[317,820,821,822,824,825,645,828,831],{},"Any of the three missing returns the same generic ",[320,823,802],{}," — the response never says which one failed. There are two roles, ",[320,826,827],{},"SUPER_ADMIN",[320,829,830],{},"SUPPORT","; today both pass the same gate with no difference in what they can do.",[833,834,835],"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 .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 .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 .sQeA1, html code.shiki .sQeA1{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}",{"title":381,"searchDepth":440,"depth":440,"links":837},[838,842,843,844,845],{"id":336,"depth":440,"text":337,"children":839},[840,841],{"id":559,"depth":461,"text":344},{"id":565,"depth":461,"text":566},{"id":663,"depth":440,"text":664},{"id":701,"depth":440,"text":702},{"id":773,"depth":440,"text":774},{"id":791,"depth":440,"text":792},"The MFA-gated \u002Fadmin console: platform-owner authority, company and plan management, the job queue, and the audit trail.","md",{},{"title":94,"description":846},"nSKj7313elc_jjblJlLL90P4en8wGIfwyKN4AN9bRG8",1792616803887]