[{"data":1,"prerenderedAt":1412},["ShallowReactive",2],{"navigation:en":3,"\u002Fblog\u002Fmulti-tenancy-rbac-saas-security":311},[4],{"title":5,"path":6,"stem":7,"children":8,"icon":10},"Nuxfire Docs","\u002Fdocs","docs\u002F0.index",[9,11,152],{"title":5,"path":6,"stem":7,"icon":10},"i-lucide-layout-grid",{"title":12,"path":13,"stem":14,"children":15,"icon":151},"SaaS Starter Kit","\u002Fdocs\u002Fsaas-starter-kit","docs\u002F1.saas-starter-kit\u002F0.index",[16,19,63,129],{"title":17,"path":13,"stem":14,"icon":18},"What is the SaaS Starter Kit?","i-lucide-info",{"title":20,"icon":21,"hide":22,"path":23,"stem":24,"children":25,"page":62},"Getting Started","i-lucide-play",true,"\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started","docs\u002F1.saas-starter-kit\u002F1.getting-started",[26,30,34,38,42,46,50,54,58],{"title":27,"path":28,"stem":29},"Quick Start","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fquick-start","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F0.quick-start",{"title":31,"path":32,"stem":33},"Installation","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Finstallation","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F1.installation",{"title":35,"path":36,"stem":37},"Cloudflare Access","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fcloudflare-access","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F2.cloudflare-access",{"title":39,"path":40,"stem":41},"Configuration","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fconfiguration","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F3.configuration",{"title":43,"path":44,"stem":45},"Secrets","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fsecrets","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F4.secrets",{"title":47,"path":48,"stem":49},"Environment Variables","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fenvironment-variables","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F5.environment-variables",{"title":51,"path":52,"stem":53},"Deploy","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fdeploy","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F6.deploy",{"title":55,"path":56,"stem":57},"Local Development","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Flocal-development","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F7.local-development",{"title":59,"path":60,"stem":61},"Project Structure","\u002Fdocs\u002Fsaas-starter-kit\u002Fgetting-started\u002Fproject-structure","docs\u002F1.saas-starter-kit\u002F1.getting-started\u002F8.project-structure",false,{"title":64,"icon":65,"hide":22,"path":66,"stem":67,"children":68,"page":62},"Features","i-lucide-box","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures","docs\u002F1.saas-starter-kit\u002F2.features",[69,73,77,81,85,89,93,97,101,105,109,113,117,121,125],{"title":70,"path":71,"stem":72},"Authentication","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fauth","docs\u002F1.saas-starter-kit\u002F2.features\u002F1.auth",{"title":74,"path":75,"stem":76},"Rate Limiting","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Frate-limiting","docs\u002F1.saas-starter-kit\u002F2.features\u002F10.rate-limiting",{"title":78,"path":79,"stem":80},"WebSockets","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fwebsockets","docs\u002F1.saas-starter-kit\u002F2.features\u002F11.websockets",{"title":82,"path":83,"stem":84},"Blog","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fblog","docs\u002F1.saas-starter-kit\u002F2.features\u002F14.blog",{"title":86,"path":87,"stem":88},"Documentation","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fdocumentation","docs\u002F1.saas-starter-kit\u002F2.features\u002F15.documentation",{"title":90,"path":91,"stem":92},"Analytics","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fanalytics","docs\u002F1.saas-starter-kit\u002F2.features\u002F16.analytics",{"title":94,"path":95,"stem":96},"Platform Admin Console","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fplatform-admin","docs\u002F1.saas-starter-kit\u002F2.features\u002F17.platform-admin",{"title":98,"path":99,"stem":100},"Database","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fdatabase","docs\u002F1.saas-starter-kit\u002F2.features\u002F2.database",{"title":102,"path":103,"stem":104},"Payments & Billing","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fpayments","docs\u002F1.saas-starter-kit\u002F2.features\u002F3.payments",{"title":106,"path":107,"stem":108},"Emails","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Femails","docs\u002F1.saas-starter-kit\u002F2.features\u002F4.emails",{"title":110,"path":111,"stem":112},"Notifications","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fnotifications","docs\u002F1.saas-starter-kit\u002F2.features\u002F5.notifications",{"title":114,"path":115,"stem":116},"Role-Based Access Control","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Frbac","docs\u002F1.saas-starter-kit\u002F2.features\u002F6.rbac",{"title":118,"path":119,"stem":120},"Async Jobs","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fasync-jobs","docs\u002F1.saas-starter-kit\u002F2.features\u002F7.async-jobs",{"title":122,"path":123,"stem":124},"Storage","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Fstorage","docs\u002F1.saas-starter-kit\u002F2.features\u002F8.storage",{"title":126,"path":127,"stem":128},"Internationalization","\u002Fdocs\u002Fsaas-starter-kit\u002Ffeatures\u002Finternationalization","docs\u002F1.saas-starter-kit\u002F2.features\u002F9.internationalization",{"title":130,"icon":131,"path":132,"stem":133,"children":134,"page":62},"Concepts","i-lucide-graduation-cap","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts","docs\u002F1.saas-starter-kit\u002F3.concepts",[135,139,143,147],{"title":136,"path":137,"stem":138},"Technology Stack","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts\u002Ftech-stack","docs\u002F1.saas-starter-kit\u002F3.concepts\u002F0.tech-stack",{"title":140,"path":141,"stem":142},"SST.dev","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts\u002Fsst","docs\u002F1.saas-starter-kit\u002F3.concepts\u002F1.sst",{"title":144,"path":145,"stem":146},"NuxtHub","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts\u002Fnuxt-hub","docs\u002F1.saas-starter-kit\u002F3.concepts\u002F2.nuxt-hub",{"title":148,"path":149,"stem":150},"Design System & Branding","\u002Fdocs\u002Fsaas-starter-kit\u002Fconcepts\u002Fdesign-system","docs\u002F1.saas-starter-kit\u002F3.concepts\u002F3.design-system","i-lucide-zap",{"title":153,"path":154,"stem":155,"children":156,"icon":159},"Fireskills","\u002Fdocs\u002Ffireskills","docs\u002F2.fireskills\u002F0.index",[157,160,219,249,275,293],{"title":158,"path":154,"stem":155,"icon":159},"What is Fireskills?","i-lucide-flame",{"title":20,"icon":21,"hide":22,"path":161,"stem":162,"children":163,"page":62},"\u002Fdocs\u002Ffireskills\u002Fgetting-started","docs\u002F2.fireskills\u002F1.getting-started",[164,167,171,175,179,183,187,191,195,199,203,207,211,215],{"title":27,"path":165,"stem":166},"\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fquick-start","docs\u002F2.fireskills\u002F1.getting-started\u002F00.quick-start",{"title":168,"path":169,"stem":170},"Workflow & Governance","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fworkflow","docs\u002F2.fireskills\u002F1.getting-started\u002F01.workflow",{"title":172,"path":173,"stem":174},"Project Context","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fproject-context","docs\u002F2.fireskills\u002F1.getting-started\u002F02.project-context",{"title":176,"path":177,"stem":178},"Step 0: Setup","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-setup","docs\u002F2.fireskills\u002F1.getting-started\u002F03.step-setup",{"title":180,"path":181,"stem":182},"Step 1: Inbox","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-inbox","docs\u002F2.fireskills\u002F1.getting-started\u002F04.step-inbox",{"title":184,"path":185,"stem":186},"Step 2: Backlog","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-backlog","docs\u002F2.fireskills\u002F1.getting-started\u002F05.step-backlog",{"title":188,"path":189,"stem":190},"Step 3: Specify","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-specify","docs\u002F2.fireskills\u002F1.getting-started\u002F06.step-specify",{"title":192,"path":193,"stem":194},"Step 4: Validate","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-validate","docs\u002F2.fireskills\u002F1.getting-started\u002F07.step-validate",{"title":196,"path":197,"stem":198},"Step 5: Tasks","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-tasks","docs\u002F2.fireskills\u002F1.getting-started\u002F08.step-tasks",{"title":200,"path":201,"stem":202},"Step 6: TDD\u002FBDD","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-tdd-bdd","docs\u002F2.fireskills\u002F1.getting-started\u002F09.step-tdd-bdd",{"title":204,"path":205,"stem":206},"Step 7: Implement","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-implement","docs\u002F2.fireskills\u002F1.getting-started\u002F10.step-implement",{"title":208,"path":209,"stem":210},"Step 8: Documentator","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fstep-documentator","docs\u002F2.fireskills\u002F1.getting-started\u002F11.step-documentator",{"title":212,"path":213,"stem":214},"Late Changes","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Flate-changes","docs\u002F2.fireskills\u002F1.getting-started\u002F12.late-changes",{"title":216,"path":217,"stem":218},"Other Entry Points","\u002Fdocs\u002Ffireskills\u002Fgetting-started\u002Fother-entry-points","docs\u002F2.fireskills\u002F1.getting-started\u002F13.other-entry-points",{"title":220,"icon":221,"hide":22,"path":222,"stem":223,"children":224,"page":62},"Methodology","i-lucide-git-branch","\u002Fdocs\u002Ffireskills\u002Fmethodology","docs\u002F2.fireskills\u002F2.methodology",[225,229,233,237,241,245],{"title":226,"path":227,"stem":228},"The Three Acts","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Fthree-acts","docs\u002F2.fireskills\u002F2.methodology\u002F0.three-acts",{"title":230,"path":231,"stem":232},"The Three Gates","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Fthree-gates","docs\u002F2.fireskills\u002F2.methodology\u002F1.three-gates",{"title":234,"path":235,"stem":236},"The Canonical Spec","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Fcanonical-spec","docs\u002F2.fireskills\u002F2.methodology\u002F2.canonical-spec",{"title":238,"path":239,"stem":240},"Lifecycle States","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Flifecycle-states","docs\u002F2.fireskills\u002F2.methodology\u002F3.lifecycle-states",{"title":242,"path":243,"stem":244},"Effort Scale","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Feffort-scale","docs\u002F2.fireskills\u002F2.methodology\u002F4.effort-scale",{"title":246,"path":247,"stem":248},"Milestones & Governance","\u002Fdocs\u002Ffireskills\u002Fmethodology\u002Fgovernance-and-milestones","docs\u002F2.fireskills\u002F2.methodology\u002F5.governance-and-milestones",{"title":250,"icon":251,"hide":22,"path":252,"stem":253,"children":254,"page":62},"Skills","i-lucide-layers","\u002Fdocs\u002Ffireskills\u002Fskills","docs\u002F2.fireskills\u002F3.skills",[255,259,263,267,271],{"title":256,"path":257,"stem":258},"Catalog","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fcatalog","docs\u002F2.fireskills\u002F3.skills\u002F0.catalog",{"title":260,"path":261,"stem":262},"Act I: Define","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fact-1-define","docs\u002F2.fireskills\u002F3.skills\u002F1.act-1-define",{"title":264,"path":265,"stem":266},"Act II: Design & Prove","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fact-2-design-prove","docs\u002F2.fireskills\u002F3.skills\u002F2.act-2-design-prove",{"title":268,"path":269,"stem":270},"Act III: Deliver","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fact-3-deliver","docs\u002F2.fireskills\u002F3.skills\u002F3.act-3-deliver",{"title":272,"path":273,"stem":274},"Governance & Decisions","\u002Fdocs\u002Ffireskills\u002Fskills\u002Fgovernance-and-decisions","docs\u002F2.fireskills\u002F3.skills\u002F4.governance-and-decisions",{"title":276,"icon":277,"hide":22,"path":278,"stem":279,"children":280,"page":62},"Specialists","i-lucide-sparkles","\u002Fdocs\u002Ffireskills\u002Fspecialists","docs\u002F2.fireskills\u002F4.specialists",[281,285,289],{"title":282,"path":283,"stem":284},"Overview","\u002Fdocs\u002Ffireskills\u002Fspecialists\u002Foverview","docs\u002F2.fireskills\u002F4.specialists\u002F0.overview",{"title":286,"path":287,"stem":288},"Nuxfire Stack","\u002Fdocs\u002Ffireskills\u002Fspecialists\u002Fnuxfire-stack","docs\u002F2.fireskills\u002F4.specialists\u002F1.nuxfire-stack",{"title":290,"path":291,"stem":292},"Engineering Specialists","\u002Fdocs\u002Ffireskills\u002Fspecialists\u002Fengineering-specialists","docs\u002F2.fireskills\u002F4.specialists\u002F2.engineering-specialists",{"title":294,"icon":295,"hide":22,"path":296,"stem":297,"children":298,"page":62},"CLI & Tooling","i-lucide-terminal","\u002Fdocs\u002Ffireskills\u002Fcli","docs\u002F2.fireskills\u002F5.cli",[299,303,307],{"title":300,"path":301,"stem":302},"CLI Overview","\u002Fdocs\u002Ffireskills\u002Fcli\u002Foverview","docs\u002F2.fireskills\u002F5.cli\u002F0.overview",{"title":304,"path":305,"stem":306},"Commands","\u002Fdocs\u002Ffireskills\u002Fcli\u002Fcommands","docs\u002F2.fireskills\u002F5.cli\u002F1.commands",{"title":308,"path":309,"stem":310},"Safety Scripts","\u002Fdocs\u002Ffireskills\u002Fcli\u002Fgovernance-scripts","docs\u002F2.fireskills\u002F5.cli\u002F2.governance-scripts",{"id":312,"title":313,"author":314,"authorType":315,"body":316,"category":1391,"date":1392,"description":1393,"extension":1394,"image":1395,"lang":1396,"meta":1397,"navigation":22,"ogImage":1398,"path":1399,"readTime":1400,"seo":1401,"stem":1402,"tags":1403,"translations":1407,"updated":315,"__hash__":1411},"blog\u002Fblog\u002Fmulti-tenancy-rbac-saas-security.md","Enterprise Multi-Tenancy and Granular RBAC on PostgreSQL with Drizzle ORM","Nuxfire Team",null,{"type":317,"value":318,"toc":1385},"minimark",[319,332,335,353,356,361,364,384,387,910,912,916,922,1319,1321,1325,1328,1352,1354,1358,1369,1372,1381],[320,321,322,323,327,328,331],"p",{},"The biggest dividing line between an amateur project and enterprise software ready for B2B sales is the security of ",[324,325,326],"strong",{},"multi-tenancy"," and ",[324,329,330],{},"RBAC (Role-Based Access Control)",".",[320,333,334],{},"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.",[320,336,337,338,342,343,346,347,327,350,331],{},"In this article, we show how the ",[339,340,341],"code",{},"layers\u002Fteams"," layer of ",[324,344,345],{},"Nuxfire"," implements failure-proof data isolation with ",[324,348,349],{},"PostgreSQL",[324,351,352],{},"Drizzle ORM",[354,355],"hr",{},[357,358,360],"h2",{"id":359},"data-modeling-the-tenant-id-on-every-table","Data Modeling: The Tenant Id on Every Table",[320,362,363],{},"There are three classic forms of multi-tenancy:",[365,366,367,371,374],"ol",{},[368,369,370],"li",{},"A separate database per customer (expensive and hard to keep migrations in sync).",[368,372,373],{},"A separate schema per customer (connection limits and complexity in the ORM).",[368,375,376,383],{},[324,377,378,379,382],{},"Shared tables with a discriminator column (",[339,380,381],{},"team_id",")"," — The modern industry standard adopted by giants like Stripe, Slack and Linear.",[320,385,386],{},"In Nuxfire, every entity belongs to a team:",[388,389,394],"pre",{"className":390,"code":391,"language":392,"meta":393,"style":393},"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","",[339,395,396,405,449,484,514,550,579,615,626,632,659,686,755,784,870,901],{"__ignoreMap":393},[397,398,401],"span",{"class":399,"line":400},"line",1,[397,402,404],{"class":403},"ss7Ak","\u002F\u002F layers\u002Fteams\u002Fserver\u002Fdatabase\u002Fschema.ts\n",[397,406,408,412,416,420,424,428,432,436,440,442,446],{"class":399,"line":407},2,[397,409,411],{"class":410},"sRxSC","export",[397,413,415],{"class":414},"srJo8"," const",[397,417,419],{"class":418},"s91G_"," teams",[397,421,423],{"class":422},"sGXK2"," =",[397,425,427],{"class":426},"sD0ED"," pgTable",[397,429,431],{"class":430},"ss--_","(",[397,433,435],{"class":434},"siCPE","\"",[397,437,439],{"class":438},"sLACW","teams",[397,441,435],{"class":434},[397,443,445],{"class":444},"swvn1",",",[397,447,448],{"class":444}," {\n",[397,450,452,456,459,462,464,466,469,471,473,475,478,481],{"class":399,"line":451},3,[397,453,455],{"class":454},"squCx","  id",[397,457,458],{"class":444},":",[397,460,461],{"class":426}," text",[397,463,431],{"class":430},[397,465,435],{"class":434},[397,467,468],{"class":438},"id",[397,470,435],{"class":434},[397,472,382],{"class":430},[397,474,331],{"class":444},[397,476,477],{"class":426},"primaryKey",[397,479,480],{"class":430},"()",[397,482,483],{"class":444},",\n",[397,485,487,490,492,494,496,498,501,503,505,507,510,512],{"class":399,"line":486},4,[397,488,489],{"class":454},"  name",[397,491,458],{"class":444},[397,493,461],{"class":426},[397,495,431],{"class":430},[397,497,435],{"class":434},[397,499,500],{"class":438},"name",[397,502,435],{"class":434},[397,504,382],{"class":430},[397,506,331],{"class":444},[397,508,509],{"class":426},"notNull",[397,511,480],{"class":430},[397,513,483],{"class":444},[397,515,517,520,522,524,526,528,531,533,535,537,539,541,543,546,548],{"class":399,"line":516},5,[397,518,519],{"class":454},"  slug",[397,521,458],{"class":444},[397,523,461],{"class":426},[397,525,431],{"class":430},[397,527,435],{"class":434},[397,529,530],{"class":438},"slug",[397,532,435],{"class":434},[397,534,382],{"class":430},[397,536,331],{"class":444},[397,538,509],{"class":426},[397,540,480],{"class":430},[397,542,331],{"class":444},[397,544,545],{"class":426},"unique",[397,547,480],{"class":430},[397,549,483],{"class":444},[397,551,553,556,558,560,562,564,567,569,571,573,575,577],{"class":399,"line":552},6,[397,554,555],{"class":454},"  ownerId",[397,557,458],{"class":444},[397,559,461],{"class":426},[397,561,431],{"class":430},[397,563,435],{"class":434},[397,565,566],{"class":438},"owner_id",[397,568,435],{"class":434},[397,570,382],{"class":430},[397,572,331],{"class":444},[397,574,509],{"class":426},[397,576,480],{"class":430},[397,578,483],{"class":444},[397,580,582,585,587,590,592,594,597,599,601,603,606,608,610,612],{"class":399,"line":581},7,[397,583,584],{"class":454},"  createdAt",[397,586,458],{"class":444},[397,588,589],{"class":426}," timestamp",[397,591,431],{"class":430},[397,593,435],{"class":434},[397,595,596],{"class":438},"created_at",[397,598,435],{"class":434},[397,600,382],{"class":430},[397,602,331],{"class":444},[397,604,605],{"class":426},"defaultNow",[397,607,480],{"class":430},[397,609,331],{"class":444},[397,611,509],{"class":426},[397,613,614],{"class":430},"()\n",[397,616,618,621,623],{"class":399,"line":617},8,[397,619,620],{"class":444},"}",[397,622,382],{"class":430},[397,624,625],{"class":444},";\n",[397,627,629],{"class":399,"line":628},9,[397,630,631],{"emptyLinePlaceholder":22},"\n",[397,633,635,637,639,642,644,646,648,650,653,655,657],{"class":399,"line":634},10,[397,636,411],{"class":410},[397,638,415],{"class":414},[397,640,641],{"class":418}," teamMembers",[397,643,423],{"class":422},[397,645,427],{"class":426},[397,647,431],{"class":430},[397,649,435],{"class":434},[397,651,652],{"class":438},"team_members",[397,654,435],{"class":434},[397,656,445],{"class":444},[397,658,448],{"class":444},[397,660,662,664,666,668,670,672,674,676,678,680,682,684],{"class":399,"line":661},11,[397,663,455],{"class":454},[397,665,458],{"class":444},[397,667,461],{"class":426},[397,669,431],{"class":430},[397,671,435],{"class":434},[397,673,468],{"class":438},[397,675,435],{"class":434},[397,677,382],{"class":430},[397,679,331],{"class":444},[397,681,477],{"class":426},[397,683,480],{"class":430},[397,685,483],{"class":444},[397,687,689,692,694,696,698,700,702,704,706,708,710,712,714,717,719,721,724,726,728,730,732,735,738,740,743,746,748,751,753],{"class":399,"line":688},12,[397,690,691],{"class":454},"  teamId",[397,693,458],{"class":444},[397,695,461],{"class":426},[397,697,431],{"class":430},[397,699,435],{"class":434},[397,701,381],{"class":438},[397,703,435],{"class":434},[397,705,382],{"class":430},[397,707,331],{"class":444},[397,709,509],{"class":426},[397,711,480],{"class":430},[397,713,331],{"class":444},[397,715,716],{"class":426},"references",[397,718,431],{"class":430},[397,720,480],{"class":444},[397,722,723],{"class":414}," =>",[397,725,419],{"class":430},[397,727,331],{"class":444},[397,729,468],{"class":430},[397,731,445],{"class":444},[397,733,734],{"class":444}," {",[397,736,737],{"class":454}," onDelete",[397,739,458],{"class":444},[397,741,742],{"class":434}," \"",[397,744,745],{"class":438},"cascade",[397,747,435],{"class":434},[397,749,750],{"class":444}," }",[397,752,382],{"class":430},[397,754,483],{"class":444},[397,756,758,761,763,765,767,769,772,774,776,778,780,782],{"class":399,"line":757},13,[397,759,760],{"class":454},"  userId",[397,762,458],{"class":444},[397,764,461],{"class":426},[397,766,431],{"class":430},[397,768,435],{"class":434},[397,770,771],{"class":438},"user_id",[397,773,435],{"class":434},[397,775,382],{"class":430},[397,777,331],{"class":444},[397,779,509],{"class":426},[397,781,480],{"class":430},[397,783,483],{"class":444},[397,785,787,790,792,794,796,798,801,803,805,807,810,812,815,817,820,822,824,826,829,831,833,835,838,840,843,845,847,849,851,853,855,858,860,862,864,866,868],{"class":399,"line":786},14,[397,788,789],{"class":454},"  role",[397,791,458],{"class":444},[397,793,461],{"class":426},[397,795,431],{"class":430},[397,797,435],{"class":434},[397,799,800],{"class":438},"role",[397,802,435],{"class":434},[397,804,445],{"class":444},[397,806,734],{"class":444},[397,808,809],{"class":454}," enum",[397,811,458],{"class":444},[397,813,814],{"class":430}," [",[397,816,435],{"class":434},[397,818,819],{"class":438},"OWNER",[397,821,435],{"class":434},[397,823,445],{"class":444},[397,825,742],{"class":434},[397,827,828],{"class":438},"ADMIN",[397,830,435],{"class":434},[397,832,445],{"class":444},[397,834,742],{"class":434},[397,836,837],{"class":438},"MEMBER",[397,839,435],{"class":434},[397,841,842],{"class":430},"] ",[397,844,620],{"class":444},[397,846,382],{"class":430},[397,848,331],{"class":444},[397,850,509],{"class":426},[397,852,480],{"class":430},[397,854,331],{"class":444},[397,856,857],{"class":426},"default",[397,859,431],{"class":430},[397,861,435],{"class":434},[397,863,837],{"class":438},[397,865,435],{"class":434},[397,867,382],{"class":430},[397,869,483],{"class":444},[397,871,873,875,877,879,881,883,885,887,889,891,893,895,897,899],{"class":399,"line":872},15,[397,874,584],{"class":454},[397,876,458],{"class":444},[397,878,589],{"class":426},[397,880,431],{"class":430},[397,882,435],{"class":434},[397,884,596],{"class":438},[397,886,435],{"class":434},[397,888,382],{"class":430},[397,890,331],{"class":444},[397,892,605],{"class":426},[397,894,480],{"class":430},[397,896,331],{"class":444},[397,898,509],{"class":426},[397,900,614],{"class":430},[397,902,904,906,908],{"class":399,"line":903},16,[397,905,620],{"class":444},[397,907,382],{"class":430},[397,909,625],{"class":444},[354,911],{},[357,913,915],{"id":914},"middleware-and-authenticated-trpc-procedures","Middleware and Authenticated tRPC Procedures",[320,917,918,919,921],{},"To make sure no developer forgets to filter by ",[339,920,381],{},", requests go through protected tRPC procedures:",[388,923,925],{"className":390,"code":924,"language":392,"meta":393,"style":393},"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",[339,926,927,941,983,1020,1060,1073,1099,1128,1133,1142,1146,1165,1180,1196,1211,1220,1225,1230,1242,1252,1263,1279,1294,1300,1309],{"__ignoreMap":393},[397,928,929,931,933,936,938],{"class":399,"line":400},[397,930,411],{"class":410},[397,932,415],{"class":414},[397,934,935],{"class":418}," teamProcedure",[397,937,423],{"class":422},[397,939,940],{"class":430}," protectedProcedure\n",[397,942,943,946,949,952,954,957,959,962,965,967,970,972,975,978,980],{"class":399,"line":407},[397,944,945],{"class":444},"  .",[397,947,948],{"class":426},"input",[397,950,951],{"class":430},"(z",[397,953,331],{"class":444},[397,955,956],{"class":426},"object",[397,958,431],{"class":430},[397,960,961],{"class":444},"{",[397,963,964],{"class":454}," teamId",[397,966,458],{"class":444},[397,968,969],{"class":430}," z",[397,971,331],{"class":444},[397,973,974],{"class":426},"string",[397,976,977],{"class":430},"() ",[397,979,620],{"class":444},[397,981,982],{"class":430},"))\n",[397,984,985,987,990,992,996,999,1003,1005,1008,1010,1013,1016,1018],{"class":399,"line":451},[397,986,945],{"class":444},[397,988,989],{"class":426},"use",[397,991,431],{"class":430},[397,993,995],{"class":994},"sTNss","async",[397,997,998],{"class":444}," ({",[397,1000,1002],{"class":1001},"sQgqH"," ctx",[397,1004,445],{"class":444},[397,1006,1007],{"class":1001}," input",[397,1009,445],{"class":444},[397,1011,1012],{"class":1001}," next",[397,1014,1015],{"class":444}," })",[397,1017,723],{"class":414},[397,1019,448],{"class":444},[397,1021,1022,1025,1028,1030,1033,1035,1037,1040,1042,1045,1047,1050,1052,1055,1057],{"class":399,"line":486},[397,1023,1024],{"class":414},"    const",[397,1026,1027],{"class":418}," membership",[397,1029,423],{"class":422},[397,1031,1032],{"class":410}," await",[397,1034,1002],{"class":430},[397,1036,331],{"class":444},[397,1038,1039],{"class":430},"db",[397,1041,331],{"class":444},[397,1043,1044],{"class":430},"query",[397,1046,331],{"class":444},[397,1048,1049],{"class":430},"teamMembers",[397,1051,331],{"class":444},[397,1053,1054],{"class":426},"findFirst",[397,1056,431],{"class":454},[397,1058,1059],{"class":444},"{\n",[397,1061,1062,1065,1067,1070],{"class":399,"line":516},[397,1063,1064],{"class":454},"      where",[397,1066,458],{"class":444},[397,1068,1069],{"class":426}," and",[397,1071,1072],{"class":454},"(\n",[397,1074,1075,1078,1080,1082,1084,1087,1089,1091,1093,1095,1097],{"class":399,"line":552},[397,1076,1077],{"class":426},"        eq",[397,1079,431],{"class":454},[397,1081,1049],{"class":430},[397,1083,331],{"class":444},[397,1085,1086],{"class":430},"teamId",[397,1088,445],{"class":444},[397,1090,1007],{"class":430},[397,1092,331],{"class":444},[397,1094,1086],{"class":430},[397,1096,382],{"class":454},[397,1098,483],{"class":444},[397,1100,1101,1103,1105,1107,1109,1112,1114,1116,1118,1121,1123,1125],{"class":399,"line":581},[397,1102,1077],{"class":426},[397,1104,431],{"class":454},[397,1106,1049],{"class":430},[397,1108,331],{"class":444},[397,1110,1111],{"class":430},"userId",[397,1113,445],{"class":444},[397,1115,1002],{"class":430},[397,1117,331],{"class":444},[397,1119,1120],{"class":430},"user",[397,1122,331],{"class":444},[397,1124,468],{"class":430},[397,1126,1127],{"class":454},")\n",[397,1129,1130],{"class":399,"line":617},[397,1131,1132],{"class":454},"      )\n",[397,1134,1135,1138,1140],{"class":399,"line":628},[397,1136,1137],{"class":444},"    }",[397,1139,382],{"class":454},[397,1141,625],{"class":444},[397,1143,1144],{"class":399,"line":634},[397,1145,631],{"emptyLinePlaceholder":22},[397,1147,1148,1151,1154,1157,1160,1163],{"class":399,"line":661},[397,1149,1150],{"class":410},"    if",[397,1152,1153],{"class":454}," (",[397,1155,1156],{"class":422},"!",[397,1158,1159],{"class":430},"membership",[397,1161,1162],{"class":454},") ",[397,1164,1059],{"class":444},[397,1166,1167,1170,1173,1176,1178],{"class":399,"line":688},[397,1168,1169],{"class":410},"      throw",[397,1171,1172],{"class":422}," new",[397,1174,1175],{"class":426}," TRPCError",[397,1177,431],{"class":454},[397,1179,1059],{"class":444},[397,1181,1182,1185,1187,1189,1192,1194],{"class":399,"line":757},[397,1183,1184],{"class":454},"        code",[397,1186,458],{"class":444},[397,1188,742],{"class":434},[397,1190,1191],{"class":438},"FORBIDDEN",[397,1193,435],{"class":434},[397,1195,483],{"class":444},[397,1197,1198,1201,1203,1205,1208],{"class":399,"line":786},[397,1199,1200],{"class":454},"        message",[397,1202,458],{"class":444},[397,1204,742],{"class":434},[397,1206,1207],{"class":438},"You do not have access to this workspace.",[397,1209,1210],{"class":434},"\"\n",[397,1212,1213,1216,1218],{"class":399,"line":872},[397,1214,1215],{"class":444},"      }",[397,1217,382],{"class":454},[397,1219,625],{"class":444},[397,1221,1222],{"class":399,"line":903},[397,1223,1224],{"class":444},"    }\n",[397,1226,1228],{"class":399,"line":1227},17,[397,1229,631],{"emptyLinePlaceholder":22},[397,1231,1233,1236,1238,1240],{"class":399,"line":1232},18,[397,1234,1235],{"class":410},"    return",[397,1237,1012],{"class":426},[397,1239,431],{"class":454},[397,1241,1059],{"class":444},[397,1243,1245,1248,1250],{"class":399,"line":1244},19,[397,1246,1247],{"class":454},"      ctx",[397,1249,458],{"class":444},[397,1251,448],{"class":444},[397,1253,1255,1258,1261],{"class":399,"line":1254},20,[397,1256,1257],{"class":422},"        ...",[397,1259,1260],{"class":430},"ctx",[397,1262,483],{"class":444},[397,1264,1266,1269,1271,1273,1275,1277],{"class":399,"line":1265},21,[397,1267,1268],{"class":454},"        teamId",[397,1270,458],{"class":444},[397,1272,1007],{"class":430},[397,1274,331],{"class":444},[397,1276,1086],{"class":430},[397,1278,483],{"class":444},[397,1280,1282,1285,1287,1289,1291],{"class":399,"line":1281},22,[397,1283,1284],{"class":454},"        role",[397,1286,458],{"class":444},[397,1288,1027],{"class":430},[397,1290,331],{"class":444},[397,1292,1293],{"class":430},"role\n",[397,1295,1297],{"class":399,"line":1296},23,[397,1298,1299],{"class":444},"      }\n",[397,1301,1303,1305,1307],{"class":399,"line":1302},24,[397,1304,1137],{"class":444},[397,1306,382],{"class":454},[397,1308,625],{"class":444},[397,1310,1312,1315,1317],{"class":399,"line":1311},25,[397,1313,1314],{"class":444},"  }",[397,1316,382],{"class":430},[397,1318,625],{"class":444},[354,1320],{},[357,1322,1324],{"id":1323},"email-invitation-flow-with-an-encrypted-token","Email Invitation Flow with an Encrypted Token",[320,1326,1327],{},"Invitations for new members follow a secure double-confirmation flow:",[365,1329,1330,1339,1342,1349],{},[368,1331,1332,1333,1335,1336,1338],{},"The team administrator enters the collaborator's email and selects the role (",[339,1334,828],{}," or ",[339,1337,837],{},").",[368,1340,1341],{},"The system generates a cryptographically secure random token that expires in 7 days.",[368,1343,1344,1345,1348],{},"A polished email built with ",[324,1346,1347],{},"React Email"," is sent through an asynchronous queue.",[368,1350,1351],{},"When they click the link, the user is taken to the accept or onboarding screen, linking the account to the team instantly.",[354,1353],{},[357,1355,1357],{"id":1356},"audit-logs-for-enterprise-compliance","Audit Logs for Enterprise Compliance",[320,1359,1360,1361,1364,1365,1368],{},"For demanding B2B customers, Nuxfire already includes a dedicated ",[324,1362,1363],{},"Audit Logs"," table (",[339,1366,1367],{},"\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.",[320,1370,1371],{},"Build your SaaS on a foundation that meets the highest enterprise security standards from the very first commit.",[320,1373,1374],{},[1375,1376,1378],"a",{"href":1377},"\u002F#pricing",[324,1379,1380],{},"Get your Nuxfire license and secure immediate access to the code and the VIP Group →",[1382,1383,1384],"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":393,"searchDepth":407,"depth":407,"links":1386},[1387,1388,1389,1390],{"id":359,"depth":407,"text":360},{"id":914,"depth":407,"text":915},{"id":1323,"depth":407,"text":1324},{"id":1356,"depth":407,"text":1357},"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":313,"description":1393},"blog\u002Fmulti-tenancy-rbac-saas-security",[1404,1405,349,352,1406],"Multi-Tenancy","RBAC","Security",{"en":1408,"pt-br":1409,"es":1410},"multi-tenancy-rbac-saas-security","multi-tenancy-rbac-seguranca-saas","multi-tenancy-rbac-seguridad-saas","aA8BAEYe0nYoqTBeIYa_YEVt1er_cAxivk9vIwco58w",1790707502974]