[{"data":1,"prerenderedAt":381},["ShallowReactive",2],{"docs:doc:\u002Fen\u002Fdocs\u002Fguides\u002Farchitecture":3,"docs:pages:docs_en":310},{"id":4,"title":5,"alt":6,"body":7,"description":299,"extension":300,"key":301,"meta":302,"navigation":303,"order":304,"path":305,"placeholder":306,"seo":307,"stem":308,"__hash__":309},"docs_en\u002Fen\u002Fdocs\u002Fguides\u002Farchitecture.md","How Kleora is put together","\u002Fpl\u002Fdocs\u002Fprzewodniki\u002Farchitektura",{"type":8,"value":9,"toc":287},"minimark",[10,22,27,39,46,53,57,123,134,141,145,152,159,166,170,214,217,220,224,250,254,274,277,281,284],[11,12,15,19],"callout",{"title":13,"type":14},"Self-hosting is not supported today","warning",[16,17,18],"p",{},"There is no published image, no supported configuration and no deployment\nguide, because there is nothing behind one: the pieces below run as a single\ndeployable, and the boundary that would let you place them yourself does not\nexist yet. If you are here to find out how to run Kleora on your own\ninfrastructure, the honest answer is that you cannot, and we would rather say\nso here than let you find out three hours in.",[16,20,21],{},"What follows is the architecture of the hosted service — accurate, and useful\nfor reasoning about latency, failure and where your users' data sits.",[23,24,26],"h2",{"id":25},"one-address-per-environment","One address per environment",[16,28,29,30,34,35,38],{},"Every environment of every App has its own issuer hostname:\n",[31,32,33],"code",{},"https:\u002F\u002Facme.kleora.eu"," for production,\n",[31,36,37],{},"https:\u002F\u002Facme.sandbox.kleora.eu"," for sandbox. That is not cosmetic. An\nOpenID Connect issuer is identified by a URL, so giving each environment its\nown means discovery, key sets, tokens and sessions are separated by the\nprotocol itself rather than by a check somewhere in our code.",[16,40,41,42,45],{},"A request arriving on one of those hostnames is resolved to exactly one\nenvironment before anything is routed. An unknown host is a ",[31,43,44],{},"404",", never a\nfallback to somewhere else.",[16,47,48,49,52],{},"The management API answers on a different host — ",[31,50,51],{},"api.kleora.io"," — and\nonly there.",[23,54,56],{"id":55},"two-surfaces-and-what-each-is-allowed-to-be","Two surfaces, and what each is allowed to be",[58,59,60,76],"table",{},[61,62,63],"thead",{},[64,65,66,70,73],"tr",{},[67,68,69],"th",{},"Surface",[67,71,72],{},"Where it answers",[67,74,75],{},"Who may call it",[77,78,79,98,109],"tbody",{},[64,80,81,85,95],{},[82,83,84],"td",{},"OAuth 2.1 \u002F OpenID Connect",[82,86,87,90,91,94],{},[31,88,89],{},"\u002Foauth\u002F",", ",[31,92,93],{},"\u002F.well-known\u002F"," on an issuer host",[82,96,97],{},"Anyone. It is the surface that issues credentials.",[64,99,100,103,106],{},[82,101,102],{},"The hosted sign-in pages",[82,104,105],{},"everything else on an issuer host",[82,107,108],{},"Your users, in a browser.",[64,110,111,114,120],{},[82,112,113],{},"The management API",[82,115,116,119],{},[31,117,118],{},"\u002Fapi\u002Fv1\u002F"," on the management host",[82,121,122],{},"Authenticated principals, scoped to one environment or account.",[16,124,125,126,130,131,133],{},"The split is enforced by the host the request arrived on: the management API\n",[127,128,129],"strong",{},"does not exist"," on an issuer hostname, and the OAuth surface does not exist\non the management host. Not \"is refused\" — the routes are not mounted, so the\nanswer is a ",[31,132,44],{}," by construction. The two also live in separate packages in\nthe source, so the question \"is this reachable without credentials?\" always has\na deliberate answer rather than an accidental one.",[16,135,136,137,140],{},"In front of both sits an edge proxy. For an issuer host it decides by path:\nthe OAuth and flow endpoints go to the application, and everything else is\nserved from the sign-in bundle. It forwards the ",[31,138,139],{},"Host"," header untouched, which\nis what lets one deployment answer for every customer's issuer without a list\nof them anywhere.",[23,142,144],{"id":143},"the-sign-in-pages-are-a-static-application","The sign-in pages are a static application",[16,146,147,148,151],{},"The pages your users see — sign in, sign up, verify email, reset password,\nchoose a workspace — are a static single-page application served on your App's\nown issuer host. It talks only to ",[31,149,150],{},"\u002Fflow\u002F*"," on that same origin, which is a\nJSON API; no HTML is rendered by the server at all.",[16,153,154,155,158],{},"Two consequences worth knowing. Your branding is ",[127,156,157],{},"data",", not a template: the\npage fetches the branding record as JSON and applies it, which is why a colour\nchange takes effect without a deploy on either side. And sign-in requires\nJavaScript.",[16,160,161,162,165],{},"Completion of a flow is always a top-level navigation back to\n",[31,163,164],{},"\u002Foauth\u002Fauthorize\u002Fcontinue",", so an authorization code is only ever delivered in\na navigation response — never in a JSON body a script could read.",[23,167,169],{"id":168},"where-state-lives","Where state lives",[58,171,172,182],{},[61,173,174],{},[64,175,176,179],{},[67,177,178],{},"Store",[67,180,181],{},"What it holds",[77,183,184,194,204],{},[64,185,186,191],{},[82,187,188],{},[127,189,190],{},"PostgreSQL",[82,192,193],{},"The system of record. Accounts, Apps, environments, users, workspaces, memberships, roles, clients, keys and the audit log.",[64,195,196,201],{},[82,197,198],{},[127,199,200],{},"Redis",[82,202,203],{},"Cache, rate-limit counters and background-job results — everything that can be rebuilt or expired.",[64,205,206,211],{},[82,207,208],{},[127,209,210],{},"RabbitMQ",[82,212,213],{},"The queue for work that must not be lost between being scheduled and being done: email delivery above all.",[16,215,216],{},"Every row that belongs to an environment carries that environment's identity,\nand the application's data access is scoped to one environment by construction\n— a query with no environment is a programming error that fails rather than\nreturning somebody else's rows.",[16,218,219],{},"Access tokens are stateless: they are signed JWTs, and nothing is looked up to\nverify one. Refresh tokens, sessions and authorization codes are the opposite —\nrows in PostgreSQL, so revocation is immediate for them.",[23,221,223],{"id":222},"what-that-means-for-your-application","What that means for your application",[225,226,227,234,244],"ul",{},[228,229,230,233],"li",{},[127,231,232],{},"Verification costs you no request to us."," Your API fetches the key set\nonce, caches it for as long as the response says, and verifies signatures\nlocally. Roles and permissions are in the token.",[228,235,236,239,240,243],{},[127,237,238],{},"Key rotation does not need a coordinated deploy."," The next signing key is\npublished in the key set before it is ever used, so the ",[31,241,242],{},"kid"," of a freshly\nsigned token is already in your cache.",[228,245,246,249],{},[127,247,248],{},"The two surfaces are not isolated from each other yet."," They are one\ndeployable today, so they cannot be scaled or rate-limited independently. The\ncode boundary is real and the deployment boundary is the future work; we\nwould rather say that than imply an isolation you are not getting.",[23,251,253],{"id":252},"the-contracts-you-can-build-against","The contracts you can build against",[225,255,256,262,268],{},[228,257,258,261],{},[31,259,260],{},"{issuer}\u002F.well-known\u002Fopenid-configuration"," — discovery, and from it the key\nset. Everything a generic OpenID Connect client needs; ours are a\nconvenience, not a requirement.",[228,263,264,267],{},[31,265,266],{},"{issuer}\u002Foauth\u002Fopenapi.json"," — the OpenAPI document for the OAuth surface.",[228,269,270,273],{},[31,271,272],{},"https:\u002F\u002Fapi.kleora.io\u002Fapi\u002Fv1\u002Fopenapi.json"," — the same for the\nmanagement API, and the document our own clients are generated from.",[16,275,276],{},"Authorization code with PKCE is the browser flow; implicit and password grants\nare not offered.",[23,278,280],{"id":279},"why-there-is-no-self-host-story","Why there is no self-host story",[16,282,283],{},"Three things would have to be true, and none of them is yet. There is no\ndeployment boundary between the two API surfaces, so \"run the public half in\nyour DMZ\" is not a configuration — it is a change to the architecture. There\nare no published images, and nothing versioned to run. And there is no\nsupported configuration: the settings that exist assume our own operational\ncontext, and we have never run the thing anywhere else.",[16,285,286],{},"We would rather publish this page than a deployment guide that quietly does not\nwork. If that changes it will be because there is something real to run, and\nthis page is where it will be said.",{"title":288,"searchDepth":289,"depth":289,"links":290},"",3,[291,293,294,295,296,297,298],{"id":25,"depth":292,"text":26},2,{"id":55,"depth":292,"text":56},{"id":143,"depth":292,"text":144},{"id":168,"depth":292,"text":169},{"id":222,"depth":292,"text":223},{"id":252,"depth":292,"text":253},{"id":279,"depth":292,"text":280},"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.","md","guides\u002Farchitecture",{},true,30,"\u002Fen\u002Fdocs\u002Fguides\u002Farchitecture",false,{"title":5,"description":299},"en\u002Fdocs\u002Fguides\u002Farchitecture","keQVmmSqOg14eUaFVMPXNGM4qqL8Ewh1qBEWegow52Q",[311,317,323,328,329,334,339,344,350,355,360,365,371,376],{"path":312,"title":313,"description":314,"order":315,"key":316},"\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":318,"title":319,"description":320,"order":321,"key":322},"\u002Fen\u002Fdocs\u002Fgetting-started\u002Finstallation","Installing an SDK","The five packages we publish, what each one is for, and the configuration each one takes.",20,"getting-started\u002Finstallation",{"path":324,"title":325,"description":326,"order":315,"key":327},"\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":305,"title":5,"description":299,"order":304,"key":301},{"path":330,"title":331,"description":332,"order":321,"key":333},"\u002Fen\u002Fdocs\u002Fguides\u002Fconcepts","Workspaces, roles and environments","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.","guides\u002Fconcepts",{"path":335,"title":336,"description":337,"order":321,"key":338},"\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":340,"title":341,"description":342,"order":315,"key":343},"\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":345,"title":346,"description":347,"order":348,"key":349},"\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":351,"title":352,"description":353,"order":315,"key":354},"\u002Fen\u002Fdocs\u002Fguides\u002Fquickstarts","Framework quickstarts","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":356,"title":357,"description":358,"order":321,"key":359},"\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":361,"title":362,"description":363,"order":304,"key":364},"\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":366,"title":367,"description":368,"order":369,"key":370},"\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":372,"title":373,"description":374,"order":315,"key":375},"\u002Fen\u002Fdocs\u002Freference\u002Fcode-highlighting","Code highlighting","One fence per preloaded Shiki grammar — a fixture, not a reference page.","reference\u002Fcode-highlighting",{"path":377,"title":378,"description":379,"order":304,"key":380},"\u002Fen\u002Fdocs\u002Freference","Reference","Reference material for the HTTP APIs and the SDK packages.","reference\u002Findex",1790698197406]