[{"data":1,"prerenderedAt":628},["ShallowReactive",2],{"navigation_docs":3,"-tenancy-and-rls":36,"-tenancy-and-rls-surround":623},[4,8,12,16,20,24,28,32],{"title":5,"path":6,"stem":7},"Getting started","\u002Fgetting-started","1.getting-started",{"title":9,"path":10,"stem":11},"Core concepts","\u002Fcore-concepts","2.core-concepts",{"title":13,"path":14,"stem":15},"Cross-cutting concerns","\u002Fcross-cutting","3.cross-cutting",{"title":17,"path":18,"stem":19},"Tenancy & RLS","\u002Ftenancy-and-rls","4.tenancy-and-rls",{"title":21,"path":22,"stem":23},"How the codegen works","\u002Fhow-the-codegen-works","5.how-the-codegen-works",{"title":25,"path":26,"stem":27},"vs. NestJS","\u002Fvs-nestjs","6.vs-nestjs",{"title":29,"path":30,"stem":31},"Before \u002F after","\u002Fbefore-after","7.before-after",{"title":33,"path":34,"stem":35},"Modules","\u002Fmodules","8.modules",{"id":37,"title":17,"body":38,"description":617,"extension":618,"links":619,"meta":620,"navigation":456,"path":18,"seo":621,"stem":19,"__hash__":622},"docs\u002F4.tenancy-and-rls.md",{"type":39,"value":40,"toc":611},"minimark",[41,46,55,257,271,275,304,512,517,604,607],[42,43,45],"h2",{"id":44},"the-model-bound-by-construction","The model: bound by construction",[47,48,49,50,54],"p",{},"roost's request scope makes multi-tenancy disappear from your method signatures. The per-request\n",[51,52,53],"code",{},"RequestContext"," (tenant + user) is registered as an injectable value, and a SCOPED, tenant-bound\nrepo is built from it once, per request:",[56,57,62],"pre",{"className":58,"code":59,"language":60,"meta":61,"style":61},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","@Injectable({ scope: 'scoped' })\nexport class ProjectsRepo {\n  constructor(\n    private readonly store: ProjectStore,\n    private readonly ctx: RequestContext, \u002F\u002F ← the tenant, injected\n  ) {}\n  all() {\n    return this.store.rows.get(this.ctx.tenantId) ?? [] \u002F\u002F scoped here, once\n  }\n}\n","ts","",[51,63,64,107,125,134,155,177,186,197,245,251],{"__ignoreMap":61},[65,66,69,73,77,81,84,88,91,94,98,101,104],"span",{"class":67,"line":68},"line",1,[65,70,72],{"class":71},"sMK4o","@",[65,74,76],{"class":75},"s2Zo4","Injectable",[65,78,80],{"class":79},"sTEyZ","(",[65,82,83],{"class":71},"{",[65,85,87],{"class":86},"swJcz"," scope",[65,89,90],{"class":71},":",[65,92,93],{"class":71}," '",[65,95,97],{"class":96},"sfazB","scoped",[65,99,100],{"class":71},"'",[65,102,103],{"class":71}," }",[65,105,106],{"class":79},")\n",[65,108,110,114,118,122],{"class":67,"line":109},2,[65,111,113],{"class":112},"s7zQu","export",[65,115,117],{"class":116},"spNyl"," class",[65,119,121],{"class":120},"sBMFI"," ProjectsRepo",[65,123,124],{"class":71}," {\n",[65,126,128,131],{"class":67,"line":127},3,[65,129,130],{"class":116},"  constructor",[65,132,133],{"class":71},"(\n",[65,135,137,140,143,147,149,152],{"class":67,"line":136},4,[65,138,139],{"class":116},"    private",[65,141,142],{"class":116}," readonly",[65,144,146],{"class":145},"sHdIc"," store",[65,148,90],{"class":71},[65,150,151],{"class":120}," ProjectStore",[65,153,154],{"class":71},",\n",[65,156,158,160,162,165,167,170,173],{"class":67,"line":157},5,[65,159,139],{"class":116},[65,161,142],{"class":116},[65,163,164],{"class":145}," ctx",[65,166,90],{"class":71},[65,168,169],{"class":120}," RequestContext",[65,171,172],{"class":71},",",[65,174,176],{"class":175},"sHwdD"," \u002F\u002F ← the tenant, injected\n",[65,178,180,183],{"class":67,"line":179},6,[65,181,182],{"class":71},"  )",[65,184,185],{"class":71}," {}\n",[65,187,189,192,195],{"class":67,"line":188},7,[65,190,191],{"class":86},"  all",[65,193,194],{"class":71},"()",[65,196,124],{"class":71},[65,198,200,203,206,209,212,215,217,220,222,225,228,230,233,236,239,242],{"class":67,"line":199},8,[65,201,202],{"class":112},"    return",[65,204,205],{"class":71}," this.",[65,207,208],{"class":79},"store",[65,210,211],{"class":71},".",[65,213,214],{"class":79},"rows",[65,216,211],{"class":71},[65,218,219],{"class":75},"get",[65,221,80],{"class":86},[65,223,224],{"class":71},"this.",[65,226,227],{"class":79},"ctx",[65,229,211],{"class":71},[65,231,232],{"class":79},"tenantId",[65,234,235],{"class":86},") ",[65,237,238],{"class":71},"??",[65,240,241],{"class":86}," [] ",[65,243,244],{"class":175},"\u002F\u002F scoped here, once\n",[65,246,248],{"class":67,"line":247},9,[65,249,250],{"class":71},"  }\n",[65,252,254],{"class":67,"line":253},10,[65,255,256],{"class":71},"}\n",[47,258,259,260,264,265,270],{},"Because the boundary is established at ",[261,262,263],"strong",{},"construction",", not re-checked at each call site, ",[261,266,267,268],{},"no\ncontroller, service, or repo method ever takes a ",[51,269,232],{},". It flows entirely through the\nscope. This is the DI analogue of Postgres row-level security.",[42,272,274],{"id":273},"the-endgame-real-postgres-rls","The endgame: real Postgres RLS",[47,276,277,278,288,289,292,293,299,300,303],{},"The ",[279,280,284,287],"a",{"href":281,"rel":282},"https:\u002F\u002Fgithub.com\u002Fnuxt-roost\u002Froost\u002Ftree\u002Fmain\u002Fexamples\u002Fpglite-rls",[283],"nofollow",[51,285,286],{},"pglite-rls"," example","\nproves the same pattern against ",[261,290,291],{},"real Postgres row-level security",", running on embedded pglite\n(no external database). The repo issues ",[261,294,295,296],{},"no ",[51,297,298],{},"WHERE tenant_id"," and INSERTs pass ",[261,301,302],{},"no tenant"," —\nyet each tenant sees only its own rows, enforced by the database:",[56,305,307],{"className":58,"code":306,"language":60,"meta":61,"style":61},"private async withTenant(fn) {\n  return this.db.transaction(async (tx) => {\n    await tx.execute(sql`select set_config('app.tenant_id', ${this.ctx.tenantId}, true)`)\n    await tx.execute(sql`set local role app_user`) \u002F\u002F RLS doesn't constrain superusers!\n    return fn(tx)\n  })\n}\n\nall() { return this.withTenant((tx) => tx.select().from(projects)) } \u002F\u002F no where(tenant)\n",[51,308,309,323,357,402,428,441,448,452,458],{"__ignoreMap":61},[65,310,311,314,317,320],{"class":67,"line":68},[65,312,313],{"class":79},"private async ",[65,315,316],{"class":75},"withTenant",[65,318,319],{"class":79},"(fn) ",[65,321,322],{"class":71},"{\n",[65,324,325,328,330,333,335,338,340,343,346,349,352,355],{"class":67,"line":109},[65,326,327],{"class":112},"  return",[65,329,205],{"class":71},[65,331,332],{"class":79},"db",[65,334,211],{"class":71},[65,336,337],{"class":75},"transaction",[65,339,80],{"class":86},[65,341,342],{"class":116},"async",[65,344,345],{"class":71}," (",[65,347,348],{"class":145},"tx",[65,350,351],{"class":71},")",[65,353,354],{"class":116}," =>",[65,356,124],{"class":71},[65,358,359,362,365,367,370,372,375,378,381,384,386,388,390,392,395,398,400],{"class":67,"line":127},[65,360,361],{"class":112},"    await",[65,363,364],{"class":79}," tx",[65,366,211],{"class":71},[65,368,369],{"class":75},"execute",[65,371,80],{"class":86},[65,373,374],{"class":75},"sql",[65,376,377],{"class":71},"`",[65,379,380],{"class":96},"select set_config('app.tenant_id', ",[65,382,383],{"class":71},"${",[65,385,224],{"class":71},[65,387,227],{"class":79},[65,389,211],{"class":71},[65,391,232],{"class":79},[65,393,394],{"class":71},"}",[65,396,397],{"class":96},", true)",[65,399,377],{"class":71},[65,401,106],{"class":86},[65,403,404,406,408,410,412,414,416,418,421,423,425],{"class":67,"line":136},[65,405,361],{"class":112},[65,407,364],{"class":79},[65,409,211],{"class":71},[65,411,369],{"class":75},[65,413,80],{"class":86},[65,415,374],{"class":75},[65,417,377],{"class":71},[65,419,420],{"class":96},"set local role app_user",[65,422,377],{"class":71},[65,424,235],{"class":86},[65,426,427],{"class":175},"\u002F\u002F RLS doesn't constrain superusers!\n",[65,429,430,432,435,437,439],{"class":67,"line":157},[65,431,202],{"class":112},[65,433,434],{"class":75}," fn",[65,436,80],{"class":86},[65,438,348],{"class":79},[65,440,106],{"class":86},[65,442,443,446],{"class":67,"line":179},[65,444,445],{"class":71},"  }",[65,447,106],{"class":86},[65,449,450],{"class":67,"line":188},[65,451,256],{"class":71},[65,453,454],{"class":67,"line":199},[65,455,457],{"emptyLinePlaceholder":456},true,"\n",[65,459,460,463,466,468,471,473,475,477,479,481,483,485,487,489,492,494,496,499,501,504,507,509],{"class":67,"line":247},[65,461,462],{"class":75},"all",[65,464,465],{"class":79},"() ",[65,467,83],{"class":71},[65,469,470],{"class":112}," return",[65,472,205],{"class":71},[65,474,316],{"class":75},[65,476,80],{"class":86},[65,478,80],{"class":71},[65,480,348],{"class":145},[65,482,351],{"class":71},[65,484,354],{"class":116},[65,486,364],{"class":79},[65,488,211],{"class":71},[65,490,491],{"class":75},"select",[65,493,194],{"class":86},[65,495,211],{"class":71},[65,497,498],{"class":75},"from",[65,500,80],{"class":86},[65,502,503],{"class":79},"projects",[65,505,506],{"class":86},")) ",[65,508,394],{"class":71},[65,510,511],{"class":175}," \u002F\u002F no where(tenant)\n",[513,514,516],"h3",{"id":515},"rls-gotchas-the-example-gets-right","RLS gotchas the example gets right",[518,519,520,536],"table",{},[521,522,523],"thead",{},[524,525,526,530,533],"tr",{},[527,528,529],"th",{},"Gotcha",[527,531,532],{},"Without the fix",[527,534,535],{},"Fix",[537,538,539,554,567,583],"tbody",{},[524,540,541,545,548],{},[542,543,544],"td",{},"Superusers bypass RLS",[542,546,547],{},"policies silently ignored → cross-tenant leak",[542,549,550,553],{},[51,551,552],{},"SET LOCAL ROLE app_user"," (non-superuser)",[524,555,556,559,562],{},[542,557,558],{},"Owner bypasses RLS",[542,560,561],{},"the table owner isn't constrained",[542,563,564],{},[51,565,566],{},"ALTER TABLE … FORCE ROW LEVEL SECURITY",[524,568,569,575,578],{},[542,570,571,574],{},[51,572,573],{},"current_setting"," throws when unset",[542,576,577],{},"a request with no tenant errors",[542,579,580],{},[51,581,582],{},"current_setting('app.tenant_id', true)",[524,584,585,591,594],{},[542,586,587,590],{},[51,588,589],{},"SET"," leaks across pooled requests",[542,592,593],{},"one request's tenant bleeds into another",[542,595,596,599,600,603],{},[51,597,598],{},"set_config(…, true)"," + ",[51,601,602],{},"SET LOCAL"," (txn-scoped)",[47,605,606],{},"Swap pglite for a Postgres pool and the pattern is identical.",[608,609,610],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}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);}",{"title":61,"searchDepth":109,"depth":109,"links":612},[613,614],{"id":44,"depth":109,"text":45},{"id":273,"depth":109,"text":274,"children":615},[616],{"id":515,"depth":127,"text":516},"Tenant isolation by construction — no tenantId threading — and the same pattern against real Postgres RLS.","md",null,{},{"title":17,"description":617},"svLDA8b076CK0dRGGaKjnXWq8KRw-sVP17a3KnWkwn8",[624,626],{"title":13,"path":14,"stem":15,"description":625,"children":-1},"Guards, interceptors, and filters — all DI'd providers, differing only in when they run.",{"title":21,"path":22,"stem":23,"description":627,"children":-1},"The build-time AST pass — what it reads, what it emits, and the dev-watch loop.",1780506501961]