[{"data":1,"prerenderedAt":1231},["ShallowReactive",2],{"navigation:en":3,"\u002Fblog\u002Fmulti-tenancy-rbac-saas-security":129},[4],{"title":5,"path":6,"stem":7,"children":8,"icon":10},"Nuxfire Docs","\u002Fdocs","docs\u002F0.index",[9,11],{"title":5,"path":6,"stem":7,"icon":10},"i-ph-squares-four-duotone",{"title":12,"path":13,"stem":14,"children":15,"icon":18},"Flow Videos","\u002Fdocs\u002Fflow-videos","docs\u002Fflow-videos\u002F0.index",[16,19,36,67,93],{"title":17,"path":13,"stem":14,"icon":18},"Serelius Flow Video","i-ph-video-camera-duotone",{"title":20,"icon":21,"path":22,"stem":23,"children":24,"page":35},"Getting Started","i-ph-rocket-launch-duotone","\u002Fdocs\u002Fflow-videos\u002Fgetting-started","docs\u002Fflow-videos\u002F1.getting-started",[25,30],{"title":26,"path":27,"stem":28,"icon":29},"Overview","\u002Fdocs\u002Fflow-videos\u002Fgetting-started\u002Foverview","docs\u002Fflow-videos\u002F1.getting-started\u002F0.overview","i-ph-play-circle-duotone",{"title":31,"path":32,"stem":33,"icon":34},"Global Features","\u002Fdocs\u002Fflow-videos\u002Fgetting-started\u002Fglobal-features","docs\u002Fflow-videos\u002F1.getting-started\u002F1.global-features","i-ph-gear-six-duotone",false,{"title":37,"icon":38,"path":39,"stem":40,"children":41,"page":35},"Pipeline","i-ph-film-strip-duotone","\u002Fdocs\u002Fflow-videos\u002Fpipeline","docs\u002Fflow-videos\u002F2.pipeline",[42,47,52,57,62],{"title":43,"path":44,"stem":45,"icon":46},"Phase 1 — Script","\u002Fdocs\u002Fflow-videos\u002Fpipeline\u002Fscript","docs\u002Fflow-videos\u002F2.pipeline\u002F1.script","i-ph-scroll-duotone",{"title":48,"path":49,"stem":50,"icon":51},"Phase 2 — Style & Identity","\u002Fdocs\u002Fflow-videos\u002Fpipeline\u002Fstyle-and-identity","docs\u002Fflow-videos\u002F2.pipeline\u002F2.style-and-identity","i-ph-palette-duotone",{"title":53,"path":54,"stem":55,"icon":56},"Phase 3 — Storyboard","\u002Fdocs\u002Fflow-videos\u002Fpipeline\u002Fstoryboard","docs\u002Fflow-videos\u002F2.pipeline\u002F3.storyboard","i-ph-image-duotone",{"title":58,"path":59,"stem":60,"icon":61},"Phase 4 — Videos","\u002Fdocs\u002Fflow-videos\u002Fpipeline\u002Fvideos","docs\u002Fflow-videos\u002F2.pipeline\u002F4.videos","i-ph-video-duotone",{"title":63,"path":64,"stem":65,"icon":66},"Phase 5 — Final Assembly","\u002Fdocs\u002Fflow-videos\u002Fpipeline\u002Ffinal-assembly","docs\u002Fflow-videos\u002F2.pipeline\u002F5.final-assembly","i-ph-scissors-duotone",{"title":68,"icon":69,"path":70,"stem":71,"children":72,"page":35},"Settings & Models","i-ph-sliders-duotone","\u002Fdocs\u002Fflow-videos\u002Fsettings-and-models","docs\u002Fflow-videos\u002F3.settings-and-models",[73,78,83,88],{"title":74,"path":75,"stem":76,"icon":77},"Settings","\u002Fdocs\u002Fflow-videos\u002Fsettings-and-models\u002Fsettings","docs\u002Fflow-videos\u002F3.settings-and-models\u002F0.settings","i-ph-sliders-horizontal-duotone",{"title":79,"path":80,"stem":81,"icon":82},"AI Models & Limits","\u002Fdocs\u002Fflow-videos\u002Fsettings-and-models\u002Fmodels-and-limits","docs\u002Fflow-videos\u002F3.settings-and-models\u002F1.models-and-limits","i-ph-cpu-duotone",{"title":84,"path":85,"stem":86,"icon":87},"Option Catalogs","\u002Fdocs\u002Fflow-videos\u002Fsettings-and-models\u002Fcatalogs","docs\u002Fflow-videos\u002F3.settings-and-models\u002F2.catalogs","i-ph-list-bullets-duotone",{"title":89,"path":90,"stem":91,"icon":92},"Pipeline Calibration","\u002Fdocs\u002Fflow-videos\u002Fsettings-and-models\u002Fcalibration","docs\u002Fflow-videos\u002F3.settings-and-models\u002F3.calibration","i-ph-gauge-duotone",{"title":94,"icon":95,"path":96,"stem":97,"children":98,"page":35},"Reference","i-ph-book-open-duotone","\u002Fdocs\u002Fflow-videos\u002Freference","docs\u002Fflow-videos\u002F4.reference",[99,104,109,114,119,124],{"title":100,"path":101,"stem":102,"icon":103},"Delivered Files","\u002Fdocs\u002Fflow-videos\u002Freference\u002Fdelivered-files","docs\u002Fflow-videos\u002F4.reference\u002F0.delivered-files","i-ph-folder-duotone",{"title":105,"path":106,"stem":107,"icon":108},"Supported Video Types","\u002Fdocs\u002Fflow-videos\u002Freference\u002Fvideo-types","docs\u002Fflow-videos\u002F4.reference\u002F1.video-types","i-ph-television-duotone",{"title":110,"path":111,"stem":112,"icon":113},"What the Tool Does NOT Do","\u002Fdocs\u002Fflow-videos\u002Freference\u002Flimitations","docs\u002Fflow-videos\u002F4.reference\u002F2.limitations","i-ph-prohibit-duotone",{"title":115,"path":116,"stem":117,"icon":118},"Business Models","\u002Fdocs\u002Fflow-videos\u002Freference\u002Fbusiness-models","docs\u002Fflow-videos\u002F4.reference\u002F3.business-models","i-ph-briefcase-duotone",{"title":120,"path":121,"stem":122,"icon":123},"FAQ & Glossary","\u002Fdocs\u002Fflow-videos\u002Freference\u002Ffaq-and-glossary","docs\u002Fflow-videos\u002F4.reference\u002F4.faq-and-glossary","i-ph-question-duotone",{"title":125,"path":126,"stem":127,"icon":128},"Technical and Architectural Audit","\u002Fdocs\u002Fflow-videos\u002Freference\u002Faudit","docs\u002Fflow-videos\u002F4.reference\u002F5.audit","i-ph-shield-check-duotone",{"id":130,"title":131,"author":132,"authorType":133,"body":134,"category":1210,"date":1211,"description":1212,"extension":1213,"image":1214,"lang":1215,"meta":1216,"navigation":449,"ogImage":1217,"path":1218,"readTime":1219,"seo":1220,"stem":1221,"tags":1222,"translations":1226,"updated":133,"__hash__":1230},"blog\u002Fblog\u002Fmulti-tenancy-rbac-saas-security.md","Enterprise Multi-Tenancy and Granular RBAC on PostgreSQL with Drizzle ORM","Nuxfire Team",null,{"type":135,"value":136,"toc":1204},"minimark",[137,150,153,171,174,179,182,202,205,729,731,735,741,1138,1140,1144,1147,1171,1173,1177,1188,1191,1200],[138,139,140,141,145,146,149],"p",{},"The biggest dividing line between an amateur project and enterprise software ready for B2B sales is the security of ",[142,143,144],"strong",{},"multi-tenancy"," and ",[142,147,148],{},"RBAC (Role-Based Access Control)",".",[138,151,152],{},"If an enterprise customer discovers that a user from another company was able to view data from their workspace, your startup's reputation is destroyed on day one.",[138,154,155,156,160,161,164,165,145,168,149],{},"In this article, we show how the ",[157,158,159],"code",{},"layers\u002Fteams"," layer of ",[142,162,163],{},"Nuxfire"," implements failure-proof data isolation with ",[142,166,167],{},"PostgreSQL",[142,169,170],{},"Drizzle ORM",[172,173],"hr",{},[175,176,178],"h2",{"id":177},"data-modeling-the-tenant-id-on-every-table","Data Modeling: The Tenant Id on Every Table",[138,180,181],{},"There are three classic forms of multi-tenancy:",[183,184,185,189,192],"ol",{},[186,187,188],"li",{},"A separate database per customer (expensive and hard to keep migrations in sync).",[186,190,191],{},"A separate schema per customer (connection limits and complexity in the ORM).",[186,193,194,201],{},[142,195,196,197,200],{},"Shared tables with a discriminator column (",[157,198,199],{},"team_id",")"," — The modern industry standard adopted by giants like Stripe, Slack and Linear.",[138,203,204],{},"In Nuxfire, every entity belongs to a team:",[206,207,212],"pre",{"className":208,"code":209,"language":210,"meta":211,"style":211},"language-typescript shiki shiki-themes material-theme-lighter github-light github-dark monokai","\u002F\u002F layers\u002Fteams\u002Fserver\u002Fdatabase\u002Fschema.ts\nexport const teams = pgTable(\"teams\", {\n  id: text(\"id\").primaryKey(),\n  name: text(\"name\").notNull(),\n  slug: text(\"slug\").notNull().unique(),\n  ownerId: text(\"owner_id\").notNull(),\n  createdAt: timestamp(\"created_at\").defaultNow().notNull()\n});\n\nexport const teamMembers = pgTable(\"team_members\", {\n  id: text(\"id\").primaryKey(),\n  teamId: text(\"team_id\").notNull().references(() => teams.id, { onDelete: \"cascade\" }),\n  userId: text(\"user_id\").notNull(),\n  role: text(\"role\", { enum: [\"OWNER\", \"ADMIN\", \"MEMBER\"] }).notNull().default(\"MEMBER\"),\n  createdAt: timestamp(\"created_at\").defaultNow().notNull()\n});\n","typescript","",[157,213,214,223,267,302,332,368,397,433,444,451,478,505,574,603,689,720],{"__ignoreMap":211},[215,216,219],"span",{"class":217,"line":218},"line",1,[215,220,222],{"class":221},"ss7Ak","\u002F\u002F layers\u002Fteams\u002Fserver\u002Fdatabase\u002Fschema.ts\n",[215,224,226,230,234,238,242,246,250,254,258,260,264],{"class":217,"line":225},2,[215,227,229],{"class":228},"sRxSC","export",[215,231,233],{"class":232},"srJo8"," const",[215,235,237],{"class":236},"s91G_"," teams",[215,239,241],{"class":240},"sGXK2"," =",[215,243,245],{"class":244},"sD0ED"," pgTable",[215,247,249],{"class":248},"ss--_","(",[215,251,253],{"class":252},"siCPE","\"",[215,255,257],{"class":256},"sLACW","teams",[215,259,253],{"class":252},[215,261,263],{"class":262},"swvn1",",",[215,265,266],{"class":262}," {\n",[215,268,270,274,277,280,282,284,287,289,291,293,296,299],{"class":217,"line":269},3,[215,271,273],{"class":272},"squCx","  id",[215,275,276],{"class":262},":",[215,278,279],{"class":244}," text",[215,281,249],{"class":248},[215,283,253],{"class":252},[215,285,286],{"class":256},"id",[215,288,253],{"class":252},[215,290,200],{"class":248},[215,292,149],{"class":262},[215,294,295],{"class":244},"primaryKey",[215,297,298],{"class":248},"()",[215,300,301],{"class":262},",\n",[215,303,305,308,310,312,314,316,319,321,323,325,328,330],{"class":217,"line":304},4,[215,306,307],{"class":272},"  name",[215,309,276],{"class":262},[215,311,279],{"class":244},[215,313,249],{"class":248},[215,315,253],{"class":252},[215,317,318],{"class":256},"name",[215,320,253],{"class":252},[215,322,200],{"class":248},[215,324,149],{"class":262},[215,326,327],{"class":244},"notNull",[215,329,298],{"class":248},[215,331,301],{"class":262},[215,333,335,338,340,342,344,346,349,351,353,355,357,359,361,364,366],{"class":217,"line":334},5,[215,336,337],{"class":272},"  slug",[215,339,276],{"class":262},[215,341,279],{"class":244},[215,343,249],{"class":248},[215,345,253],{"class":252},[215,347,348],{"class":256},"slug",[215,350,253],{"class":252},[215,352,200],{"class":248},[215,354,149],{"class":262},[215,356,327],{"class":244},[215,358,298],{"class":248},[215,360,149],{"class":262},[215,362,363],{"class":244},"unique",[215,365,298],{"class":248},[215,367,301],{"class":262},[215,369,371,374,376,378,380,382,385,387,389,391,393,395],{"class":217,"line":370},6,[215,372,373],{"class":272},"  ownerId",[215,375,276],{"class":262},[215,377,279],{"class":244},[215,379,249],{"class":248},[215,381,253],{"class":252},[215,383,384],{"class":256},"owner_id",[215,386,253],{"class":252},[215,388,200],{"class":248},[215,390,149],{"class":262},[215,392,327],{"class":244},[215,394,298],{"class":248},[215,396,301],{"class":262},[215,398,400,403,405,408,410,412,415,417,419,421,424,426,428,430],{"class":217,"line":399},7,[215,401,402],{"class":272},"  createdAt",[215,404,276],{"class":262},[215,406,407],{"class":244}," timestamp",[215,409,249],{"class":248},[215,411,253],{"class":252},[215,413,414],{"class":256},"created_at",[215,416,253],{"class":252},[215,418,200],{"class":248},[215,420,149],{"class":262},[215,422,423],{"class":244},"defaultNow",[215,425,298],{"class":248},[215,427,149],{"class":262},[215,429,327],{"class":244},[215,431,432],{"class":248},"()\n",[215,434,436,439,441],{"class":217,"line":435},8,[215,437,438],{"class":262},"}",[215,440,200],{"class":248},[215,442,443],{"class":262},";\n",[215,445,447],{"class":217,"line":446},9,[215,448,450],{"emptyLinePlaceholder":449},true,"\n",[215,452,454,456,458,461,463,465,467,469,472,474,476],{"class":217,"line":453},10,[215,455,229],{"class":228},[215,457,233],{"class":232},[215,459,460],{"class":236}," teamMembers",[215,462,241],{"class":240},[215,464,245],{"class":244},[215,466,249],{"class":248},[215,468,253],{"class":252},[215,470,471],{"class":256},"team_members",[215,473,253],{"class":252},[215,475,263],{"class":262},[215,477,266],{"class":262},[215,479,481,483,485,487,489,491,493,495,497,499,501,503],{"class":217,"line":480},11,[215,482,273],{"class":272},[215,484,276],{"class":262},[215,486,279],{"class":244},[215,488,249],{"class":248},[215,490,253],{"class":252},[215,492,286],{"class":256},[215,494,253],{"class":252},[215,496,200],{"class":248},[215,498,149],{"class":262},[215,500,295],{"class":244},[215,502,298],{"class":248},[215,504,301],{"class":262},[215,506,508,511,513,515,517,519,521,523,525,527,529,531,533,536,538,540,543,545,547,549,551,554,557,559,562,565,567,570,572],{"class":217,"line":507},12,[215,509,510],{"class":272},"  teamId",[215,512,276],{"class":262},[215,514,279],{"class":244},[215,516,249],{"class":248},[215,518,253],{"class":252},[215,520,199],{"class":256},[215,522,253],{"class":252},[215,524,200],{"class":248},[215,526,149],{"class":262},[215,528,327],{"class":244},[215,530,298],{"class":248},[215,532,149],{"class":262},[215,534,535],{"class":244},"references",[215,537,249],{"class":248},[215,539,298],{"class":262},[215,541,542],{"class":232}," =>",[215,544,237],{"class":248},[215,546,149],{"class":262},[215,548,286],{"class":248},[215,550,263],{"class":262},[215,552,553],{"class":262}," {",[215,555,556],{"class":272}," onDelete",[215,558,276],{"class":262},[215,560,561],{"class":252}," \"",[215,563,564],{"class":256},"cascade",[215,566,253],{"class":252},[215,568,569],{"class":262}," }",[215,571,200],{"class":248},[215,573,301],{"class":262},[215,575,577,580,582,584,586,588,591,593,595,597,599,601],{"class":217,"line":576},13,[215,578,579],{"class":272},"  userId",[215,581,276],{"class":262},[215,583,279],{"class":244},[215,585,249],{"class":248},[215,587,253],{"class":252},[215,589,590],{"class":256},"user_id",[215,592,253],{"class":252},[215,594,200],{"class":248},[215,596,149],{"class":262},[215,598,327],{"class":244},[215,600,298],{"class":248},[215,602,301],{"class":262},[215,604,606,609,611,613,615,617,620,622,624,626,629,631,634,636,639,641,643,645,648,650,652,654,657,659,662,664,666,668,670,672,674,677,679,681,683,685,687],{"class":217,"line":605},14,[215,607,608],{"class":272},"  role",[215,610,276],{"class":262},[215,612,279],{"class":244},[215,614,249],{"class":248},[215,616,253],{"class":252},[215,618,619],{"class":256},"role",[215,621,253],{"class":252},[215,623,263],{"class":262},[215,625,553],{"class":262},[215,627,628],{"class":272}," enum",[215,630,276],{"class":262},[215,632,633],{"class":248}," [",[215,635,253],{"class":252},[215,637,638],{"class":256},"OWNER",[215,640,253],{"class":252},[215,642,263],{"class":262},[215,644,561],{"class":252},[215,646,647],{"class":256},"ADMIN",[215,649,253],{"class":252},[215,651,263],{"class":262},[215,653,561],{"class":252},[215,655,656],{"class":256},"MEMBER",[215,658,253],{"class":252},[215,660,661],{"class":248},"] ",[215,663,438],{"class":262},[215,665,200],{"class":248},[215,667,149],{"class":262},[215,669,327],{"class":244},[215,671,298],{"class":248},[215,673,149],{"class":262},[215,675,676],{"class":244},"default",[215,678,249],{"class":248},[215,680,253],{"class":252},[215,682,656],{"class":256},[215,684,253],{"class":252},[215,686,200],{"class":248},[215,688,301],{"class":262},[215,690,692,694,696,698,700,702,704,706,708,710,712,714,716,718],{"class":217,"line":691},15,[215,693,402],{"class":272},[215,695,276],{"class":262},[215,697,407],{"class":244},[215,699,249],{"class":248},[215,701,253],{"class":252},[215,703,414],{"class":256},[215,705,253],{"class":252},[215,707,200],{"class":248},[215,709,149],{"class":262},[215,711,423],{"class":244},[215,713,298],{"class":248},[215,715,149],{"class":262},[215,717,327],{"class":244},[215,719,432],{"class":248},[215,721,723,725,727],{"class":217,"line":722},16,[215,724,438],{"class":262},[215,726,200],{"class":248},[215,728,443],{"class":262},[172,730],{},[175,732,734],{"id":733},"middleware-and-authenticated-trpc-procedures","Middleware and Authenticated tRPC Procedures",[138,736,737,738,740],{},"To make sure no developer forgets to filter by ",[157,739,199],{},", requests go through protected tRPC procedures:",[206,742,744],{"className":208,"code":743,"language":210,"meta":211,"style":211},"export const teamProcedure = protectedProcedure\n  .input(z.object({ teamId: z.string() }))\n  .use(async ({ ctx, input, next }) => {\n    const membership = await ctx.db.query.teamMembers.findFirst({\n      where: and(\n        eq(teamMembers.teamId, input.teamId),\n        eq(teamMembers.userId, ctx.user.id)\n      )\n    });\n\n    if (!membership) {\n      throw new TRPCError({\n        code: \"FORBIDDEN\",\n        message: \"You do not have access to this workspace.\"\n      });\n    }\n\n    return next({\n      ctx: {\n        ...ctx,\n        teamId: input.teamId,\n        role: membership.role\n      }\n    });\n  });\n",[157,745,746,760,802,839,879,892,918,947,952,961,965,984,999,1015,1030,1039,1044,1049,1061,1071,1082,1098,1113,1119,1128],{"__ignoreMap":211},[215,747,748,750,752,755,757],{"class":217,"line":218},[215,749,229],{"class":228},[215,751,233],{"class":232},[215,753,754],{"class":236}," teamProcedure",[215,756,241],{"class":240},[215,758,759],{"class":248}," protectedProcedure\n",[215,761,762,765,768,771,773,776,778,781,784,786,789,791,794,797,799],{"class":217,"line":225},[215,763,764],{"class":262},"  .",[215,766,767],{"class":244},"input",[215,769,770],{"class":248},"(z",[215,772,149],{"class":262},[215,774,775],{"class":244},"object",[215,777,249],{"class":248},[215,779,780],{"class":262},"{",[215,782,783],{"class":272}," teamId",[215,785,276],{"class":262},[215,787,788],{"class":248}," z",[215,790,149],{"class":262},[215,792,793],{"class":244},"string",[215,795,796],{"class":248},"() ",[215,798,438],{"class":262},[215,800,801],{"class":248},"))\n",[215,803,804,806,809,811,815,818,822,824,827,829,832,835,837],{"class":217,"line":269},[215,805,764],{"class":262},[215,807,808],{"class":244},"use",[215,810,249],{"class":248},[215,812,814],{"class":813},"sTNss","async",[215,816,817],{"class":262}," ({",[215,819,821],{"class":820},"sQgqH"," ctx",[215,823,263],{"class":262},[215,825,826],{"class":820}," input",[215,828,263],{"class":262},[215,830,831],{"class":820}," next",[215,833,834],{"class":262}," })",[215,836,542],{"class":232},[215,838,266],{"class":262},[215,840,841,844,847,849,852,854,856,859,861,864,866,869,871,874,876],{"class":217,"line":304},[215,842,843],{"class":232},"    const",[215,845,846],{"class":236}," membership",[215,848,241],{"class":240},[215,850,851],{"class":228}," await",[215,853,821],{"class":248},[215,855,149],{"class":262},[215,857,858],{"class":248},"db",[215,860,149],{"class":262},[215,862,863],{"class":248},"query",[215,865,149],{"class":262},[215,867,868],{"class":248},"teamMembers",[215,870,149],{"class":262},[215,872,873],{"class":244},"findFirst",[215,875,249],{"class":272},[215,877,878],{"class":262},"{\n",[215,880,881,884,886,889],{"class":217,"line":334},[215,882,883],{"class":272},"      where",[215,885,276],{"class":262},[215,887,888],{"class":244}," and",[215,890,891],{"class":272},"(\n",[215,893,894,897,899,901,903,906,908,910,912,914,916],{"class":217,"line":370},[215,895,896],{"class":244},"        eq",[215,898,249],{"class":272},[215,900,868],{"class":248},[215,902,149],{"class":262},[215,904,905],{"class":248},"teamId",[215,907,263],{"class":262},[215,909,826],{"class":248},[215,911,149],{"class":262},[215,913,905],{"class":248},[215,915,200],{"class":272},[215,917,301],{"class":262},[215,919,920,922,924,926,928,931,933,935,937,940,942,944],{"class":217,"line":399},[215,921,896],{"class":244},[215,923,249],{"class":272},[215,925,868],{"class":248},[215,927,149],{"class":262},[215,929,930],{"class":248},"userId",[215,932,263],{"class":262},[215,934,821],{"class":248},[215,936,149],{"class":262},[215,938,939],{"class":248},"user",[215,941,149],{"class":262},[215,943,286],{"class":248},[215,945,946],{"class":272},")\n",[215,948,949],{"class":217,"line":435},[215,950,951],{"class":272},"      )\n",[215,953,954,957,959],{"class":217,"line":446},[215,955,956],{"class":262},"    }",[215,958,200],{"class":272},[215,960,443],{"class":262},[215,962,963],{"class":217,"line":453},[215,964,450],{"emptyLinePlaceholder":449},[215,966,967,970,973,976,979,982],{"class":217,"line":480},[215,968,969],{"class":228},"    if",[215,971,972],{"class":272}," (",[215,974,975],{"class":240},"!",[215,977,978],{"class":248},"membership",[215,980,981],{"class":272},") ",[215,983,878],{"class":262},[215,985,986,989,992,995,997],{"class":217,"line":507},[215,987,988],{"class":228},"      throw",[215,990,991],{"class":240}," new",[215,993,994],{"class":244}," TRPCError",[215,996,249],{"class":272},[215,998,878],{"class":262},[215,1000,1001,1004,1006,1008,1011,1013],{"class":217,"line":576},[215,1002,1003],{"class":272},"        code",[215,1005,276],{"class":262},[215,1007,561],{"class":252},[215,1009,1010],{"class":256},"FORBIDDEN",[215,1012,253],{"class":252},[215,1014,301],{"class":262},[215,1016,1017,1020,1022,1024,1027],{"class":217,"line":605},[215,1018,1019],{"class":272},"        message",[215,1021,276],{"class":262},[215,1023,561],{"class":252},[215,1025,1026],{"class":256},"You do not have access to this workspace.",[215,1028,1029],{"class":252},"\"\n",[215,1031,1032,1035,1037],{"class":217,"line":691},[215,1033,1034],{"class":262},"      }",[215,1036,200],{"class":272},[215,1038,443],{"class":262},[215,1040,1041],{"class":217,"line":722},[215,1042,1043],{"class":262},"    }\n",[215,1045,1047],{"class":217,"line":1046},17,[215,1048,450],{"emptyLinePlaceholder":449},[215,1050,1052,1055,1057,1059],{"class":217,"line":1051},18,[215,1053,1054],{"class":228},"    return",[215,1056,831],{"class":244},[215,1058,249],{"class":272},[215,1060,878],{"class":262},[215,1062,1064,1067,1069],{"class":217,"line":1063},19,[215,1065,1066],{"class":272},"      ctx",[215,1068,276],{"class":262},[215,1070,266],{"class":262},[215,1072,1074,1077,1080],{"class":217,"line":1073},20,[215,1075,1076],{"class":240},"        ...",[215,1078,1079],{"class":248},"ctx",[215,1081,301],{"class":262},[215,1083,1085,1088,1090,1092,1094,1096],{"class":217,"line":1084},21,[215,1086,1087],{"class":272},"        teamId",[215,1089,276],{"class":262},[215,1091,826],{"class":248},[215,1093,149],{"class":262},[215,1095,905],{"class":248},[215,1097,301],{"class":262},[215,1099,1101,1104,1106,1108,1110],{"class":217,"line":1100},22,[215,1102,1103],{"class":272},"        role",[215,1105,276],{"class":262},[215,1107,846],{"class":248},[215,1109,149],{"class":262},[215,1111,1112],{"class":248},"role\n",[215,1114,1116],{"class":217,"line":1115},23,[215,1117,1118],{"class":262},"      }\n",[215,1120,1122,1124,1126],{"class":217,"line":1121},24,[215,1123,956],{"class":262},[215,1125,200],{"class":272},[215,1127,443],{"class":262},[215,1129,1131,1134,1136],{"class":217,"line":1130},25,[215,1132,1133],{"class":262},"  }",[215,1135,200],{"class":248},[215,1137,443],{"class":262},[172,1139],{},[175,1141,1143],{"id":1142},"email-invitation-flow-with-an-encrypted-token","Email Invitation Flow with an Encrypted Token",[138,1145,1146],{},"Invitations for new members follow a secure double-confirmation flow:",[183,1148,1149,1158,1161,1168],{},[186,1150,1151,1152,1154,1155,1157],{},"The team administrator enters the collaborator's email and selects the role (",[157,1153,647],{}," or ",[157,1156,656],{},").",[186,1159,1160],{},"The system generates a cryptographically secure random token that expires in 7 days.",[186,1162,1163,1164,1167],{},"A polished email built with ",[142,1165,1166],{},"React Email"," is sent through an asynchronous queue.",[186,1169,1170],{},"When they click the link, the user is taken to the accept or onboarding screen, linking the account to the team instantly.",[172,1172],{},[175,1174,1176],{"id":1175},"audit-logs-for-enterprise-compliance","Audit Logs for Enterprise Compliance",[138,1178,1179,1180,1183,1184,1187],{},"For demanding B2B customers, Nuxfire already includes a dedicated ",[142,1181,1182],{},"Audit Logs"," table (",[157,1185,1186],{},"\u002Fadmin\u002Faudit-log","). All critical changes — permission changes, member removals, plan changes and data downloads — are recorded immutably with a timestamp, IP and the author's identifier.",[138,1189,1190],{},"Build your SaaS on a foundation that meets the highest enterprise security standards from the very first commit.",[138,1192,1193],{},[1194,1195,1197],"a",{"href":1196},"\u002F#pricing",[142,1198,1199],{},"Get your Nuxfire license and secure immediate access to the code and the VIP Group →",[1201,1202,1203],"style",{},"html pre.shiki code .ss7Ak, html code.shiki .ss7Ak{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit;--shiki-sepia:#88846F;--shiki-sepia-font-style:inherit}html pre.shiki code .sRxSC, html code.shiki .sRxSC{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit;--shiki-sepia:#F92672;--shiki-sepia-font-style:inherit}html pre.shiki code .srJo8, html code.shiki .srJo8{--shiki-light:#9C3EDA;--shiki-light-font-style:inherit;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic}html pre.shiki code .s91G_, html code.shiki .s91G_{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#F8F8F2}html pre.shiki code .sGXK2, html code.shiki .sGXK2{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .sD0ED, html code.shiki .sD0ED{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0;--shiki-sepia:#A6E22E}html pre.shiki code .ss--_, html code.shiki .ss--_{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .siCPE, html code.shiki .siCPE{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html pre.shiki code .sLACW, html code.shiki .sLACW{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html pre.shiki code .swvn1, html code.shiki .swvn1{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .squCx, html code.shiki .squCx{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html pre.shiki code .sTNss, html code.shiki .sTNss{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .sQgqH, html code.shiki .sQgqH{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit;--shiki-sepia:#FD971F;--shiki-sepia-font-style:italic}",{"title":211,"searchDepth":225,"depth":225,"links":1205},[1206,1207,1208,1209],{"id":177,"depth":225,"text":178},{"id":733,"depth":225,"text":734},{"id":1142,"depth":225,"text":1143},{"id":1175,"depth":225,"text":1176},"Security & Databases","2026-09-15","How to implement strict isolation between teams, granular permissions and secure token-based invitations with no security gaps in your B2B SaaS.","md","\u002Fimages\u002Fblog\u002Fmulti-tenancy-rbac-postgresql-en.svg","en",{},"\u002Fimages\u002Fblog\u002Fmulti-tenancy-rbac-postgresql-en.png","\u002Fblog\u002Fmulti-tenancy-rbac-saas-security","9 min",{"title":131,"description":1212},"blog\u002Fmulti-tenancy-rbac-saas-security",[1223,1224,167,170,1225],"Multi-Tenancy","RBAC","Security",{"en":1227,"pt-br":1228,"es":1229},"multi-tenancy-rbac-saas-security","multi-tenancy-rbac-seguranca-saas","multi-tenancy-rbac-seguridad-saas","aA8BAEYe0nYoqTBeIYa_YEVt1er_cAxivk9vIwco58w",1794055339929]