Loading...
Loading...
> Multi-tenancy support with role-based access control (RBAC), team invitations, and organization management.
Fabrk includes a complete multi-tenancy system for B2B SaaS applications with organization creation and management, role-based access control (Owner, Admin, Member, Guest), email-based team invitations, organization-scoped data isolation, member management and role changes, and organization settings and branding.
API endpoint to create a new organization
1// src/app/api/v1/organizations/route.ts2import { NextResponse } from "next/server";3import { prisma } from "@/lib/db";4import { auth } from "@/lib/auth";5import slugify from "slugify";67export async function POST(request: Request) {8 const session = await auth();9 if (!session?.user) {10 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });11 }1213 const { name } = await request.json();1415 if (!name || name.length < 2) {16 return NextResponse.json(17 { error: "Organization name must be at least 2 characters" },18 { status: 400 }19 );20 }2122 // Generate unique slug23 let slug = slugify(name, { lower: true, strict: true });24 const existing = await prisma.organization.findUnique({25 where: { slug },26 });2728 if (existing) {29 slug = `${slug}-${Date.now()}`;30 }3132 // Create org with user as owner33 const organization = await prisma.organization.create({34 data: {35 name,36 slug,37 members: {38 create: {39 userId: session.user.id,40 role: "OWNER",41 },42 },43 },44 include: {45 members: {46 include: { user: { select: { id: true, name: true, email: true } } },47 },48 },49 });5051 return NextResponse.json(organization, { status: 201 });52}
Send team invitations
1// src/app/api/v1/organizations/invite/route.ts2import { nanoid } from "nanoid";3import { sendInviteEmail } from "@/lib/email";45export async function POST(request: Request) {6 const session = await auth();7 if (!session?.user) {8 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });9 }1011 const { organizationId, email, role = "MEMBER" } = await request.json();1213 // Check user has permission to invite14 const membership = await prisma.organizationMember.findUnique({15 where: {16 userId_organizationId: {17 userId: session.user.id,18 organizationId,19 },20 },21 });2223 if (!membership || !["OWNER", "ADMIN"].includes(membership.role)) {24 return NextResponse.json({ error: "Forbidden" }, { status: 403 });25 }2627 // Create invite28 const token = nanoid(32);29 const invite = await prisma.organizationInvite.create({30 data: {31 email,32 organizationId,33 role,34 token,35 invitedById: session.user.id,36 expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days37 },38 include: {39 organization: { select: { name: true } },40 },41 });4243 // Send invite email44 await sendInviteEmail({45 to: email,46 organizationName: invite.organization.name,47 inviterName: session.user.name || session.user.email,48 inviteUrl: `${config.app.url}/invite/${token}`,49 role,50 });5152 return NextResponse.json({ success: true, invite });53}
Reusable permission checking
1// src/lib/permissions.ts2import { prisma } from "@/lib/db";34type Permission =5 | "view"6 | "create"7 | "invite"8 | "remove_members"9 | "edit_settings"10 | "manage_billing"11 | "delete_org";1213const rolePermissions: Record<string, Permission[]> = {14 OWNER: ["view", "create", "invite", "remove_members", "edit_settings", "manage_billing", "delete_org"],15 ADMIN: ["view", "create", "invite", "remove_members", "edit_settings", "manage_billing"],16 MEMBER: ["view", "create"],17 GUEST: ["view"],18};1920export async function checkPermission(21 userId: string,22 organizationId: string,23 permission: Permission24): Promise<boolean> {25 const membership = await prisma.organizationMember.findUnique({26 where: {27 userId_organizationId: { userId, organizationId },28 },29 });3031 if (!membership) return false;3233 return rolePermissions[membership.role]?.includes(permission) ?? false;34}
Core models in prisma/schema.prisma:
1enum OrgRole {2 OWNER3 ADMIN4 MEMBER5 GUEST6}78model Organization {9 id String @id @default(cuid())10 name String11 slug String @unique12 logo String?13 website String?14 createdAt DateTime @default(now())15 updatedAt DateTime @updatedAt1617 members OrganizationMember[]18 invites OrganizationInvite[]19}2021model OrganizationMember {22 id String @id @default(cuid())23 userId String24 organizationId String25 role OrgRole @default(MEMBER)26 joinedAt DateTime @default(now())2728 user User @relation(fields: [userId], references: [id], onDelete: Cascade)29 organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)3031 @@unique([userId, organizationId])32 @@index([organizationId])33}3435model OrganizationInvite {36 id String @id @default(cuid())37 email String38 organizationId String39 role OrgRole @default(MEMBER)40 token String @unique41 expiresAt DateTime42 invitedById String43 createdAt DateTime @default(now())4445 organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)4647 @@index([email])48 @@index([token])49}
| Permission | Owner | Admin | Member | Guest |
|---|---|---|---|---|
| View organization | Yes | Yes | Yes | Yes |
| Create/edit content | Yes | Yes | Yes | No |
| Invite members | Yes | Yes | No | No |
| Remove members | Yes | Yes | No | No |
| Edit org settings | Yes | Yes | No | No |
| Manage billing | Yes | Yes | No | No |
| Delete organization | Yes | No | No | No |
organizationId