[{"data":1,"prerenderedAt":673},["ShallowReactive",2],{"docs:doc:\u002Fen\u002Fdocs\u002Fguides\u002Fconcepts":3,"docs:pages:docs_en":606},{"id":4,"title":5,"alt":6,"body":7,"description":595,"extension":596,"key":597,"meta":598,"navigation":599,"order":600,"path":601,"placeholder":602,"seo":603,"stem":604,"__hash__":605},"docs_en\u002Fen\u002Fdocs\u002Fguides\u002Fconcepts.md","Workspaces, roles and environments","\u002Fpl\u002Fdocs\u002Fprzewodniki\u002Fpojecia",{"type":8,"value":9,"toc":582},"minimark",[10,14,25,28,61,66,85,88,99,103,118,218,227,230,235,242,252,255,266,269,289,295,299,305,323,326,330,333,337,355,361,382,395,413,417,420,527,540,547,557,561,578],[11,12,13],"p",{},"Five words carry the whole model, and they nest:",[15,16,21],"pre",{"className":17,"code":19,"language":20},[18],"language-text","Account          — you, the customer, and the billing entity\n└── App          — one authentication realm: your product\n    └── Environment   — production and sandbox, exactly two, always\n        ├── User      — a person with credentials, belonging to this environment\n        └── Workspace — a group your customers manage, holding Members with roles\n","text",[22,23,19],"code",{"__ignoreMap":24},"",[11,26,27],{},"Nothing crosses an environment. A user of sandbox does not exist in production;\na role defined in one is not the role in the other, even with the same name.",[29,30,33],"callout",{"title":31,"type":32},"Workspace, or tenant?","note",[11,34,35,36,40,41,44,45,48,49,51,52,55,56,60],{},"The same thing, in two vocabularies. The pages your users see say ",[37,38,39],"strong",{},"workspace",";\nthe API, the SDKs and the token claims say ",[37,42,43],{},"tenant"," — ",[22,46,47],{},"\u002Ftenants\u002F{id}",", the\n",[22,50,43],{}," claim, ",[22,53,54],{},"tenant_slug",". We never use \"tenant\" to mean ",[57,58,59],"em",{},"our"," customer;\nthat is an Account.",[62,63,65],"h2",{"id":64},"app","App",[11,67,68,69,72,73,76,77,80,81,84],{},"An App is one product with one set of users. It gets a ",[37,70,71],{},"slug",", and the slug\nbecomes two permanent addresses — ",[22,74,75],{},"https:\u002F\u002Facme.kleora.eu"," and\n",[22,78,79],{},"https:\u002F\u002Facme.sandbox.kleora.eu",". Those are the App's issuers: the ",[22,82,83],{},"iss","\nclaim of every token it mints, the host its sign-in pages are served from, and\nthe base of its discovery document.",[11,86,87],{},"The slug cannot be changed afterwards. Every service that has ever verified one\nof your tokens has that string cached; picking it deserves the care a domain\nname gets.",[11,89,90,91,94,95,98],{},"Creating an App creates, in one transaction, both environments and in each of\nthem: a default workspace, a ",[22,92,93],{},"spa"," client named ",[37,96,97],{},"Default",", the signing keys,\nthe default branding and settings, the reserved management permissions and the\ndefault email templates. There is nothing to provision afterwards.",[62,100,102],{"id":101},"environment","Environment",[11,104,105,106,109,110,113,114,117],{},"Two per App, ",[22,107,108],{},"production"," and ",[22,111,112],{},"sandbox",", and ",[37,115,116],{},"structurally identical",". Same\nfeatures, same endpoints, same shapes. They differ in the ways that keep them\nfrom being confused for one another:",[119,120,121,136],"table",{},[122,123,124],"thead",{},[125,126,127,130,133],"tr",{},[128,129],"th",{},[128,131,132],{},"Production",[128,134,135],{},"Sandbox",[137,138,139,155,170,186,207],"tbody",{},[125,140,141,145,150],{},[142,143,144],"td",{},"Issuer",[142,146,147],{},[22,148,149],{},"acme.kleora.eu",[142,151,152],{},[22,153,154],{},"acme.sandbox.kleora.eu",[125,156,157,160,165],{},[142,158,159],{},"API key prefix",[142,161,162],{},[22,163,164],{},"kl_live_",[142,166,167],{},[22,168,169],{},"kl_test_",[125,171,172,178,182],{},[142,173,174,177],{},[22,175,176],{},"env"," claim in tokens",[142,179,180],{},[22,181,108],{},[142,183,184],{},[22,185,112],{},[125,187,188,191,197],{},[142,189,190],{},"Redirect URIs",[142,192,193,196],{},[22,194,195],{},"https:\u002F\u002F"," only",[142,198,199,200,109,203,206],{},"also ",[22,201,202],{},"http:\u002F\u002Flocalhost",[22,204,205],{},"http:\u002F\u002F127.0.0.1",", any port, any path",[125,208,209,212,215],{},[142,210,211],{},"Counted for billing",[142,213,214],{},"yes",[142,216,217],{},"never",[11,219,220,221,223,224,226],{},"The SDKs refuse to mix them: a ",[22,222,169],{}," key with a production issuer, or a\ntoken whose ",[22,225,176],{}," disagrees with the issuer it was configured with, fails at\nconfiguration time rather than in production at two in the morning. The\nenvironment is visible in every URL you paste, which is the cheapest guard\nthere is against pointing production code at sandbox.",[11,228,229],{},"Each environment also holds its own settings — token lifetimes, session\nlifetimes, password rules, whether people may register themselves. The defaults\nare an access token of 15 minutes, a refresh token of 30 days, and a session\nthat lasts 7 days with a 24-hour idle limit; each is adjustable within a range.",[231,232,234],"h3",{"id":233},"promotion-rather-than-editing-production-twice","Promotion, rather than editing production twice",[11,236,237,238,241],{},"Configuration moves from sandbox to production in one operation: ",[37,239,240],{},"promote",".\nIt copies the settings, the branding, the permissions, the app-scoped roles and\nthe email templates, and it can be run as a dry run first, which returns the\nsame diff and changes nothing.",[11,243,244,245,248,249,251],{},"What it never copies is everything that belongs to real people: users,\nidentities, sessions, workspaces, members, invitations, clients, API keys and\nsigning keys. Production members keep the roles they hold; because roles are\nmatched by slug, a member holding ",[22,246,247],{},"editor"," simply gains ",[22,250,247],{},"'s new\npermission set on their next token.",[62,253,254],{"id":39},"Workspace",[11,256,257,258,261,262,265],{},"A workspace is a group ",[37,259,260],{},"your customers"," manage inside one environment — a\ncompany, a team, a project. A User joins one as a ",[37,263,264],{},"Member",", and it is the\nmembership that carries roles. The same person can be a member of several\nworkspaces with different roles in each.",[11,267,268],{},"An environment is in one of two modes:",[270,271,272,281],"ul",{},[273,274,275,280],"li",{},[37,276,277],{},[22,278,279],{},"single"," — the default, and the right answer for a consumer product.\nThere is one workspace, it is hidden, and every new user joins it\nautomatically. Nobody is asked to choose anything at sign-in.",[273,282,283,288],{},[37,284,285],{},[22,286,287],{},"multi"," — workspaces are real. They appear in lists, they can be created,\nand a sign-in resolves to exactly one of them.",[11,290,291,292,294],{},"Switching to ",[22,293,287],{}," needs no migration. Switching back is allowed only when\nthe default workspace is the only one left, because the alternative is deleting\nworkspaces that still hold members on your behalf.",[231,296,298],{"id":297},"which-workspace-a-sign-in-lands-in","Which workspace a sign-in lands in",[11,300,301,302,304],{},"In ",[22,303,287],{}," mode, after the person has authenticated:",[306,307,308,314,317,320],"ol",{},[273,309,310,311,313],{},"If the authorization request pinned a workspace (the ",[22,312,43],{}," parameter),\nthat one — or the sign-in fails, rather than quietly falling back to another.",[273,315,316],{},"Otherwise, if the session already has a workspace they are still a member\nof, that one is reused.",[273,318,319],{},"Otherwise, exactly one candidate resolves to itself; several show a chooser.",[273,321,322],{},"None at all does what the environment's settings say: refuse, or offer a\n\"create your workspace\" form.",[11,324,325],{},"The result is stored on the session, so every later token in that session\ncarries the same workspace until it is deliberately switched.",[231,327,329],{"id":328},"invitations","Invitations",[11,331,332],{},"An invitation names an email address and the roles it will carry, and it is\nopen for seven days. Accepting proves control of the address, so somebody\ninvited who has no account signs up with the email pre-filled and read-only —\nand lands verified, because clicking the link was the proof. One open\ninvitation per address per workspace; resending issues a new token and\ninvalidates the old one.",[62,334,336],{"id":335},"roles-and-permissions","Roles and permissions",[11,338,339,340,343,344,44,347,350,351,354],{},"A ",[37,341,342],{},"permission"," is a string of the form ",[22,345,346],{},"resource:verb",[22,348,349],{},"invoices:read",",\n",[22,352,353],{},"reports:write",". You define your own; they mean whatever your application\ndecides they mean.",[11,356,339,357,360],{},[37,358,359],{},"role"," is a bundle of them, and it has one of two scopes:",[270,362,363,376],{},[273,364,365,368,369,109,372,375],{},[37,366,367],{},"App-scoped"," — defined on the environment, assignable in every workspace of\nit. This is where ",[22,370,371],{},"admin",[22,373,374],{},"member"," usually live.",[273,377,378,381],{},[37,379,380],{},"Workspace-scoped"," — defined inside one workspace, assignable only there.\nThis is how one customer gives themselves a role the others do not have.",[11,383,384,385,387,388,390,391,394],{},"A role's slug is what tokens carry, so it is immutable after creation; the name\nand the description are not. Slugs cannot collide across the two scopes — an\napp-scoped ",[22,386,371],{}," blocks a workspace-scoped ",[22,389,371],{}," everywhere — which is what\nkeeps a token's ",[22,392,393],{},"roles"," array unambiguous.",[11,396,397,398,401,402,405,406,405,409,412],{},"Kleora also creates a set of ",[37,399,400],{},"reserved management permissions"," in every\nenvironment (",[22,403,404],{},"users:read",", ",[22,407,408],{},"members:write",[22,410,411],{},"roles:write",", and so on). You\ncannot delete or rename them, but you can put them in a role — and that is how\nyou let a customer's own administrator manage their workspace's members through\nthe API, with their own access token, without becoming an administrator of\nyours.",[62,414,416],{"id":415},"what-the-token-carries-and-when-it-changes","What the token carries, and when it changes",[11,418,419],{},"Every access token carries the resolved workspace and the authorisation that\ngoes with it:",[15,421,425],{"className":422,"code":423,"language":424,"meta":24,"style":24},"language-json shiki shiki-themes github-light","{\n  \"sub\": \"…\",\n  \"tenant\": \"…\",\n  \"tenant_slug\": \"acme\",\n  \"roles\": [\"admin\"],\n  \"permissions\": [\"invoices:read\", \"invoices:write\"],\n  \"env\": \"production\"\n}\n","json",[22,426,427,436,452,464,477,492,510,521],{"__ignoreMap":24},[428,429,432],"span",{"class":430,"line":431},"line",1,[428,433,435],{"class":434},"sgsFI","{\n",[428,437,439,443,446,450],{"class":430,"line":438},2,[428,440,442],{"class":441},"sYu0t","  \"sub\"",[428,444,445],{"class":434},": ",[428,447,449],{"class":448},"sYBdl","\"…\"",[428,451,350],{"class":434},[428,453,455,458,460,462],{"class":430,"line":454},3,[428,456,457],{"class":441},"  \"tenant\"",[428,459,445],{"class":434},[428,461,449],{"class":448},[428,463,350],{"class":434},[428,465,467,470,472,475],{"class":430,"line":466},4,[428,468,469],{"class":441},"  \"tenant_slug\"",[428,471,445],{"class":434},[428,473,474],{"class":448},"\"acme\"",[428,476,350],{"class":434},[428,478,480,483,486,489],{"class":430,"line":479},5,[428,481,482],{"class":441},"  \"roles\"",[428,484,485],{"class":434},": [",[428,487,488],{"class":448},"\"admin\"",[428,490,491],{"class":434},"],\n",[428,493,495,498,500,503,505,508],{"class":430,"line":494},6,[428,496,497],{"class":441},"  \"permissions\"",[428,499,485],{"class":434},[428,501,502],{"class":448},"\"invoices:read\"",[428,504,405],{"class":434},[428,506,507],{"class":448},"\"invoices:write\"",[428,509,491],{"class":434},[428,511,513,516,518],{"class":430,"line":512},7,[428,514,515],{"class":441},"  \"env\"",[428,517,445],{"class":434},[428,519,520],{"class":448},"\"production\"\n",[428,522,524],{"class":430,"line":523},8,[428,525,526],{"class":434},"}\n",[11,528,529,531,532,535,536,539],{},[22,530,393],{}," is the member's role slugs; ",[22,533,534],{},"permissions"," is the union of those roles'\npermissions, de-duplicated and sorted. Both are always present — ",[22,537,538],{},"[]"," rather\nthan absent — so an authorisation check never has to test for absence first.\nYour API reads them from the token it has already verified.",[11,541,542,543,546],{},"The cost of that is the one thing to design around: ",[37,544,545],{},"a change to a role, or to\na member's roles, is visible at the next token issuance"," — within the access\ntoken's lifetime for somebody holding one, immediately on their next refresh.\nThere is no revocation list for access tokens. When a change has to take effect\nnow rather than in fifteen minutes, revoke that user's sessions; the next\nrequest has no valid token at all.",[29,548,550],{"title":549,"type":32},"A ceiling worth knowing about",[11,551,552,553,556],{},"If a member's permissions exceed 200 entries, the reserved management ones are\nkept in full and your own are dropped from the end of the sorted list, and the\ntoken gains ",[22,554,555],{},"permissions_truncated: true",". Somebody designing per-object\npermissions as permission strings will meet this; roles are the cheaper shape.",[62,558,560],{"id":559},"where-to-go-next","Where to go next",[270,562,563,571],{},[273,564,565,570],{},[566,567,569],"a",{"href":568},"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts","Framework quickstarts"," — the code that turns\nall of this into a signed-in user.",[273,572,573,577],{},[566,574,576],{"href":575},"\u002Fen\u002Fdocs\u002Fguides\u002Farchitecture","How Kleora is put together"," — where the\nenvironment boundary lives in the running system.",[579,580,581],"style",{},"html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}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);}",{"title":24,"searchDepth":454,"depth":454,"links":583},[584,585,588,592,593,594],{"id":64,"depth":438,"text":65},{"id":101,"depth":438,"text":102,"children":586},[587],{"id":233,"depth":454,"text":234},{"id":39,"depth":438,"text":254,"children":589},[590,591],{"id":297,"depth":454,"text":298},{"id":328,"depth":454,"text":329},{"id":335,"depth":438,"text":336},{"id":415,"depth":438,"text":416},{"id":559,"depth":438,"text":560},"The five words the product is built from, which of them your tokens carry, and how a change to a role reaches a running application.","md","guides\u002Fconcepts",{},true,20,"\u002Fen\u002Fdocs\u002Fguides\u002Fconcepts",false,{"title":5,"description":595},"en\u002Fdocs\u002Fguides\u002Fconcepts","Z_oX9W8WUgxW7TA61yxeGTpfB1xU7G4LM9_6WkJlPxs",[607,613,618,623,627,628,633,638,644,647,652,657,663,668],{"path":608,"title":609,"description":610,"order":611,"key":612},"\u002Fen\u002Fdocs\u002Fgetting-started","Getting started","From an empty project to a working sign-in, and the packages that get you there.",10,"getting-started\u002Findex",{"path":614,"title":615,"description":616,"order":600,"key":617},"\u002Fen\u002Fdocs\u002Fgetting-started\u002Finstallation","Installing an SDK","The five packages we publish, what each one is for, and the configuration each one takes.","getting-started\u002Finstallation",{"path":619,"title":620,"description":621,"order":611,"key":622},"\u002Fen\u002Fdocs\u002Fgetting-started\u002Fquickstart","Quickstart","Create an App, wire up the browser SDK, and sign in for the first time — entirely in sandbox.","getting-started\u002Fquickstart",{"path":575,"title":576,"description":624,"order":625,"key":626},"The shape of the hosted service — one address per environment, two API surfaces, and where state lives. And why self-hosting is not something we support today.",30,"guides\u002Farchitecture",{"path":601,"title":5,"description":595,"order":600,"key":597},{"path":629,"title":630,"description":631,"order":600,"key":632},"\u002Fen\u002Fdocs\u002Fguides","Guides","Wiring Kleora into your stack, the words the product uses, and how the hosted service is put together.","guides\u002Findex",{"path":634,"title":635,"description":636,"order":611,"key":637},"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fbrowser","Plain browser","One client instance, one callback route, and an access token for every request your application makes.","guides\u002Fquickstarts\u002Fbrowser",{"path":639,"title":640,"description":641,"order":642,"key":643},"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fexpress","Express","Verifying the access token on your own API — middleware, permissions, and the three answers a refusal can have.",40,"guides\u002Fquickstarts\u002Fexpress",{"path":568,"title":569,"description":645,"order":611,"key":646},"One page per stack — Next.js, Nuxt, Express and the plain browser — and the two redirect URIs all of them share.","guides\u002Fquickstarts\u002Findex",{"path":648,"title":649,"description":650,"order":600,"key":651},"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fnextjs","Next.js","The browser SDK in an App Router application — a lazy client, a callback route, and token verification in a route handler.","guides\u002Fquickstarts\u002Fnextjs",{"path":653,"title":654,"description":655,"order":625,"key":656},"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts\u002Fnuxt","Nuxt","The Nuxt module — two lines of config, a callback page you do not write, and a route middleware that protects a page from its own meta.","guides\u002Fquickstarts\u002Fnuxt",{"path":658,"title":659,"description":660,"order":661,"key":662},"\u002Fen\u002Fdocs","Kleora documentation","Add hosted sign-in to your application, verify the token on your API, and manage users, workspaces and roles from one console.",0,"index",{"path":664,"title":665,"description":666,"order":611,"key":667},"\u002Fen\u002Fdocs\u002Freference\u002Fcode-highlighting","Code highlighting","One fence per preloaded Shiki grammar — a fixture, not a reference page.","reference\u002Fcode-highlighting",{"path":669,"title":670,"description":671,"order":625,"key":672},"\u002Fen\u002Fdocs\u002Freference","Reference","Reference material for the HTTP APIs and the SDK packages.","reference\u002Findex",1790698197402]