[{"data":1,"prerenderedAt":405},["ShallowReactive",2],{"docs:/docs/scopes/concepts":3},{"id":4,"title":5,"body":6,"description":397,"extension":398,"meta":399,"navigation":400,"path":401,"seo":402,"stem":403,"__hash__":404},"docs/docs/scopes/concepts.md","Access Scopes - How They Work",{"type":7,"value":8,"toc":384},"minimark",[9,13,17,22,43,46,50,53,58,68,72,78,97,100,104,111,117,151,155,169,212,216,357,361],[10,11,5],"h1",{"id":12},"access-scopes-how-they-work",[14,15,16],"p",{},"Scopes follow the actor, not the room or the app. This one design choice makes multi-tenancy simple.",[18,19,21],"h2",{"id":20},"core-concept-the-scope-follows-the-actor","Core Concept: The Scope Follows the Actor",[14,23,24,25,29,30,33,34,38,39,42],{},"Access Scopes are ",[26,27,28],"strong",{},"not attached to rooms or apps",". They are attached to ",[26,31,32],{},"actors",". When an actor authenticates, the control plane resolves its scope and hands the broker two things: the scope's id and slug, and the list of topic patterns the actor may reach, every one of them already carrying the scope slug. From then on the broker rewrites the actor's plain ",[35,36,37],"code",{},"app/room/topic"," subscriptions and publishes to ",[35,40,41],{},"app/scope/room/topic",". The actor and the SDK do not need to know about scopes at all; the broker does.",[14,44,45],{},"This means you can add multi-tenant isolation to an existing application by creating scopes and assigning actors to them. No client code changes, no room duplication, no SDK upgrades.",[18,47,49],{"id":48},"topic-namespace-diagram","Topic Namespace Diagram",[14,51,52],{},"The scope slug is inserted between the app slug and the room slug in the client-facing topic address:",[54,55,57],"h3",{"id":56},"without-scope-unscoped-actor","Without Scope (unscoped actor)",[59,60,66],"pre",{"className":61,"code":63,"language":64,"meta":65},[62],"language-text","app-slug / room-slug / topic-name\n   |           |            |\n   app         room         topic\n","text","",[35,67,63],{"__ignoreMap":65},[54,69,71],{"id":70},"with-scope-scoped-actor","With Scope (scoped actor)",[59,73,76],{"className":74,"code":75,"language":64,"meta":65},[62],"app-slug / scope-slug / room-slug / topic-name\n   |           |            |            |\n   app       scope         room         topic\n",[35,77,75],{"__ignoreMap":65},[14,79,80,81,84,85,88,89,92,93,96],{},"That four-segment form is what the broker matches against the actor's allowed patterns. Behind it, the broker maps each pattern to a distinct internal routing topic: ",[35,82,83],{},"{scopeId}/{roomId}/{topic}"," for a scoped actor, ",[35,86,87],{},"{roomId}/{topic}"," for an unscoped one. An actor subscribed to ",[35,90,91],{},"my-app-a3f9/client-acme/chat/messages"," therefore never shares a routing key with ",[35,94,95],{},"my-app-a3f9/client-beta/chat/messages",", even though both name the same app, room and topic.",[14,98,99],{},"Isolation is enforced by the ACL, not by string matching alone. A scoped actor's allowed patterns all carry its own scope slug, so a subscribe or publish that names another scope, or that tries the three-segment unscoped form, matches nothing and is refused. Writing the four-segment form yourself works too, as long as it names the actor's own scope.",[18,101,103],{"id":102},"data-model","Data Model",[14,105,106,107,110],{},"Scopes are a ",[26,108,109],{},"project-level"," resource. They exist at the same level as apps and actors in the NoLag hierarchy:",[59,112,115],{"className":113,"code":114,"language":64,"meta":65},[62],"Organization\n  |-- Project\n       |-- App (has rooms, topics)\n       |-- Actor (has accessScopeId FK, nullable)\n       |-- Access Scope (slug, name, description, metadata, isActive)\n",[35,116,114],{"__ignoreMap":65},[118,119,120,128,142],"ul",{},[121,122,123,124,127],"li",{},"An actor has an optional ",[35,125,126],{},"accessScopeId"," foreign key pointing to an Access Scope",[121,129,130,131,133,134,137,138,141],{},"If ",[35,132,126],{}," is ",[35,135,136],{},"null",", the actor is ",[26,139,140],{},"unscoped"," and uses the default topic namespace",[121,143,130,144,146,147,150],{},[35,145,126],{}," is set, the actor is ",[26,148,149],{},"scoped"," and its topics include the scope slug",[18,152,154],{"id":153},"no-sdk-changes-needed","No SDK Changes Needed",[14,156,157,158,161,162,164,165,168],{},"The client SDKs never see the scope. ",[35,159,160],{},"@nolag/js-sdk",", the Python SDK and the Go SDK subscribe and publish to ",[35,163,37],{}," (or through the fluent ",[35,166,167],{},"client.setApp(slug).setRoom(slug)"," context), and message handlers keep firing on the topic name you registered. All of the work happens in the platform:",[118,170,171,177,206],{},[121,172,173,176],{},[26,174,175],{},"The control plane"," resolves the actor's scope at authentication and at every revalidation, and builds the actor's allowed patterns with the scope slug already in place.",[121,178,179,182,183,185,186,189,190,192,193,192,196,199,200,205],{},[26,180,181],{},"The broker"," stores the scope id and slug on the connection, rewrites ",[35,184,37],{}," to ",[35,187,188],{},"app/\u003Cscope>/room/topic"," on subscribe and publish, and maps that to a scope-specific internal topic. It also includes the scope (",[35,191,126],{},", ",[35,194,195],{},"slug",[35,197,198],{},"name",") in ",[201,202,204],"a",{"href":203},"/docs/concepts/webhooks","webhook payloads",", so a hydration or trigger endpoint can route to the right tenant without a lookup.",[121,207,208,211],{},[26,209,210],{},"Your client code"," is unchanged.",[18,213,215],{"id":214},"key-rules","Key Rules",[217,218,219,232],"table",{},[220,221,222],"thead",{},[223,224,225,229],"tr",{},[226,227,228],"th",{},"Rule",[226,230,231],{},"Details",[233,234,235,249,262,288,298,308,318,336],"tbody",{},[223,236,237,243],{},[238,239,240],"td",{},[26,241,242],{},"One scope per actor",[238,244,245,246,248],{},"An actor can belong to at most one scope at a time. To move an actor to a different scope, update the ",[35,247,126],{}," field.",[223,250,251,256],{},[238,252,253],{},[26,254,255],{},"Nullable FK",[238,257,258,259,261],{},"The ",[35,260,126],{}," is optional. Actors without a scope use the default (unscoped) topic namespace and can communicate with other unscoped actors.",[223,263,264,269],{},[238,265,266],{},[26,267,268],{},"Slug is immutable",[238,270,271,272,274,275,192,277,192,280,283,284,287],{},"Once a scope is created, its ",[35,273,195],{}," cannot be changed. The slug is part of every topic address the scope's actors resolve to, so changing it would break existing subscriptions. ",[35,276,198],{},[35,278,279],{},"description",[35,281,282],{},"metadata"," and ",[35,285,286],{},"isActive"," can be updated.",[223,289,290,295],{},[238,291,292],{},[26,293,294],{},"Project-level resource",[238,296,297],{},"Scopes belong to a project, not to an individual app. All apps in the project use the same set of scopes, and an actor's scope must belong to the actor's own project.",[223,299,300,305],{},[238,301,302],{},[26,303,304],{},"Scoped actors cannot see unscoped actors",[238,306,307],{},"A scoped actor and an unscoped actor are in different topic namespaces. They cannot communicate even if they are in the same room.",[223,309,310,315],{},[238,311,312],{},[26,313,314],{},"Takes effect on connect",[238,316,317],{},"The broker reads the scope when the actor authenticates. A changed assignment applies at the actor's next connection, or within about 10 minutes on a live connection when the broker revalidates it.",[223,319,320,325],{},[238,321,322],{},[26,323,324],{},"An inactive or deleted scope denies its actors",[238,326,327,328,331,332,335],{},"If a scope is set to ",[35,329,330],{},"isActive: false",", or can no longer be resolved, its actors are not unscoped. The control plane refuses their session with ",[35,333,334],{},"scope_inactive",": a new connection is rejected at auth, and a live connection is closed at its next revalidation. Deactivating a scope never widens access.",[223,337,338,343],{},[238,339,340],{},[26,341,342],{},"Delete only when empty",[238,344,345,348,349,352,353,356],{},[35,346,347],{},"DELETE /v1/scopes/:id"," returns ",[35,350,351],{},"409 Conflict"," while any actor is still assigned. Unscope the actors first (",[35,354,355],{},"accessScopeId: null","), then delete.",[18,358,360],{"id":359},"next-steps","Next Steps",[118,362,363,370,377],{},[121,364,365,369],{},[201,366,368],{"href":367},"/docs/scopes/getting-started","Getting Started"," - create your first scope and assign actors",[121,371,372,376],{},[201,373,375],{"href":374},"/docs/scopes/multi-tenancy","Multi-Tenancy Patterns"," - real-world patterns for tenant isolation",[121,378,379,383],{},[201,380,382],{"href":381},"/docs/scopes/api-reference","API Reference"," - complete endpoint documentation",{"title":65,"searchDepth":385,"depth":385,"links":386},2,[387,388,393,394,395,396],{"id":20,"depth":385,"text":21},{"id":48,"depth":385,"text":49,"children":389},[390,392],{"id":56,"depth":391,"text":57},3,{"id":70,"depth":391,"text":71},{"id":102,"depth":385,"text":103},{"id":153,"depth":385,"text":154},{"id":214,"depth":385,"text":215},{"id":359,"depth":385,"text":360},"Understand how Access Scopes provide tenant isolation through topic namespacing. Learn the data model, key rules, and why no SDK changes are needed.","md",{},true,"/docs/scopes/concepts",{"title":5,"description":397},"docs/scopes/concepts","wUJ47S8_sXkXLOZHkK53WN13pD4sk8jr9Wp8YpXZYnA",1789869421065]