LogoBoring Template

Multi Workspace

Manage multiple workspaces, members, roles, settings, and invitations in your application.

This template supports multi-tenant architecture, allowing users to create and belong to multiple workspaces simultaneously, each with its own members, settings, and resources.

Workspace Overview

Multi-tenancy

Our platform supports multi-workspace management, allowing users to create and belong to multiple workspaces simultaneously. Each workspace functions as a separate environment with its own members, settings, and resources.

Workspace Schema

Database schema for workspaces:

server/db/schemas/workspaces.ts
export const workspaces = pgTable("workspaces", {
  id: text("id").primaryKey().notNull(),
  name: text("name").notNull(),
  slug: text("slug").notNull().unique(),
  logo: text("logo"),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at"),
  ownerId: text("owner_id")
    .notNull()
    .references(() => users.id, { onDelete: "cascade" }),
});

Key Features

Multiple Workspaces

🏢 Users can create and join multiple workspaces, each with separate settings and members

Role-Based Access

🔑 Granular permission system with customizable roles for different access levels

Member Management

👥 Add, remove, and manage workspace members with different permission levels

Invitation System

✉️ Secure email-based invitation system for adding new members to workspaces

Workspace Management

Workspace operations are protected by authentication and authorization checks. Always verify permissions before performing sensitive actions like deleting workspaces or managing members.

Create Workspace

Create a new workspace with custom settings using tRPC:

Example: Creating a workspace
import { useCreateWorkspaceTRPC } from "@/hooks/workspaces";
 
// Inside your component
const { mutate, isPending } = useCreateWorkspaceTRPC();
 
// Create a new workspace
mutate({
  name: "My Team Workspace",
  slug: "my-team", // Optional, generated from name if not provided
});

Update Workspace

Modify workspace settings and details using tRPC:

Example: Updating a workspace
import { useUpdateWorkspaceTRPC } from "@/hooks/workspaces";
 
// Inside your component
const { mutate, isPending } = useUpdateWorkspaceTRPC({
  onSuccess: () => {
    // Additional success handling
    console.log("Workspace updated successfully");
  },
  onError: () => {
    // Additional error handling
    console.error("Failed to update workspace");
  },
});
 
// Update workspace settings
mutate({
  slug: workspace.slug,
  name: "Updated Workspace Name",
  logo: logoFileId, // Optional
});

Delete Workspace

Permanently delete a workspace and all its data using tRPC:

Example: Deleting a workspace
import { useDeleteWorkspaceTRPC } from "@/hooks/workspaces";
 
// Inside your component
const { mutate, isPending } = useDeleteWorkspaceTRPC({
  enableRedirect: true, // Redirect after successful deletion
});
 
// Delete workspace (requires owner permissions)
mutate({
  slug: workspace.slug,
});

Transfer Ownership

Transfer workspace ownership to another member:

Example: Transferring ownership
import { useTransferOwnershipTRPC } from "@/hooks/workspaces";
 
// Inside your component
const { mutate, isPending } = useTransferOwnershipTRPC();
 
// Transfer ownership to another user
mutate({
  slug: workspace.slug,
  userId: newOwnerId,
});

Update the workspace logo:

Example: Updating workspace logo
import { useUpdateWorkspaceLogoTRPC } from "@/hooks/workspaces";
 
// Inside your component
const { mutate, isPending } = useUpdateWorkspaceLogoTRPC();
 
// Update workspace logo
mutate({
  slug: workspace.slug,
  logoId: newLogoFileId,
});

Member Management

Collaboration

The member management system allows workspace owners and admins to invite, remove, and manage the roles of workspace members.

Invite Members

Send email invitations to new members:

Example: Inviting members
import { trpc } from "@/trpc/client";
import { toast } from "sonner";
 
// Inside your component
const utils = trpc.useUtils();
const { mutate, isPending } = trpc.invitations.create.useMutation({
  onSuccess: (data) => {
    toast.success(data.message);
  },
  onError: (error) => {
    toast.error(error.message);
  },
  onSettled: () => {
    utils.invitations.getAll.invalidate();
  },
});
 
// Invite new members
mutate({
  emails: ["user@example.com", "team@example.com"],
  role: "member", // Default role for invited users
  slug: workspace.slug,
});

Change Member Role

Update a member's role and permissions:

Example: Changing member role
import { trpc } from "@/trpc/client";
import { toast } from "sonner";
 
// Inside your component
const utils = trpc.useUtils();
const { mutate, isPending } = trpc.members.update.useMutation({
  onSuccess: (data) => {
    toast.success(data.message);
  },
  onError: (error) => {
    toast.error(error.message);
  },
  onSettled: () => {
    utils.members.getMany.invalidate();
  },
});
 
// Update member role
mutate({
  userId: member.id,
  role: "admin",
  slug: workspace.slug,
});

Remove Member

Remove a member from the workspace:

Example: Removing a member
import { trpc } from "@/trpc/client";
import { toast } from "sonner";
 
// Inside your component
const utils = trpc.useUtils();
const { mutate, isPending } = trpc.members.remove.useMutation({
  onSuccess: (data) => {
    toast.success(data.message);
  },
  onError: (error) => {
    toast.error(error.message);
  },
  onSettled: () => {
    utils.members.getMany.invalidate();
  },
});
 
// Remove member from workspace
mutate({
  userId: member.id,
  slug: workspace.slug,
});

Workspace Settings

Available Settings

General Settings

  • Workspace name and slug
  • Logo and branding
  • Default member role
  • Workspace visibility

Security Settings

  • Invitation expiration time
  • Required email domains
  • Two-factor authentication
  • Session management

Role Management

  • Create custom roles
  • Assign permissions to roles
  • Modify existing roles
  • Set default member role

Advanced Features

  • Workspace transfer
  • Data export/import
  • API key management
  • Audit logging

Technical Implementation

The multi-workspace system uses a combination of PostgreSQL tables with foreign key relationships to maintain data integrity. Each workspace has its own set of members, roles, and resources.

Database Schema

The multi-workspace system relies on several interconnected database tables:

Example schema relationships
// Workspaces
export const workspaces = pgTable("workspaces", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  slug: text("slug").notNull().unique(),
  // ... other fields
});
 
// Workspace Members
export const members = pgTable("members", {
  id: text("id").primaryKey(),
  workspaceId: text("workspace_id").references(() => workspaces.id, {
    onDelete: "cascade",
  }),
  userId: text("user_id").references(() => users.id, {
    onDelete: "cascade",
  }),
  role: text("role").notNull().default("member"),
  // ... other fields
});
 
// Workspace Invitations
export const invitations = pgTable("invitations", {
  id: text("id").primaryKey(),
  workspaceId: text("workspace_id").references(() => workspaces.id, {
    onDelete: "cascade",
  }),
  email: text("email").notNull(),
  role: text("role").notNull().default("member"),
  expiresAt: timestamp("expires_at").notNull(),
  // ... other fields
});

Workspace Context with React Query

Access current workspace data in components:

Example: Accessing workspace context
import { trpc } from "@/trpc/client";
 
// Use the tRPC query to fetch workspace data
const {
  data: workspace,
  isLoading,
  error,
} = trpc.workspaces.getOne.useQuery(
  { slug: params.slug },
  {
    enabled: !!params.slug,
    staleTime: 1000 * 60 * 5, // 5 minutes
  }
);
 
// Access workspace data
if (workspace) {
  console.log(workspace.name);
  console.log(workspace.members);
  console.log(workspace.isOwner); // Current user is owner
  console.log(workspace.currentUserRole); // Role of current user
}

TRPC Router Implementation

The workspace functionality is implemented using tRPC routers:

Example: Workspace TRPC router
export const workspacesRouter = createTRPCRouter({
  getAll: protectedProcedure.query(async ({ ctx }) => {
    // Get all workspaces the user is a member of
    return await getWorkspaces(ctx.session.user.id);
  }),
 
  getOne: protectedProcedure
    .input(z.object({ slug: z.string() }))
    .query(async ({ ctx, input }) => {
      // Get a specific workspace by slug
      return await getWorkspaceBySlug(input.slug, ctx.session.user.id);
    }),
 
  create: protectedProcedure
    .input(createWorkspaceSchema)
    .mutation(async ({ ctx, input }) => {
      // Create a new workspace
      return await createWorkspace({
        ...input,
        userId: ctx.session.user.id,
      });
    }),
 
  // ... other endpoints
});

Best Practices

Security Considerations

  • Always verify user permissions for workspace operations
  • Implement rate limiting for invite operations
  • Use transactions for operations that modify multiple tables
  • Sanitize and validate all user inputs

Performance Optimization

  • Use proper database indexes on workspace_id, user_id fields
  • Cache frequently accessed workspace data
  • Implement pagination for member lists in large workspaces
  • Use optimistic updates for improved UX

Additional Resources

Authentication Integration

See the Authentication documentation for details on integrating workspace access with authentication.

Database Schema

Refer to the Database documentation for more information on the database schema.