Organization & Team Management
Complete guide to workspace management, member invitations, role permissions, and team collaboration
Organization & Team Management
ProductReady uses Better Auth's organization plugin to implement complete multi-tenant collaboration features. This guide covers workspace management, member invitations, role permissions, and team collaboration.
Terminology: In ProductReady, "Workspace" (Space) and "Organization" refer to the same concept. The code uses Organization, while the user interface displays Space.
Quick Overview
Core Features:
- ✅ Create and manage multiple workspaces
- ✅ Invite members to workspaces
- ✅ Role-based access control (RBAC)
- ✅ Team/Group management
- ✅ Workspace settings and billing management
- ✅ Leave or delete workspaces
Role Hierarchy:
- Owner: Full permissions, including workspace deletion
- Admin: Nearly full permissions, cannot delete workspace or modify billing
- Member: Basic work permissions, can create and edit content
- Viewer: Read-only permissions, can view but not modify
Workspace (Space) Management
Create Workspace
After logging in, users can create multiple workspaces for different projects or teams.
How to create:
- Click the workspace selector at the top of the left sidebar
- Click "+ New Workspace" button
- Enter workspace name and description
- Click "Create"
'use client';
import { authClient } from '~/lib/auth/client';
import { useState } from 'react';
export function CreateSpaceButton() {
const [name, setName] = useState('');
const [slug, setSlug] = useState('');
async function handleCreate() {
const result = await authClient.organization.create({
name,
slug, // Optional, for URL
});
if (result.error) {
console.error('Create failed:', result.error);
return;
}
console.log('Workspace created:', result.data);
}
return (
<div>
<input
type="text"
placeholder="Workspace name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<button onClick={handleCreate}>Create Workspace</button>
</div>
);
}Switch Workspace
Users can quickly switch between multiple workspaces.
How to switch:
- Click the workspace selector at the top of the left sidebar
- Select the target workspace from the list
- The page automatically switches to the new workspace context
'use client';
import { authClient } from '~/lib/auth/client';
export function SwitchSpace({ organizationId }: { organizationId: string }) {
async function handleSwitch() {
await authClient.organization.setActive({
organizationId,
});
// Refresh page to update context
window.location.reload();
}
return (
<button onClick={handleSwitch}>
Switch to this workspace
</button>
);
}Update Workspace Info
Workspace owners and admins can update basic workspace information.
'use client';
import { authClient } from '~/lib/auth/client';
export function UpdateSpace() {
async function handleUpdate(organizationId: string, updates: { name?: string; slug?: string }) {
const result = await authClient.organization.update({
organizationId,
data: updates,
});
if (result.error) {
console.error('Update failed:', result.error);
return;
}
console.log('Workspace updated');
}
return (
<button onClick={() => handleUpdate('org-id', { name: 'New Name' })}>
Update Workspace
</button>
);
}Delete Workspace
Only workspace owners can delete workspaces. Deletion is irreversible.
Steps to delete:
- Open workspace settings (click "Settings" button in sidebar footer)
- Switch to "General" tab
- Scroll to "Danger Zone"
- Click "Delete Workspace" button
- Enter workspace name to confirm
- Click "Confirm Deletion"
Warning: Deleting a workspace will permanently delete all related data, including members, teams, projects, documents, etc. This action cannot be undone!
'use client';
import { authClient } from '~/lib/auth/client';
export function DeleteSpace({ organizationId }: { organizationId: string }) {
async function handleDelete() {
const confirmed = window.confirm('Are you sure you want to delete this workspace? All data will be permanently deleted!');
if (!confirmed) return;
const result = await authClient.organization.delete({
organizationId,
});
if (result.error) {
console.error('Delete failed:', result.error);
return;
}
// Deletion successful, redirect to workspace list
window.location.href = '/spaces';
}
return (
<button onClick={handleDelete} className="btn-danger">
Delete Workspace
</button>
);
}Member Management
Invite Members
Admins and owners can invite new members to the workspace via email.
Steps to invite:
- Open workspace settings → "People" tab
- Click "Invite Member" button
- Enter member's email address
- Select role (Member, Admin, Viewer)
- Click "Send Invitation"
Invitees will receive an invitation email and can accept by clicking the link.
'use client';
import { authClient } from '~/lib/auth/client';
import { useState } from 'react';
export function InviteMemberForm() {
const [email, setEmail] = useState('');
const [role, setRole] = useState<'member' | 'admin'>('member');
async function handleInvite() {
const result = await authClient.organization.inviteMember({
email,
role,
});
if (result.error) {
console.error('Invite failed:', result.error);
return;
}
alert('Invitation sent!');
setEmail('');
}
return (
<form onSubmit={(e) => { e.preventDefault(); handleInvite(); }}>
<input
type="email"
placeholder="Member email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<select value={role} onChange={(e) => setRole(e.target.value as any)}>
<option value="member">Member</option>
<option value="admin">Admin</option>
</select>
<button type="submit">Send Invitation</button>
</form>
);
}View Pending Invitations
Admins can view and manage all pending invitations.
How to view:
- Open workspace settings → "People" tab
- See "Pending Invitations" section below member list
- Can cancel any pending invitation
'use client';
import { authClient } from '~/lib/auth/client';
import { useEffect, useState } from 'react';
interface Invitation {
id: string;
email: string;
role: string;
status: string;
expiresAt: Date;
}
export function PendingInvitations() {
const [invitations, setInvitations] = useState<Invitation[]>([]);
useEffect(() => {
loadInvitations();
}, []);
async function loadInvitations() {
const result = await authClient.organization.listInvitations();
if (result.data) {
setInvitations(result.data as Invitation[]);
}
}
async function cancelInvitation(invitationId: string) {
await authClient.organization.cancelInvitation({
invitationId,
});
loadInvitations(); // Refresh list
}
return (
<div>
<h3>Pending Invitations</h3>
{invitations.map((inv) => (
<div key={inv.id}>
<span>{inv.email} - {inv.role}</span>
<button onClick={() => cancelInvitation(inv.id)}>
Cancel Invitation
</button>
</div>
))}
</div>
);
}Accept or Reject Invitations
Invited users can view and manage all received invitations.
View invitations:
- After logging in, if there are pending invitations, you'll see "My Invitations" button on the people settings page
- Click the button to view all invitations
- Can choose to accept or reject
'use client';
import { authClient } from '~/lib/auth/client';
import { useEffect, useState } from 'react';
export function UserInvitations() {
const [invitations, setInvitations] = useState<any[]>([]);
useEffect(() => {
loadUserInvitations();
}, []);
async function loadUserInvitations() {
const result = await authClient.organization.listUserInvitations();
if (result.data) {
setInvitations(result.data);
}
}
async function acceptInvitation(invitationId: string) {
const result = await authClient.organization.acceptInvitation({
invitationId,
});
if (!result.error) {
alert('Joined workspace!');
loadUserInvitations();
}
}
async function rejectInvitation(invitationId: string) {
const result = await authClient.organization.rejectInvitation({
invitationId,
});
if (!result.error) {
loadUserInvitations();
}
}
return (
<div>
<h3>My Invitations</h3>
{invitations.map((inv) => (
<div key={inv.id}>
<p>Invited to join: {inv.organizationName}</p>
<p>Role: {inv.role}</p>
<button onClick={() => acceptInvitation(inv.id)}>Accept</button>
<button onClick={() => rejectInvitation(inv.id)}>Reject</button>
</div>
))}
</div>
);
}Change Member Role
Admins and owners can change member roles.
Steps to change:
- Open workspace settings → "People" tab
- Find the member to modify
- Click role badge or action button
- Select new role
- Confirm change
'use client';
import { authClient } from '~/lib/auth/client';
export function UpdateMemberRole({
memberId,
newRole,
}: {
memberId: string;
newRole: 'member' | 'admin';
}) {
async function handleUpdate() {
const result = await authClient.organization.updateMemberRole({
memberIdOrEmail: memberId,
role: newRole,
});
if (result.error) {
console.error('Update failed:', result.error);
return;
}
alert('Role updated!');
}
return <button onClick={handleUpdate}>Change to {newRole}</button>;
}Remove Member
Admins and owners can remove members from the workspace.
'use client';
import { authClient } from '~/lib/auth/client';
export function RemoveMember({ memberId }: { memberId: string }) {
async function handleRemove() {
const confirmed = window.confirm('Are you sure you want to remove this member?');
if (!confirmed) return;
const result = await authClient.organization.removeMember({
memberIdOrEmail: memberId,
});
if (result.error) {
console.error('Remove failed:', result.error);
return;
}
alert('Member removed');
}
return <button onClick={handleRemove}>Remove Member</button>;
}Leave Workspace
Non-owner members can voluntarily leave a workspace.
Steps to leave:
- Open workspace settings → "People" tab
- Find yourself in the member list
- Click "Leave" button
- Confirm action
Note: Owners cannot leave their own workspace; they must first transfer ownership or delete the workspace.
'use client';
import { authClient } from '~/lib/auth/client';
import { useRouter } from 'next/navigation';
export function LeaveOrganization({ organizationId }: { organizationId: string }) {
const router = useRouter();
async function handleLeave() {
const confirmed = window.confirm('Are you sure you want to leave this workspace? You will lose access to all content.');
if (!confirmed) return;
const result = await authClient.organization.leave({
organizationId,
});
if (result.error) {
console.error('Leave failed:', result.error);
return;
}
// Redirect to workspace list
router.push('/spaces');
}
return <button onClick={handleLeave}>Leave Workspace</button>;
}Team/Group Management
Teams (Team/Group) are sub-units within a workspace, used to organize members and assign permissions.
Create Team
Admins and owners can create teams.
Steps to create:
- Open workspace settings → "People" tab → "Groups" sub-tab
- Click "Create Team" button
- Enter team name
- Click "Create"
'use client';
import { authClient } from '~/lib/auth/client';
import { useState } from 'react';
export function CreateTeamForm() {
const [name, setName] = useState('');
async function handleCreate() {
const result = await authClient.organization.createTeam({
name,
});
if (result.error) {
console.error('Create failed:', result.error);
return;
}
alert('Team created!');
setName('');
}
return (
<form onSubmit={(e) => { e.preventDefault(); handleCreate(); }}>
<input
type="text"
placeholder="Team name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<button type="submit">Create Team</button>
</form>
);
}Add Member to Team
Admins can add workspace members to specific teams.
'use client';
import { authClient } from '~/lib/auth/client';
export function AddMemberToTeam({
teamId,
memberIdOrEmail,
}: {
teamId: string;
memberIdOrEmail: string;
}) {
async function handleAdd() {
const result = await authClient.organization.addTeamMember({
teamId,
memberIdOrEmail,
});
if (result.error) {
console.error('Add failed:', result.error);
return;
}
alert('Member added to team!');
}
return <button onClick={handleAdd}>Add to Team</button>;
}Remove Member from Team
'use client';
import { authClient } from '~/lib/auth/client';
export function RemoveMemberFromTeam({
teamId,
memberIdOrEmail,
}: {
teamId: string;
memberIdOrEmail: string;
}) {
async function handleRemove() {
const result = await authClient.organization.removeTeamMember({
teamId,
memberIdOrEmail,
});
if (result.error) {
console.error('Remove failed:', result.error);
return;
}
alert('Member removed from team');
}
return <button onClick={handleRemove}>Remove from Team</button>;
}View Team Members
'use client';
import { authClient } from '~/lib/auth/client';
import { useEffect, useState } from 'react';
export function TeamMembers({ teamId }: { teamId: string }) {
const [members, setMembers] = useState<any[]>([]);
useEffect(() => {
loadMembers();
}, []);
async function loadMembers() {
const result = await authClient.organization.listTeamMembers({
teamId,
});
if (result.data) {
setMembers(result.data);
}
}
return (
<div>
<h3>Team Members</h3>
<ul>
{members.map((member) => (
<li key={member.id}>
{member.user.name} - {member.role}
</li>
))}
</ul>
</div>
);
}Delete Team
'use client';
import { authClient } from '~/lib/auth/client';
export function DeleteTeam({ teamId }: { teamId: string }) {
async function handleDelete() {
const confirmed = window.confirm('Are you sure you want to delete this team?');
if (!confirmed) return;
const result = await authClient.organization.deleteTeam({
teamId,
});
if (result.error) {
console.error('Delete failed:', result.error);
return;
}
alert('Team deleted');
}
return <button onClick={handleDelete}>Delete Team</button>;
}Permission System
ProductReady implements role-based access control (RBAC), supporting fine-grained permission management.
Role & Permission Matrix
| Resource | Owner | Admin | Member | Viewer |
|---|---|---|---|---|
| Workspace | ||||
| View settings | ✅ | ✅ | ✅ | ✅ |
| Update settings | ✅ | ✅ | ❌ | ❌ |
| Delete workspace | ✅ | ❌ | ❌ | ❌ |
| Member Management | ||||
| Invite members | ✅ | ✅ | ❌ | ❌ |
| Remove members | ✅ | ✅ | ❌ | ❌ |
| Change roles | ✅ | ✅ | ❌ | ❌ |
| Team Management | ||||
| Create teams | ✅ | ✅ | ❌ | ❌ |
| Delete teams | ✅ | ✅ | ❌ | ❌ |
| Manage team members | ✅ | ✅ | ❌ | ❌ |
| Projects | ||||
| Create projects | ✅ | ✅ | ❌ | ❌ |
| View projects | ✅ | ✅ | ✅ | ✅ |
| Edit projects | ✅ | ✅ | ✅ | ❌ |
| Delete projects | ✅ | ✅ | ❌ | ❌ |
| Billing | ||||
| View billing | ✅ | ✅ | ❌ | ❌ |
| Update billing | ✅ | ❌ | ❌ | ❌ |
Check Permissions in Code
Use the usePermissions hook to check permissions in React components:
'use client';
import { usePermissions } from '~/lib/auth/client';
export function ProjectActions() {
const { can, canManage, role, isLoading } = usePermissions();
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
<p>Current role: {role}</p>
{can('project', 'create') && (
<button>Create Project</button>
)}
{can('issue', 'assign') && (
<button>Assign Task</button>
)}
{canManage && (
<button>Manage Settings</button>
)}
</div>
);
}Check multiple permissions:
// Check if any permission is satisfied
const hasAnyPermission = canAny([
{ resource: 'project', action: 'create' },
{ resource: 'project', action: 'update' },
]);
// Check if all permissions are satisfied
const hasAllPermissions = canAll([
{ resource: 'member', action: 'create' },
{ resource: 'member', action: 'delete' },
]);Server-side Permission Checks
Check permissions in tRPC routes or API routes:
import { protectedProcedure } from '~/lib/trpc';
import { hasPermission } from '~/lib/auth/permissions';
import { TRPCError } from '@trpc/server';
export const projectsRouter = createTRPCRouter({
create: protectedProcedure
.input(z.object({ name: z.string() }))
.mutation(async ({ ctx, input }) => {
const member = await ctx.auth.organization.getActiveMember();
const role = member?.role;
// Check permission
if (!hasPermission(role as Role, 'project', 'create')) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'No permission to create projects',
});
}
// Create project
const project = await ctx.db.project.create({
data: {
name: input.name,
organizationId: member.organizationId,
},
});
return project;
}),
});Workspace Settings
Open Workspace Settings
Method 1: Click the "Settings" button in the sidebar footer
Method 2: Trigger settings modal in code
'use client';
import { SettingsModal } from '~/components/dashboard/settings-modal';
import { useState } from 'react';
export function OpenSpaceSettings() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>
Workspace Settings
</button>
<SettingsModal
open={isOpen}
onOpenChange={setIsOpen}
defaultSection="space"
defaultTab="general"
/>
</>
);
}General Settings
In the "General" tab, you can:
- View and edit workspace name
- View and edit workspace slug (for URL)
- View creation time and member count
- Delete workspace (only visible to owners)
Billing Settings
In the "Billing" tab, you can:
- View current plan and usage
- Upgrade or downgrade plan
- Manage payment methods
- View billing history
Permission Note: Only workspace owners can modify billing information; admins can only view.
Best Practices
✅ Do
-
Clear Role Assignment
- Only give Owner role to few people (usually 1-2)
- Give Admin role to those who need management permissions
- Most members should use Member role
- External collaborators should use Viewer role
-
Use Teams to Organize Members
- Create teams by department or project
- Use teams for permission grouping
- Easier batch member management
-
Regular Member Review
- Periodically check member list
- Remove inactive members
- Update member roles to match actual responsibilities
-
Secure Offboarding Process
- Immediately remove access when employees leave
- Check and cancel their created invitations
- Change API keys if necessary
❌ Don't
-
Don't Abuse Owner Role
- Owner has permission to delete workspace
- Should only be assigned to those who truly need it
-
Don't Ignore Pending Invitations
- Regularly clean up expired or invalid invitations
- Avoid security risks
-
Don't Delete Workspace Without Backup
- Deletion is irreversible
- Ensure important data is backed up
FAQ
How to Transfer Workspace Ownership?
Currently requires manual transfer through these steps:
- Set new owner as Admin
- Original and new owner coordinate transfer
- Contact technical support if help is needed
Is There a Limit on Member Count?
Depends on your plan:
- Free: Up to 3 members
- Pro: Up to 10 members
- Enterprise: Unlimited
How to Bulk Invite Members?
Currently only supports individual invitations. Bulk invitation feature is in development.
Can I Rejoin After Leaving a Workspace?
Yes, but you need to receive a new invitation. Leaving doesn't delete your previous contribution records.
Troubleshooting
Cannot Invite Members
Possible Causes:
- You don't have invite permissions (need Admin or Owner)
- Email format is incorrect
- Reached plan member limit
- User is already a member
Solutions:
- Check your role permissions
- Verify email format
- Upgrade plan or remove inactive members
Member List Not Displaying
Possible Causes:
- Network issues
- Insufficient permissions
- Data loading error
Solutions:
- Refresh page
- Check console errors
- Contact technical support
Cannot Delete Workspace
Possible Causes:
- You're not the Owner
- Entered workspace name doesn't match
Solutions:
- Confirm you have Owner role
- Accurately enter workspace name (case-sensitive)
Next Steps
- Learn about authentication system
- Configure billing and plans
- Build APIs with tRPC
- Implement custom permissions
📚 More Resources: