[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"docs-navigation":3,"docs-page-tenancy":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":2369,"linksToKit":317},{"path":48,"title":49,"description":50,"navTitle":49,"group":40,"seo":168,"body":170},{"title":169,"description":50},"Multi-tenant Nuxt: organizations, roles and seats",{"type":171,"value":172,"toc":2354},"minimark",[173,177,197,202,262,266,285,464,470,525,552,570,573,604,610,614,628,762,788,791,869,876,880,886,1117,1176,1189,1218,1246,1255,1259,1262,1422,1442,1472,1475,1479,1493,1523,1527,1570,1581,1603,1626,1640,1644,1710,1714,1720,1902,1908,2063,2072,2076,2097,2101,2112,2115,2300,2306,2310,2350],[174,175,49],"h1",{"id":176},"organizations",[178,179,180,181,185,186,196],"p",{},"Data belongs to an organization, and a solo user is an organization of one.\nMembers, roles, seats, invitations and plans all hang off it. You work here\nwhen you add routes or pages that act on an organization's data, change the\nroles, or add a second level such as branches. It is all switched from the\n",[182,183,184],"code",{},"orgs"," block in ",[187,188,193],"a",{"href":189,"rel":190,"title":192},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fstarter.config.ts",[191],"nofollow","Licensees only",[182,194,195],{},"starter.config.ts",".",[198,199,201],"h2",{"id":200},"how-it-works","How it works",[203,204,205,227,238,245,252],"ol",{},[206,207,208,209,211,212,211,215,218,219,222,223,226],"li",{},"Four tables hold it: ",[182,210,176],{},", ",[182,213,214],{},"org_members",[182,216,217],{},"org_invitations"," and\n",[182,220,221],{},"org_audit_log",". They ship on every build. ",[182,224,225],{},"orgs.enabled"," adds or removes\nroutes and pages, never tables.",[206,228,229,230,233,234,237],{},"An organization is addressed by its ",[182,231,232],{},"public_id",", 16 lowercase characters\nsuch as ",[182,235,236],{},"3k9x0v7q2m8d1c4f",". URLs and API routes use it. The integer id stays\non the server.",[206,239,240,241,244],{},"The organization a request acts on comes from the URL, never from the\nsession. ",[182,242,243],{},"requireOrgScope"," resolves it and reads the caller's roles on every\nrequest, so a role change or a removal applies on the next one.",[206,246,247,248,251],{},"Each signup gets an organization of its own with the new account as owner,\nwhile ",[182,249,250],{},"orgs.createAtSignup"," is on.",[206,253,254,255,258,259,261],{},"Plans and seats belong to the root organization. Checkout and the billing\nportal take the organization by public id and need ",[182,256,257],{},"org:billing"," there\n(",[187,260,59],{"href":62},").",[198,263,265],{"id":264},"scope-a-route-to-an-organization","Scope a route to an organization",[178,267,268,269,272,273,280,281,284],{},"Every route that reads or writes an organization's rows starts with\n",[182,270,271],{},"requireOrgScope(event, publicId, permission?)"," from\n",[187,274,277],{"href":275,"rel":276,"title":192},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Ftenancy\u002Fscope.ts",[191],[182,278,279],{},"server\u002Ftenancy\u002Fscope.ts",".\n",[182,282,283],{},"server\u002Fapi\u002Forgs\u002F[org]\u002Fprojects.get.ts",":",[286,287,292],"pre",{"className":288,"code":289,"language":290,"meta":291,"style":291},"language-ts shiki shiki-themes github-light github-light github-dark","import { eq } from 'drizzle-orm'\n\nimport { requireOrgScope } from '#server\u002Ftenancy\u002Fscope'\n\nexport default defineEventHandler(async (event) => {\n  const scope = await requireOrgScope(event, getRouterParam(event, 'org') ?? '')\n\n  return useDrizzle(event)\n    .select()\n    .from(tables.projects)\n    .where(eq(tables.projects.orgId, scope.orgId))\n})\n","ts","",[182,293,294,313,319,331,335,369,409,413,424,435,444,459],{"__ignoreMap":291},[295,296,298,302,306,309],"span",{"class":297,"line":12},"line",[295,299,301],{"class":300},"s8jYJ","import",[295,303,305],{"class":304},"sxrX7"," { eq } ",[295,307,308],{"class":300},"from",[295,310,312],{"class":311},"sIIMD"," 'drizzle-orm'\n",[295,314,315],{"class":297,"line":17},[295,316,318],{"emptyLinePlaceholder":317},true,"\n",[295,320,321,323,326,328],{"class":297,"line":22},[295,322,301],{"class":300},[295,324,325],{"class":304}," { requireOrgScope } ",[295,327,308],{"class":300},[295,329,330],{"class":311}," '#server\u002Ftenancy\u002Fscope'\n",[295,332,333],{"class":297,"line":27},[295,334,318],{"emptyLinePlaceholder":317},[295,336,337,340,343,347,350,353,356,360,363,366],{"class":297,"line":32},[295,338,339],{"class":300},"export",[295,341,342],{"class":300}," default",[295,344,346],{"class":345},"snPdu"," defineEventHandler",[295,348,349],{"class":304},"(",[295,351,352],{"class":300},"async",[295,354,355],{"class":304}," (",[295,357,359],{"class":358},"sP4rz","event",[295,361,362],{"class":304},") ",[295,364,365],{"class":300},"=>",[295,367,368],{"class":304}," {\n",[295,370,371,374,378,381,384,387,390,393,395,398,400,403,406],{"class":297,"line":37},[295,372,373],{"class":300},"  const",[295,375,377],{"class":376},"sBjJW"," scope",[295,379,380],{"class":300}," =",[295,382,383],{"class":300}," await",[295,385,386],{"class":345}," requireOrgScope",[295,388,389],{"class":304},"(event, ",[295,391,392],{"class":345},"getRouterParam",[295,394,389],{"class":304},[295,396,397],{"class":311},"'org'",[295,399,362],{"class":304},[295,401,402],{"class":300},"??",[295,404,405],{"class":311}," ''",[295,407,408],{"class":304},")\n",[295,410,411],{"class":297,"line":46},[295,412,318],{"emptyLinePlaceholder":317},[295,414,415,418,421],{"class":297,"line":51},[295,416,417],{"class":300},"  return",[295,419,420],{"class":345}," useDrizzle",[295,422,423],{"class":304},"(event)\n",[295,425,426,429,432],{"class":297,"line":56},[295,427,428],{"class":304},"    .",[295,430,431],{"class":345},"select",[295,433,434],{"class":304},"()\n",[295,436,437,439,441],{"class":297,"line":64},[295,438,428],{"class":304},[295,440,308],{"class":345},[295,442,443],{"class":304},"(tables.projects)\n",[295,445,446,448,451,453,456],{"class":297,"line":73},[295,447,428],{"class":304},[295,449,450],{"class":345},"where",[295,452,349],{"class":304},[295,454,455],{"class":345},"eq",[295,457,458],{"class":304},"(tables.projects.orgId, scope.orgId))\n",[295,460,461],{"class":297,"line":78},[295,462,463],{"class":304},"})\n",[178,465,466,467,284],{},"It answers 401 without a verified session, and 404 when the organization does\nnot exist, is closed, or the caller is not a member. Pass a permission as the\nthird argument and a member without it gets 403 with\n",[182,468,469],{},"data.reason: 'org-permission-required'",[286,471,473],{"className":288,"code":472,"language":290,"meta":291,"style":291},"const scope = await requireOrgScope(\n  event,\n  getRouterParam(event, 'org') ?? '',\n  'org:manage',\n)\n",[182,474,475,491,496,514,521],{"__ignoreMap":291},[295,476,477,480,482,484,486,488],{"class":297,"line":12},[295,478,479],{"class":300},"const",[295,481,377],{"class":376},[295,483,380],{"class":300},[295,485,383],{"class":300},[295,487,386],{"class":345},[295,489,490],{"class":304},"(\n",[295,492,493],{"class":297,"line":17},[295,494,495],{"class":304},"  event,\n",[295,497,498,501,503,505,507,509,511],{"class":297,"line":22},[295,499,500],{"class":345},"  getRouterParam",[295,502,389],{"class":304},[295,504,397],{"class":311},[295,506,362],{"class":304},[295,508,402],{"class":300},[295,510,405],{"class":311},[295,512,513],{"class":304},",\n",[295,515,516,519],{"class":297,"line":27},[295,517,518],{"class":311},"  'org:manage'",[295,520,513],{"class":304},[295,522,523],{"class":297,"line":32},[295,524,408],{"class":304},[178,526,527,528,531,532,211,535,211,538,211,541,513,544,547,548,551],{},"The returned ",[182,529,530],{},"OrgScope"," carries ",[182,533,534],{},"orgId",[182,536,537],{},"rootOrgId",[182,539,540],{},"publicId",[182,542,543],{},"level",[182,545,546],{},"roles"," (every role the caller holds there) and ",[182,549,550],{},"actorUserId",". Take ids for\nwrites from it, never from the request body.",[178,553,554,555,558,559,562,563,566,567,569],{},"For a public page that shows an organization to visitors without a session,\n",[182,556,557],{},"resolvePublicOrg(event, publicId)"," returns ",[182,560,561],{},"{ orgId, publicId }"," or ",[182,564,565],{},"null",".\nIts type can't be passed where an ",[182,568,530],{}," is expected, so a public read\ncan't reach a write.",[178,571,572],{},"To gate on a plan, pass the scope's root:",[286,574,576],{"className":288,"code":575,"language":290,"meta":291,"style":291},"await requirePlanGate(event, scope)\nawait requirePermission(event, 'insights:read', scope.rootOrgId)\n",[182,577,578,589],{"__ignoreMap":291},[295,579,580,583,586],{"class":297,"line":12},[295,581,582],{"class":300},"await",[295,584,585],{"class":345}," requirePlanGate",[295,587,588],{"class":304},"(event, scope)\n",[295,590,591,593,596,598,601],{"class":297,"line":17},[295,592,582],{"class":300},[295,594,595],{"class":345}," requirePermission",[295,597,389],{"class":304},[295,599,600],{"class":311},"'insights:read'",[295,602,603],{"class":304},", scope.rootOrgId)\n",[178,605,606,607,609],{},"With organizations on, a plan check that names no organization answers 500.\n",[187,608,59],{"href":62}," covers both guards.",[198,611,613],{"id":612},"add-pages-under-an-organization","Add pages under an organization",[178,615,616,617,624,625,284],{},"Pages in ",[187,618,621],{"href":619,"rel":620,"title":192},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Ftree\u002Fmain\u002Fapp\u002Fpages\u002Fo",[191],[182,622,623],{},"app\u002Fpages\u002Fo\u002F[org]\u002F"," read the organization from\n",[182,626,627],{},"route.params.org",[286,629,633],{"className":630,"code":631,"language":632,"meta":291,"style":291},"language-vue shiki shiki-themes github-light github-light github-dark","\u003Cscript setup lang=\"ts\">\ndefinePageMeta({ layout: 'dashboard', middleware: 'auth' })\n\nconst route = useRoute()\nconst org = computed(() => String(route.params.org ?? ''))\nconst { can, member } = useOrgRole(org)\n\u003C\u002Fscript>\n","vue",[182,634,635,659,679,683,697,727,753],{"__ignoreMap":291},[295,636,637,640,644,647,650,653,656],{"class":297,"line":12},[295,638,639],{"class":304},"\u003C",[295,641,643],{"class":642},"sovSZ","script",[295,645,646],{"class":345}," setup",[295,648,649],{"class":345}," lang",[295,651,652],{"class":304},"=",[295,654,655],{"class":311},"\"ts\"",[295,657,658],{"class":304},">\n",[295,660,661,664,667,670,673,676],{"class":297,"line":17},[295,662,663],{"class":345},"definePageMeta",[295,665,666],{"class":304},"({ layout: ",[295,668,669],{"class":311},"'dashboard'",[295,671,672],{"class":304},", middleware: ",[295,674,675],{"class":311},"'auth'",[295,677,678],{"class":304}," })\n",[295,680,681],{"class":297,"line":22},[295,682,318],{"emptyLinePlaceholder":317},[295,684,685,687,690,692,695],{"class":297,"line":27},[295,686,479],{"class":300},[295,688,689],{"class":376}," route",[295,691,380],{"class":300},[295,693,694],{"class":345}," useRoute",[295,696,434],{"class":304},[295,698,699,701,704,706,709,712,714,717,720,722,724],{"class":297,"line":32},[295,700,479],{"class":300},[295,702,703],{"class":376}," org",[295,705,380],{"class":300},[295,707,708],{"class":345}," computed",[295,710,711],{"class":304},"(() ",[295,713,365],{"class":300},[295,715,716],{"class":345}," String",[295,718,719],{"class":304},"(route.params.org ",[295,721,402],{"class":300},[295,723,405],{"class":311},[295,725,726],{"class":304},"))\n",[295,728,729,731,734,737,739,742,745,747,750],{"class":297,"line":37},[295,730,479],{"class":300},[295,732,733],{"class":304}," { ",[295,735,736],{"class":376},"can",[295,738,211],{"class":304},[295,740,741],{"class":376},"member",[295,743,744],{"class":304}," } ",[295,746,652],{"class":300},[295,748,749],{"class":345}," useOrgRole",[295,751,752],{"class":304},"(org)\n",[295,754,755,758,760],{"class":297,"line":46},[295,756,757],{"class":304},"\u003C\u002F",[295,759,643],{"class":642},[295,761,658],{"class":304},[178,763,764,765,768,769,772,773,775,776,783,784,787],{},"A page like this keeps the app's sidebar and the full width, as the overview\nat ",[182,766,767],{},"\u002Fo\u002F\u003Cid>"," does. ",[182,770,771],{},"orgManagement: true"," in ",[182,774,663],{}," makes it one of\nthe organization's settings pages instead, next to General, Members, Billing\nand Audit. That menu is built in\n",[187,777,780],{"href":778,"rel":779,"title":192},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fapp\u002Fcomposables\u002FuseOrgNav.ts",[191],[182,781,782],{},"app\u002Fcomposables\u002FuseOrgNav.ts",", so add your\nentry there. ",[182,785,786],{},"\u002Fo\u002F**"," renders on the client only.",[178,789,790],{},"The client helpers:",[792,793,794,816,836,849],"ul",{},[206,795,796,558,799,801,802,513,805,211,808,811,812,815],{},[182,797,798],{},"useOrgs()",[182,800,184],{}," (every organization the account is in), ",[182,803,804],{},"roots",[182,806,807],{},"pending",[182,809,810],{},"error"," and ",[182,813,814],{},"orgless",", which is true once the list loaded and was\nempty.",[206,817,818,558,821,211,824,811,827,829,830,832,833,835],{},[182,819,820],{},"useOrgRole(publicId)",[182,822,823],{},"role",[182,825,826],{},"can(permission)",[182,828,741],{},", which\nstays ",[182,831,565],{}," until the list loads. Use it to hide controls; the route's\n",[182,834,243],{}," is what enforces.",[206,837,838,558,841,844,845,848],{},[182,839,840],{},"useActiveOrg()",[182,842,843],{},"active",": the organization in the URL, or else the\none the account last switched to. ",[182,846,847],{},"\u002Fdashboard"," redirects there.",[206,850,851,854,855,211,858,211,861,513,863,211,866,196],{},[182,852,853],{},"useStarterOrgs()"," exposes the config: ",[182,856,857],{},"enabled",[182,859,860],{},"levels",[182,862,546],{},[182,864,865],{},"ownerRole",[182,867,868],{},"levelFor(isChild)",[178,870,871,872,875],{},"After a mutation of your own, invalidate the ",[182,873,874],{},"['orgs']"," query key prefix. The\nlist, the landing organization and the member counts all live under it.",[198,877,879],{"id":878},"roles-and-org-permissions","Roles and org permissions",[178,881,882,883,885],{},"Org roles are separate from app roles (",[187,884,54],{"href":53},").\nThe shipped set:",[286,887,889],{"className":288,"code":888,"language":290,"meta":291,"style":291},"orgs: {\n  permissions: [\n    'org:manage',\n    'org:members:manage',\n    'org:billing',\n    'org:scopes:manage',\n  ],\n\n  roles: {\n    owner: {\n      permissions: [\n    'org:manage',\n    'org:members:manage',\n    'org:billing',\n    'org:scopes:manage',\n  ],\n      at: ['organization'],\n    },\n    admin: {\n      permissions: ['org:manage', 'org:members:manage', 'org:scopes:manage'],\n      at: ['organization'],\n    },\n    member: { permissions: [], at: ['organization'] },\n  },\n\n  defaultRole: 'member',\n  ownerRole: 'owner',\n},\n",[182,890,891,898,906,913,920,927,934,939,943,950,957,964,970,976,982,988,992,1006,1011,1018,1039,1049,1053,1077,1082,1086,1099,1111],{"__ignoreMap":291},[295,892,893,895],{"class":297,"line":12},[295,894,184],{"class":345},[295,896,897],{"class":304},": {\n",[295,899,900,903],{"class":297,"line":17},[295,901,902],{"class":345},"  permissions",[295,904,905],{"class":304},": [\n",[295,907,908,911],{"class":297,"line":22},[295,909,910],{"class":311},"    'org:manage'",[295,912,513],{"class":304},[295,914,915,918],{"class":297,"line":27},[295,916,917],{"class":311},"    'org:members:manage'",[295,919,513],{"class":304},[295,921,922,925],{"class":297,"line":32},[295,923,924],{"class":311},"    'org:billing'",[295,926,513],{"class":304},[295,928,929,932],{"class":297,"line":37},[295,930,931],{"class":311},"    'org:scopes:manage'",[295,933,513],{"class":304},[295,935,936],{"class":297,"line":46},[295,937,938],{"class":304},"  ],\n",[295,940,941],{"class":297,"line":51},[295,942,318],{"emptyLinePlaceholder":317},[295,944,945,948],{"class":297,"line":56},[295,946,947],{"class":345},"  roles",[295,949,897],{"class":304},[295,951,952,955],{"class":297,"line":64},[295,953,954],{"class":345},"    owner",[295,956,897],{"class":304},[295,958,959,962],{"class":297,"line":73},[295,960,961],{"class":345},"      permissions",[295,963,905],{"class":304},[295,965,966,968],{"class":297,"line":78},[295,967,910],{"class":311},[295,969,513],{"class":304},[295,971,972,974],{"class":297,"line":83},[295,973,917],{"class":311},[295,975,513],{"class":304},[295,977,978,980],{"class":297,"line":88},[295,979,924],{"class":311},[295,981,513],{"class":304},[295,983,984,986],{"class":297,"line":93},[295,985,931],{"class":311},[295,987,513],{"class":304},[295,989,990],{"class":297,"line":98},[295,991,938],{"class":304},[295,993,994,997,1000,1003],{"class":297,"line":103},[295,995,996],{"class":345},"      at",[295,998,999],{"class":304},": [",[295,1001,1002],{"class":311},"'organization'",[295,1004,1005],{"class":304},"],\n",[295,1007,1008],{"class":297,"line":108},[295,1009,1010],{"class":304},"    },\n",[295,1012,1013,1016],{"class":297,"line":113},[295,1014,1015],{"class":345},"    admin",[295,1017,897],{"class":304},[295,1019,1020,1022,1024,1027,1029,1032,1034,1037],{"class":297,"line":122},[295,1021,961],{"class":345},[295,1023,999],{"class":304},[295,1025,1026],{"class":311},"'org:manage'",[295,1028,211],{"class":304},[295,1030,1031],{"class":311},"'org:members:manage'",[295,1033,211],{"class":304},[295,1035,1036],{"class":311},"'org:scopes:manage'",[295,1038,1005],{"class":304},[295,1040,1041,1043,1045,1047],{"class":297,"line":127},[295,1042,996],{"class":345},[295,1044,999],{"class":304},[295,1046,1002],{"class":311},[295,1048,1005],{"class":304},[295,1050,1051],{"class":297,"line":132},[295,1052,1010],{"class":304},[295,1054,1055,1058,1061,1064,1067,1070,1072,1074],{"class":297,"line":137},[295,1056,1057],{"class":345},"    member",[295,1059,1060],{"class":304},": { ",[295,1062,1063],{"class":345},"permissions",[295,1065,1066],{"class":304},": [], ",[295,1068,1069],{"class":345},"at",[295,1071,999],{"class":304},[295,1073,1002],{"class":311},[295,1075,1076],{"class":304},"] },\n",[295,1078,1079],{"class":297,"line":146},[295,1080,1081],{"class":304},"  },\n",[295,1083,1084],{"class":297,"line":151},[295,1085,318],{"emptyLinePlaceholder":317},[295,1087,1088,1091,1094,1097],{"class":297,"line":156},[295,1089,1090],{"class":345},"  defaultRole",[295,1092,1093],{"class":304},": ",[295,1095,1096],{"class":311},"'member'",[295,1098,513],{"class":304},[295,1100,1101,1104,1106,1109],{"class":297,"line":165},[295,1102,1103],{"class":345},"  ownerRole",[295,1105,1093],{"class":304},[295,1107,1108],{"class":311},"'owner'",[295,1110,513],{"class":304},[295,1112,1114],{"class":297,"line":1113},28,[295,1115,1116],{"class":304},"},\n",[1118,1119,1120,1133],"table",{},[1121,1122,1123],"thead",{},[1124,1125,1126,1130],"tr",{},[1127,1128,1129],"th",{},"Permission",[1127,1131,1132],{},"Opens",[1134,1135,1136,1147,1157,1166],"tbody",{},[1124,1137,1138,1144],{},[1139,1140,1141],"td",{},[182,1142,1143],{},"org:manage",[1139,1145,1146],{},"Rename, photo, the audit tab and its chart, closing",[1124,1148,1149,1154],{},[1139,1150,1151],{},[182,1152,1153],{},"org:members:manage",[1139,1155,1156],{},"Inviting, revoking invitations, changing roles, removing",[1124,1158,1159,1163],{},[1139,1160,1161],{},[182,1162,257],{},[1139,1164,1165],{},"The billing tab, checkout for the organization, the portal",[1124,1167,1168,1173],{},[1139,1169,1170],{},[182,1171,1172],{},"org:scopes:manage",[1139,1174,1175],{},"Creating branches, when a second level is declared",[178,1177,1178,1179,1181,1182,1185,1186,1188],{},"Any member can read the roster. ",[182,1180,865],{}," is the role an organization's\ncreator gets and the one the last-owner guard counts. ",[182,1183,1184],{},"defaultRole"," is what the\ninvitation form preselects. ",[182,1187,1069],{}," lists the levels a role may be held at.",[178,1190,1191,1192,1194,1195,1198,1199,562,1202,1205,1206,1209,1210,1217],{},"A member can only grant, change or remove a role whose permissions their own\nroles all hold. The shipped admin lacks ",[182,1193,257],{},", so it can't make anyone\nan owner, itself included, or demote one. The refusal is 403 with\n",[182,1196,1197],{},"data.reason"," ",[182,1200,1201],{},"role-beyond-yours",[182,1203,1204],{},"member-outranks-you",". The check is\n",[182,1207,1208],{},"roleCoveredBy"," in\n",[187,1211,1214],{"href":1212,"rel":1213,"title":192},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fshared\u002Futils\u002Forg-permissions.ts",[191],[182,1215,1216],{},"shared\u002Futils\u002Forg-permissions.ts",", so a\nrole you add follows the same rule.",[178,1219,1220,1221,1224,1225,1228,1229,280,1232,1239,1240,1243,1244,196],{},"To add a role, add its key to ",[182,1222,1223],{},"orgs.roles"," and its label as\n",[182,1226,1227],{},"dashboard.orgs.roles.\u003Cid>"," in every file in ",[182,1230,1231],{},"i18n\u002Flocales\u002F",[187,1233,1236],{"href":1234,"rel":1235,"title":192},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Ftest\u002Funit\u002Forg-copy.test.ts",[191],[182,1237,1238],{},"test\u002Funit\u002Forg-copy.test.ts"," fails on a\nmissing label. To add a permission, list it in ",[182,1241,1242],{},"orgs.permissions",", grant it to\nroles, and pass it to ",[182,1245,243],{},[178,1247,1248,1249,1251,1252,196],{},"Org permissions are never sold; a plan only sets how many people an\norganization holds. The build fails if a string in ",[182,1250,1242],{}," also\nappears in ",[182,1253,1254],{},"access.planPermissions",[198,1256,1258],{"id":1257},"add-a-second-level","Add a second level",[178,1260,1261],{},"Two levels give each organization child scopes that own rows: branches, sites\nor projects. Declare the level and let the owner role be held there:",[286,1263,1265],{"className":288,"code":1264,"language":290,"meta":291,"style":291},"orgs: {\n  levels: ['organization', 'branch'],\n\n  roles: {\n    owner: {\n      permissions: [\n    'org:manage',\n    'org:members:manage',\n    'org:billing',\n    'org:scopes:manage',\n  ],\n      at: ['organization', 'branch'],\n    },\n    manager: {\n      permissions: ['org:manage', 'org:members:manage'],\n      at: ['branch'],\n    },\n    member: { permissions: [], at: ['organization', 'branch'] },\n  },\n},\n",[182,1266,1267,1273,1289,1293,1299,1305,1311,1317,1323,1329,1335,1339,1353,1357,1364,1378,1388,1392,1414,1418],{"__ignoreMap":291},[295,1268,1269,1271],{"class":297,"line":12},[295,1270,184],{"class":345},[295,1272,897],{"class":304},[295,1274,1275,1278,1280,1282,1284,1287],{"class":297,"line":17},[295,1276,1277],{"class":345},"  levels",[295,1279,999],{"class":304},[295,1281,1002],{"class":311},[295,1283,211],{"class":304},[295,1285,1286],{"class":311},"'branch'",[295,1288,1005],{"class":304},[295,1290,1291],{"class":297,"line":22},[295,1292,318],{"emptyLinePlaceholder":317},[295,1294,1295,1297],{"class":297,"line":27},[295,1296,947],{"class":345},[295,1298,897],{"class":304},[295,1300,1301,1303],{"class":297,"line":32},[295,1302,954],{"class":345},[295,1304,897],{"class":304},[295,1306,1307,1309],{"class":297,"line":37},[295,1308,961],{"class":345},[295,1310,905],{"class":304},[295,1312,1313,1315],{"class":297,"line":46},[295,1314,910],{"class":311},[295,1316,513],{"class":304},[295,1318,1319,1321],{"class":297,"line":51},[295,1320,917],{"class":311},[295,1322,513],{"class":304},[295,1324,1325,1327],{"class":297,"line":56},[295,1326,924],{"class":311},[295,1328,513],{"class":304},[295,1330,1331,1333],{"class":297,"line":64},[295,1332,931],{"class":311},[295,1334,513],{"class":304},[295,1336,1337],{"class":297,"line":73},[295,1338,938],{"class":304},[295,1340,1341,1343,1345,1347,1349,1351],{"class":297,"line":78},[295,1342,996],{"class":345},[295,1344,999],{"class":304},[295,1346,1002],{"class":311},[295,1348,211],{"class":304},[295,1350,1286],{"class":311},[295,1352,1005],{"class":304},[295,1354,1355],{"class":297,"line":83},[295,1356,1010],{"class":304},[295,1358,1359,1362],{"class":297,"line":88},[295,1360,1361],{"class":345},"    manager",[295,1363,897],{"class":304},[295,1365,1366,1368,1370,1372,1374,1376],{"class":297,"line":93},[295,1367,961],{"class":345},[295,1369,999],{"class":304},[295,1371,1026],{"class":311},[295,1373,211],{"class":304},[295,1375,1031],{"class":311},[295,1377,1005],{"class":304},[295,1379,1380,1382,1384,1386],{"class":297,"line":98},[295,1381,996],{"class":345},[295,1383,999],{"class":304},[295,1385,1286],{"class":311},[295,1387,1005],{"class":304},[295,1389,1390],{"class":297,"line":103},[295,1391,1010],{"class":304},[295,1393,1394,1396,1398,1400,1402,1404,1406,1408,1410,1412],{"class":297,"line":108},[295,1395,1057],{"class":345},[295,1397,1060],{"class":304},[295,1399,1063],{"class":345},[295,1401,1066],{"class":304},[295,1403,1069],{"class":345},[295,1405,999],{"class":304},[295,1407,1002],{"class":311},[295,1409,211],{"class":304},[295,1411,1286],{"class":311},[295,1413,1076],{"class":304},[295,1415,1416],{"class":297,"line":113},[295,1417,1081],{"class":304},[295,1419,1420],{"class":297,"line":122},[295,1421,1116],{"class":304},[178,1423,1424,1425,513,1428,211,1431,811,1434,1437,1438,1441],{},"Then add the level's copy to every locale: ",[182,1426,1427],{},"dashboard.orgs.levels.branch",[182,1429,1430],{},"levelsPlural.branch",[182,1432,1433],{},"switcher.manage.branch",[182,1435,1436],{},"scopes.action.branch","\nunder ",[182,1439,1440],{},"dashboard.orgs",". General gains a branches card on each root.",[792,1443,1444,1457,1460,1463],{},[206,1445,1446,1449,1450,1453,1454,1456],{},[182,1447,1448],{},"POST \u002Fapi\u002Forgs"," with ",[182,1451,1452],{},"{ name, parent }"," creates a branch. It needs\n",[182,1455,1172],{}," at the parent, and the parent must be a root (422\notherwise). Its creator becomes the branch's owner.",[206,1458,1459],{},"A scope at a branch unions the caller's roles at the branch and at its root,\nso the company's owners and admins run every branch without joining it.",[206,1461,1462],{},"A branch uses its root's plan and seats. If each unit should pay on its own,\nmake them roots.",[206,1464,1465,1466,1468,1469,196],{},"A membership at a level its role's ",[182,1467,1069],{}," doesn't list answers 422\n",[182,1470,1471],{},"role-not-allowed-here",[178,1473,1474],{},"The build refuses a third level, and a second level the owner role can't be\nheld at.",[198,1476,1478],{"id":1477},"seats","Seats",[178,1480,1481,1484,1485,1488,1489,1492],{},[182,1482,1483],{},"orgs.seatLimits"," sets how many people each plan admits, with ",[182,1486,1487],{},"false"," for no\nlimit and ",[182,1490,1491],{},"free"," for an organization with no grant. When a root holds several\nlive grants, the highest limit wins. A seat is a person anywhere in the root\nand its branches, so an owner who also belongs to three branches takes one.",[178,1494,1495,1496,1209,1499,1506,1507,1510,1511,1514,1515,1518,1519,1522],{},"Every membership is created through ",[182,1497,1498],{},"admitMember",[187,1500,1503],{"href":1501,"rel":1502,"title":192},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Ftenancy\u002Fseats.ts",[191],[182,1504,1505],{},"server\u002Ftenancy\u002Fseats.ts",", which counts and\ninserts inside one serializable transaction. A full organization answers 402\nwith ",[182,1508,1509],{},"data.reason: 'seats-full'",". ",[182,1512,1513],{},"GET \u002Fapi\u002Forgs\u002F:org\u002Fmembers"," returns the\nroster with ",[182,1516,1517],{},"seats: { used, invited, limit }",", where ",[182,1520,1521],{},"limit: null"," means no\nlimit.",[198,1524,1526],{"id":1525},"invitations","Invitations",[203,1528,1529,1547,1564],{},[206,1530,1531,1532,1534,1535,1538,1539,1542,1543,1546],{},"A member with ",[182,1533,1153],{}," posts ",[182,1536,1537],{},"{ email, role }"," to\n",[182,1540,1541],{},"POST \u002Fapi\u002Forgs\u002F:org\u002Finvitations",". The row stores a hash of the token, and\nthe plaintext travels only on the ",[182,1544,1545],{},"org:invitation:created"," event.",[206,1548,1549,1556,1557,1560,1561,196],{},[187,1550,1553],{"href":1551,"rel":1552,"title":192},"https:\u002F\u002Fgithub.com\u002Fnuxter-app\u002Fkit\u002Fblob\u002Fmain\u002Fserver\u002Flisteners\u002F00-org-invitation-mail.ts",[191],[182,1554,1555],{},"server\u002Flisteners\u002F00-org-invitation-mail.ts","\nenqueues an ",[182,1558,1559],{},"org-invitation-email"," job, or sends inline without a queue.\nThe mail links to ",[182,1562,1563],{},"\u002Finvite?token=…",[206,1565,1566,1567,196],{},"The invitee signs in or registers, keeping the link, and the page posts to\n",[182,1568,1569],{},"POST \u002Fapi\u002Forgs\u002Finvitations\u002Faccept",[178,1571,1572,1573,1576,1577,1580],{},"A pending invitation takes no seat, but issuing one is refused with 402 once\nmembers plus open invitations fill the plan. Acceptance checks the seat\nagain. Accepting from an account whose address differs from the invited one\nanswers 409 ",[182,1574,1575],{},"invitation-other-address",", naming both addresses, until the\nrequest sends ",[182,1578,1579],{},"acceptAsCurrentUser: true",". Acceptance also refuses when the\nperson who sent the invitation has since lost the right to send it.",[178,1582,1583,1584,1587,1588,1591,1592,1595,1596,1598,1599,1602],{},"An account whose address is not proven yet\n(",[187,1585,44],{"href":1586},"\u002Fdocs\u002Fauth#verify-email-addresses",") gets 403\n",[182,1589,1590],{},"data.reason: 'address-unverified'"," when it issues an invitation, changes a\nmember's role (",[182,1593,1594],{},"PATCH \u002Fapi\u002Forgs\u002F:org\u002Fmembers\u002F:userId",") or accepts an\ninvitation sent to another address. Accepting one sent to its own address,\nrevoking, removing members and creating an organization stay open. A route of\nyours scoped with ",[182,1597,243],{}," passes ",[182,1600,1601],{},"{ provenAddress: true }"," as the\nfourth argument to refuse the same way.",[178,1604,1605,1606,1609,1610,1613,1614,1617,1618,1621,1622,1625],{},"Limits on issuing: ",[182,1607,1608],{},"maxPending"," live invitations per organization (429), and\nthe rate limits ",[182,1611,1612],{},"org-invite"," (10 an hour per address, a minute apart) and\n",[182,1615,1616],{},"org-invite-sender"," (50 an hour per inviter). Revoking is\n",[182,1619,1620],{},"DELETE \u002Fapi\u002Forgs\u002F:org\u002Finvitations\u002F:id",". The nightly ",[182,1623,1624],{},"invitations:prune"," task\ndeletes invitations 30 days after they expire, whatever became of them.",[178,1627,1628,1629,1632,1633,1636,1637,196],{},"The invite route answers 500 when the deploy has no site URL to build the link\nfrom (",[182,1630,1631],{},"NUXT_SITE_URL","). Set ",[182,1634,1635],{},"orgs.invitations: false"," to remove the four\ninvitation routes and add members your own way. With invitations on, the build\nrequires ",[182,1638,1639],{},"email.enabled",[198,1641,1643],{"id":1642},"leave-remove-and-close","Leave, remove and close",[792,1645,1646,1656,1666,1685,1703],{},[206,1647,1648,1649,1652,1653,1655],{},"Leaving and removing share ",[182,1650,1651],{},"DELETE \u002Fapi\u002Forgs\u002F:org\u002Fmembers\u002F:userId",". Your own\nid leaves; anyone else's needs ",[182,1654,1153],{},". Removing someone from a\nroot also removes them from its branches.",[206,1657,1658,1659,1661,1662,1665],{},"The last holder of ",[182,1660,865],{}," at a root can't leave or be demoted: 409 with\n",[182,1663,1664],{},"data.reason: 'last-owner'",". Make someone else an owner first.",[206,1667,1668,1671,1672,1674,1675,1677,1678,1681,1682,1684],{},[182,1669,1670],{},"DELETE \u002Fapi\u002Forgs\u002F:org"," closes an organization. It needs ",[182,1673,1143],{},", and\non a root the caller's roles must also cover ",[182,1676,865],{},". The close is soft:\nit sets ",[182,1679,1680],{},"deleted_at"," on the organization and its branches, and every read\nskips them. There is no reopen route; clear ",[182,1683,1680],{}," by hand.",[206,1686,1687,1688,1691,1692,1695,1696,1699,1700,196],{},"A close answers 409 ",[182,1689,1690],{},"live-subscription"," while the provider can still bill a\nsubscription, so cancel first. An active one-time purchase answers 409\n",[182,1693,1694],{},"lifetime-purchase"," with the ",[182,1697,1698],{},"plans"," it would forfeit, until the body sends\n",[182,1701,1702],{},"{ \"forfeitPurchases\": true }",[206,1704,1705,1706,1709],{},"Deleting an account answers 409 ",[182,1707,1708],{},"sole-owner",", listing the organizations,\nwhile it is the only owner of a root that has other people in it.",[198,1711,1713],{"id":1712},"react-to-organization-events","React to organization events",[178,1715,1716,1717,1719],{},"Tenancy fires eight events on the bus (",[187,1718,81],{"href":80},"), each after its\ntransaction commits:",[1118,1721,1722,1732],{},[1121,1723,1724],{},[1124,1725,1726,1729],{},[1127,1727,1728],{},"Event",[1127,1730,1731],{},"Payload",[1134,1733,1734,1756,1771,1794,1821,1843,1868,1887],{},[1124,1735,1736,1741],{},[1139,1737,1738],{},[182,1739,1740],{},"org:created",[1139,1742,1743,211,1745,211,1747,211,1750,211,1753],{},[182,1744,534],{},[182,1746,540],{},[182,1748,1749],{},"name",[182,1751,1752],{},"parentId",[182,1754,1755],{},"createdByUserId",[1124,1757,1758,1763],{},[1139,1759,1760],{},[182,1761,1762],{},"org:deleted",[1139,1764,1765,211,1767,211,1769],{},[182,1766,534],{},[182,1768,537],{},[182,1770,550],{},[1124,1772,1773,1778],{},[1139,1774,1775],{},[182,1776,1777],{},"org:member:added",[1139,1779,1780,211,1782,211,1784,211,1787,211,1789,211,1792],{},[182,1781,534],{},[182,1783,537],{},[182,1785,1786],{},"userId",[182,1788,823],{},[182,1790,1791],{},"source",[182,1793,550],{},[1124,1795,1796,1801],{},[1139,1797,1798],{},[182,1799,1800],{},"org:member:removed",[1139,1802,1803,211,1805,211,1807,211,1809,355,1812,562,1815,1818,1819],{},[182,1804,534],{},[182,1806,537],{},[182,1808,1786],{},[182,1810,1811],{},"reason",[182,1813,1814],{},"removed",[182,1816,1817],{},"left","), ",[182,1820,550],{},[1124,1822,1823,1828],{},[1139,1824,1825],{},[182,1826,1827],{},"org:member:roleChanged",[1139,1829,1830,211,1832,211,1834,211,1836,211,1838,211,1841],{},[182,1831,534],{},[182,1833,537],{},[182,1835,1786],{},[182,1837,308],{},[182,1839,1840],{},"to",[182,1842,550],{},[1124,1844,1845,1849],{},[1139,1846,1847],{},[182,1848,1545],{},[1139,1850,1851,211,1853,211,1855,211,1858,211,1861,211,1863,211,1865],{},[182,1852,534],{},[182,1854,537],{},[182,1856,1857],{},"invitationId",[182,1859,1860],{},"email",[182,1862,823],{},[182,1864,550],{},[182,1866,1867],{},"token",[1124,1869,1870,1875],{},[1139,1871,1872],{},[182,1873,1874],{},"org:invitation:accepted",[1139,1876,1877,211,1879,211,1881,211,1883,211,1885],{},[182,1878,534],{},[182,1880,537],{},[182,1882,1857],{},[182,1884,1860],{},[182,1886,1786],{},[1124,1888,1889,1894],{},[1139,1890,1891],{},[182,1892,1893],{},"org:invitation:revoked",[1139,1895,1896,211,1898,211,1900],{},[182,1897,534],{},[182,1899,1857],{},[182,1901,550],{},[178,1903,1904,1905,284],{},"The invitation mail is the only listener the kit ships; notifying people about\nthe rest is up to you. ",[182,1906,1907],{},"server\u002Flisteners\u002Funassign-removed-member.ts",[286,1909,1911],{"className":288,"code":1910,"language":290,"meta":291,"style":291},"import { and, eq } from 'drizzle-orm'\n\nimport { defineListener } from '#server\u002Fevents\u002Ftypes'\n\nexport default defineListener({\n  event: 'org:member:removed',\n\n  async handle({ orgId, userId }, { event }) {\n    await useDrizzle(event)\n      .delete(tables.projectAssignees)\n      .where(\n        and(\n          eq(tables.projectAssignees.orgId, orgId),\n          eq(tables.projectAssignees.userId, userId),\n        ),\n      )\n  },\n})\n",[182,1912,1913,1924,1928,1940,1944,1956,1966,1970,1995,2004,2015,2023,2030,2038,2045,2050,2055,2059],{"__ignoreMap":291},[295,1914,1915,1917,1920,1922],{"class":297,"line":12},[295,1916,301],{"class":300},[295,1918,1919],{"class":304}," { and, eq } ",[295,1921,308],{"class":300},[295,1923,312],{"class":311},[295,1925,1926],{"class":297,"line":17},[295,1927,318],{"emptyLinePlaceholder":317},[295,1929,1930,1932,1935,1937],{"class":297,"line":22},[295,1931,301],{"class":300},[295,1933,1934],{"class":304}," { defineListener } ",[295,1936,308],{"class":300},[295,1938,1939],{"class":311}," '#server\u002Fevents\u002Ftypes'\n",[295,1941,1942],{"class":297,"line":27},[295,1943,318],{"emptyLinePlaceholder":317},[295,1945,1946,1948,1950,1953],{"class":297,"line":32},[295,1947,339],{"class":300},[295,1949,342],{"class":300},[295,1951,1952],{"class":345}," defineListener",[295,1954,1955],{"class":304},"({\n",[295,1957,1958,1961,1964],{"class":297,"line":37},[295,1959,1960],{"class":304},"  event: ",[295,1962,1963],{"class":311},"'org:member:removed'",[295,1965,513],{"class":304},[295,1967,1968],{"class":297,"line":46},[295,1969,318],{"emptyLinePlaceholder":317},[295,1971,1972,1975,1978,1981,1983,1985,1987,1990,1992],{"class":297,"line":51},[295,1973,1974],{"class":300},"  async",[295,1976,1977],{"class":345}," handle",[295,1979,1980],{"class":304},"({ ",[295,1982,534],{"class":358},[295,1984,211],{"class":304},[295,1986,1786],{"class":358},[295,1988,1989],{"class":304}," }, { ",[295,1991,359],{"class":358},[295,1993,1994],{"class":304}," }) {\n",[295,1996,1997,2000,2002],{"class":297,"line":56},[295,1998,1999],{"class":300},"    await",[295,2001,420],{"class":345},[295,2003,423],{"class":304},[295,2005,2006,2009,2012],{"class":297,"line":64},[295,2007,2008],{"class":304},"      .",[295,2010,2011],{"class":345},"delete",[295,2013,2014],{"class":304},"(tables.projectAssignees)\n",[295,2016,2017,2019,2021],{"class":297,"line":73},[295,2018,2008],{"class":304},[295,2020,450],{"class":345},[295,2022,490],{"class":304},[295,2024,2025,2028],{"class":297,"line":78},[295,2026,2027],{"class":345},"        and",[295,2029,490],{"class":304},[295,2031,2032,2035],{"class":297,"line":83},[295,2033,2034],{"class":345},"          eq",[295,2036,2037],{"class":304},"(tables.projectAssignees.orgId, orgId),\n",[295,2039,2040,2042],{"class":297,"line":88},[295,2041,2034],{"class":345},[295,2043,2044],{"class":304},"(tables.projectAssignees.userId, userId),\n",[295,2046,2047],{"class":297,"line":93},[295,2048,2049],{"class":304},"        ),\n",[295,2051,2052],{"class":297,"line":98},[295,2053,2054],{"class":304},"      )\n",[295,2056,2057],{"class":297,"line":103},[295,2058,1081],{"class":304},[295,2060,2061],{"class":297,"line":108},[295,2062,463],{"class":304},[178,2064,2065,2066,2068,2069,196],{},"Closing an organization because its last member erased their account fires no\n",[182,2067,1762],{},". That path fires only ",[182,2070,2071],{},"account:purged",[198,2073,2075],{"id":2074},"staff-directory","Staff directory",[178,2077,2078,2081,2082,2085,2086,2089,2090,2093,2094,2096],{},[182,2079,2080],{},"\u002Fdashboard\u002Forganizations"," lets app staff list and search every organization,\nincluding closed ones on request, and open one to see its members, branches,\ngrants and seats. Viewing needs ",[182,2083,2084],{},"users:read","; renaming from there needs\n",[182,2087,2088],{},"users:manage",", and the organization's audit trail shows it as done by an\nadministrator. Staff aren't members, so these routes check ",[182,2091,2092],{},"requirePermission"," instead of\n",[182,2095,243],{},". They exist only while organizations are on.",[198,2098,2100],{"id":2099},"turn-organizations-off","Turn organizations off",[178,2102,2103,2104,2107,2108,2111],{},"Set ",[182,2105,2106],{},"orgs.enabled: false",". The organization routes and pages are not\nregistered, and the account menu drops its organization entries. The tables\nstay. Payments keep working: a purchase lands on the account's own\norganization, created at the first purchase, and plan checks read that\norganization without being told which. Organizations need ",[182,2109,2110],{},"auth.enabled",",\nand the build fails otherwise.",[198,2113,20],{"id":2114},"configuration",[1118,2116,2117,2130],{},[1121,2118,2119],{},[1124,2120,2121,2124,2127],{},[1127,2122,2123],{},"Key",[1127,2125,2126],{},"Default",[1127,2128,2129],{},"What it does",[1134,2131,2132,2146,2161,2174,2190,2212,2226,2240,2255,2270,2285],{},[1124,2133,2134,2138,2143],{},[1139,2135,2136],{},[182,2137,225],{},[1139,2139,2140],{},[182,2141,2142],{},"true",[1139,2144,2145],{},"Registers the organization routes and pages.",[1124,2147,2148,2153,2158],{},[1139,2149,2150],{},[182,2151,2152],{},"orgs.levels",[1139,2154,2155],{},[182,2156,2157],{},"['organization']",[1139,2159,2160],{},"One or two level ids, root first. Each keys its labels.",[1124,2162,2163,2167,2171],{},[1139,2164,2165],{},[182,2166,250],{},[1139,2168,2169],{},[182,2170,2142],{},[1139,2172,2173],{},"Gives every new account an organization it owns.",[1124,2175,2176,2180,2187],{},[1139,2177,2178],{},[182,2179,1242],{},[1139,2181,2182,2183,2186],{},"the four ",[182,2184,2185],{},"org:*"," permissions",[1139,2188,2189],{},"Every capability an org role may hold. Never sold.",[1124,2191,2192,2196,2206],{},[1139,2193,2194],{},[182,2195,1223],{},[1139,2197,2198,211,2201,211,2204],{},[182,2199,2200],{},"owner",[182,2202,2203],{},"admin",[182,2205,741],{},[1139,2207,2208,2209,2211],{},"Each role's permissions and the levels (",[182,2210,1069],{},") it may be held at.",[1124,2213,2214,2219,2223],{},[1139,2215,2216],{},[182,2217,2218],{},"orgs.defaultRole",[1139,2220,2221],{},[182,2222,1096],{},[1139,2224,2225],{},"The role the invitation form preselects.",[1124,2227,2228,2233,2237],{},[1139,2229,2230],{},[182,2231,2232],{},"orgs.ownerRole",[1139,2234,2235],{},[182,2236,1108],{},[1139,2238,2239],{},"The creator's role; the last-owner guard counts it. Must be holdable at every level.",[1124,2241,2242,2247,2250],{},[1139,2243,2244],{},[182,2245,2246],{},"orgs.invitations",[1139,2248,2249],{},"an object",[1139,2251,2252,2254],{},[182,2253,1487],{}," removes invitations.",[1124,2256,2257,2262,2267],{},[1139,2258,2259],{},[182,2260,2261],{},"orgs.invitations.expiryHours",[1139,2263,2264],{},[182,2265,2266],{},"336",[1139,2268,2269],{},"Hours an invitation can be accepted.",[1124,2271,2272,2277,2282],{},[1139,2273,2274],{},[182,2275,2276],{},"orgs.invitations.maxPending",[1139,2278,2279],{},[182,2280,2281],{},"50",[1139,2283,2284],{},"Live invitations per organization.",[1124,2286,2287,2291,2294],{},[1139,2288,2289],{},[182,2290,1483],{},[1139,2292,2293],{},"free 3, pro 10, promax unlimited, lifetime 10",[1139,2295,2296,2297,2299],{},"People each plan admits. ",[182,2298,1487],{}," is no limit.",[178,2301,2302,2303,2305],{},"The only environment variable involved is ",[182,2304,1631],{},", which invitation\nlinks are built from.",[198,2307,2309],{"id":2308},"good-to-know","Good to know",[792,2311,2312,2317,2328,2331,2340],{},[206,2313,2314,2316],{},[182,2315,243],{}," answers 404 to a non-member on purpose, so a public id\ncan't be probed for existence. Keep that in your own routes.",[206,2318,2319,2320,2323,2324,2327],{},"The client knows one role per organization, from ",[182,2321,2322],{},"GET \u002Fapi\u002Forgs",", while the\nserver unions root and branch roles. At a branch ",[182,2325,2326],{},"useOrgRole"," can hide a\ncontrol the server would allow.",[206,2329,2330],{},"A closed organization grants nothing, but its members, invitations, audit\ntrail and grants stay in the database.",[206,2332,2333,2335,2336,2339],{},[182,2334,221],{}," has no foreign key to ",[182,2337,2338],{},"users"," and keeps names as text, so an\norganization's trail survives a member erasing their account. Account\nerasure doesn't touch it.",[206,2341,2342,2343,2345,2346,2349],{},"A role you remove from ",[182,2344,1223],{}," stays in ",[182,2347,2348],{},"org_members.role"," and grants\nnothing. Invitations for it stop working. Migrate the rows yourself.",[2351,2352,2353],"style",{},"html pre.shiki code .s8jYJ, html code.shiki .s8jYJ{--shiki-light:#D73A49;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sxrX7, html code.shiki .sxrX7{--shiki-light:#24292E;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sIIMD, html code.shiki .sIIMD{--shiki-light:#032F62;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .snPdu, html code.shiki .snPdu{--shiki-light:#6F42C1;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sP4rz, html code.shiki .sP4rz{--shiki-light:#E36209;--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sBjJW, html code.shiki .sBjJW{--shiki-light:#005CC5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .sovSZ, html code.shiki .sovSZ{--shiki-light:#22863A;--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":291,"searchDepth":17,"depth":17,"links":2355},[2356,2357,2358,2359,2360,2361,2362,2363,2364,2365,2366,2367,2368],{"id":200,"depth":17,"text":201},{"id":264,"depth":17,"text":265},{"id":612,"depth":17,"text":613},{"id":878,"depth":17,"text":879},{"id":1257,"depth":17,"text":1258},{"id":1477,"depth":17,"text":1478},{"id":1525,"depth":17,"text":1526},{"id":1642,"depth":17,"text":1643},{"id":1712,"depth":17,"text":1713},{"id":2074,"depth":17,"text":2075},{"id":2099,"depth":17,"text":2100},{"id":2114,"depth":17,"text":20},{"id":2308,"depth":17,"text":2309},[2370,2371],{"path":43,"title":44,"description":45,"navTitle":44,"group":40,"order":46},{"path":53,"title":54,"description":55,"navTitle":54,"group":40,"order":56}]