diff --git a/packages/backend/server/schema.prisma b/packages/backend/server/schema.prisma
index 97daa338a43b7..7dcffd80577a8 100644
--- a/packages/backend/server/schema.prisma
+++ b/packages/backend/server/schema.prisma
@@ -141,7 +141,7 @@ model WorkspaceUserPermission {
   id          String                @id @default(uuid()) @db.VarChar
   workspaceId String                @map("workspace_id") @db.VarChar
   userId      String                @map("user_id") @db.VarChar
-  // Read/Write
+  // Workspace Role, Owner/Admin/Collaborator/External
   type        Int                   @db.SmallInt
   /// @deprecated Whether the permission invitation is accepted by the user
   accepted    Boolean               @default(false)
@@ -165,7 +165,7 @@ model WorkspacePageUserPermission {
   workspaceId String   @map("workspace_id") @db.VarChar
   pageId      String   @map("page_id") @db.VarChar
   userId      String   @map("user_id") @db.VarChar
-  // Read/Write
+  // External/Reader/Editor/Manager/Owner
   type        Int      @db.SmallInt
   /// Whether the permission invitation is accepted by the user
   accepted    Boolean  @default(false)
diff --git a/packages/backend/server/src/__tests__/models/page.spec.ts b/packages/backend/server/src/__tests__/models/page.spec.ts
index 1d3a35da5f697..5974c86facdcc 100644
--- a/packages/backend/server/src/__tests__/models/page.spec.ts
+++ b/packages/backend/server/src/__tests__/models/page.spec.ts
@@ -3,7 +3,8 @@ import { PrismaClient } from '@prisma/client';
 import ava, { TestFn } from 'ava';
 
 import { Config } from '../../base/config';
-import { Permission, PublicPageMode } from '../../models/common';
+import { WorkspaceRole } from '../../core/permission';
+import { PublicPageMode } from '../../models/common';
 import { PageModel } from '../../models/page';
 import { type User, UserModel } from '../../models/user';
 import { type Workspace, WorkspaceModel } from '../../models/workspace';
@@ -131,7 +132,7 @@ test('should grant a member to access a page', async t => {
     workspace.id,
     'page1',
     user.id,
-    Permission.Write
+    WorkspaceRole.Collaborator
   );
   t.false(hasAccess);
   // grant write permission
@@ -139,20 +140,20 @@ test('should grant a member to access a page', async t => {
     workspace.id,
     'page1',
     user.id,
-    Permission.Write
+    WorkspaceRole.Collaborator
   );
   hasAccess = await t.context.page.isMember(
     workspace.id,
     'page1',
     user.id,
-    Permission.Write
+    WorkspaceRole.Collaborator
   );
   t.true(hasAccess);
   hasAccess = await t.context.page.isMember(
     workspace.id,
     'page1',
     user.id,
-    Permission.Read
+    WorkspaceRole.Collaborator
   );
   t.true(hasAccess);
   // delete member
@@ -174,14 +175,14 @@ test('should change the page owner', async t => {
     workspace.id,
     'page1',
     user.id,
-    Permission.Owner
+    WorkspaceRole.Owner
   );
   t.true(
     await t.context.page.isMember(
       workspace.id,
       'page1',
       user.id,
-      Permission.Owner
+      WorkspaceRole.Owner
     )
   );
 
@@ -193,14 +194,14 @@ test('should change the page owner', async t => {
     workspace.id,
     'page1',
     otherUser.id,
-    Permission.Owner
+    WorkspaceRole.Owner
   );
   t.true(
     await t.context.page.isMember(
       workspace.id,
       'page1',
       otherUser.id,
-      Permission.Owner
+      WorkspaceRole.Owner
     )
   );
   t.false(
@@ -208,7 +209,7 @@ test('should change the page owner', async t => {
       workspace.id,
       'page1',
       user.id,
-      Permission.Owner
+      WorkspaceRole.Owner
     )
   );
 });
@@ -221,7 +222,7 @@ test('should not delete owner from page', async t => {
     workspace.id,
     'page1',
     user.id,
-    Permission.Owner
+    WorkspaceRole.Owner
   );
   const count = await t.context.page.deleteMember(
     workspace.id,
diff --git a/packages/backend/server/src/__tests__/models/user.spec.ts b/packages/backend/server/src/__tests__/models/user.spec.ts
index a12d5f0641473..0947ce40412a8 100644
--- a/packages/backend/server/src/__tests__/models/user.spec.ts
+++ b/packages/backend/server/src/__tests__/models/user.spec.ts
@@ -4,7 +4,7 @@ import ava, { TestFn } from 'ava';
 import Sinon from 'sinon';
 
 import { EmailAlreadyUsed, EventBus } from '../../base';
-import { Permission } from '../../models/common';
+import { WorkspaceRole } from '../../core/permission';
 import { UserModel } from '../../models/user';
 import { WorkspaceMemberStatus } from '../../models/workspace';
 import { createTestingModule, initTestingDB } from '../utils';
@@ -263,7 +263,7 @@ test('should trigger user.deleted event', async t => {
             public: false,
           },
         },
-        type: Permission.Owner,
+        type: WorkspaceRole.Owner,
         status: WorkspaceMemberStatus.Accepted,
       },
     },
diff --git a/packages/backend/server/src/__tests__/models/workspace.spec.ts b/packages/backend/server/src/__tests__/models/workspace.spec.ts
index fccb5acf0afdd..32ae94637f3d4 100644
--- a/packages/backend/server/src/__tests__/models/workspace.spec.ts
+++ b/packages/backend/server/src/__tests__/models/workspace.spec.ts
@@ -4,7 +4,7 @@ import ava, { TestFn } from 'ava';
 import Sinon from 'sinon';
 
 import { Config, EventBus } from '../../base';
-import { Permission } from '../../models/common';
+import { WorkspaceRole } from '../../core/permission';
 import { UserModel } from '../../models/user';
 import { WorkspaceModel } from '../../models/workspace';
 import { createTestingModule, initTestingDB } from '../utils';
@@ -92,25 +92,25 @@ test('should workspace owner has all permissions', async t => {
   let allowed = await t.context.workspace.isMember(
     workspace.id,
     user.id,
-    Permission.Owner
+    WorkspaceRole.Owner
   );
   t.is(allowed, true);
   allowed = await t.context.workspace.isMember(
     workspace.id,
     user.id,
-    Permission.Admin
+    WorkspaceRole.Admin
   );
   t.is(allowed, true);
   allowed = await t.context.workspace.isMember(
     workspace.id,
     user.id,
-    Permission.Write
+    WorkspaceRole.Collaborator
   );
   t.is(allowed, true);
   allowed = await t.context.workspace.isMember(
     workspace.id,
     user.id,
-    Permission.Read
+    WorkspaceRole.Collaborator
   );
   t.is(allowed, true);
 });
@@ -127,32 +127,32 @@ test('should workspace admin has all permissions except owner', async t => {
     data: {
       workspaceId: workspace.id,
       userId: otherUser.id,
-      type: Permission.Admin,
+      type: WorkspaceRole.Admin,
       status: WorkspaceMemberStatus.Accepted,
     },
   });
   let allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Owner
+    WorkspaceRole.Owner
   );
   t.is(allowed, false);
   allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Admin
+    WorkspaceRole.Admin
   );
   t.is(allowed, true);
   allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Write
+    WorkspaceRole.Collaborator
   );
   t.is(allowed, true);
   allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Read
+    WorkspaceRole.Collaborator
   );
   t.is(allowed, true);
 });
@@ -169,32 +169,32 @@ test('should workspace write has write and read permissions', async t => {
     data: {
       workspaceId: workspace.id,
       userId: otherUser.id,
-      type: Permission.Write,
+      type: WorkspaceRole.Collaborator,
       status: WorkspaceMemberStatus.Accepted,
     },
   });
   let allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Owner
+    WorkspaceRole.Owner
   );
   t.is(allowed, false);
   allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Admin
+    WorkspaceRole.Admin
   );
   t.is(allowed, false);
   allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Write
+    WorkspaceRole.Collaborator
   );
   t.is(allowed, true);
   allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Read
+    WorkspaceRole.Collaborator
   );
   t.is(allowed, true);
 });
@@ -211,32 +211,26 @@ test('should workspace read has read permission only', async t => {
     data: {
       workspaceId: workspace.id,
       userId: otherUser.id,
-      type: Permission.Read,
+      type: WorkspaceRole.Collaborator,
       status: WorkspaceMemberStatus.Accepted,
     },
   });
   let allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Owner
+    WorkspaceRole.Owner
   );
   t.is(allowed, false);
   allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Admin
+    WorkspaceRole.Admin
   );
   t.is(allowed, false);
   allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Write
-  );
-  t.is(allowed, false);
-  allowed = await t.context.workspace.isMember(
-    workspace.id,
-    otherUser.id,
-    Permission.Read
+    WorkspaceRole.Collaborator
   );
   t.is(allowed, true);
 });
@@ -252,25 +246,25 @@ test('should user not in workspace has no permissions', async t => {
   let allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Owner
+    WorkspaceRole.Owner
   );
   t.is(allowed, false);
   allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Admin
+    WorkspaceRole.Admin
   );
   t.is(allowed, false);
   allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Write
+    WorkspaceRole.Collaborator
   );
   t.is(allowed, false);
   allowed = await t.context.workspace.isMember(
     workspace.id,
     otherUser.id,
-    Permission.Read
+    WorkspaceRole.Collaborator
   );
   t.is(allowed, false);
 });
@@ -313,7 +307,7 @@ test('should grant member with read permission and Pending status by default', a
   );
   t.is(member1.workspaceId, workspace.id);
   t.is(member1.userId, otherUser.id);
-  t.is(member1.type, Permission.Read);
+  t.is(member1.type, WorkspaceRole.Collaborator);
   t.is(member1.status, WorkspaceMemberStatus.Pending);
 
   // grant again should do nothing
@@ -344,18 +338,18 @@ test('should grant Pending status member to Accepted status', async t => {
   );
   t.is(member1.workspaceId, workspace.id);
   t.is(member1.userId, otherUser.id);
-  t.is(member1.type, Permission.Read);
+  t.is(member1.type, WorkspaceRole.Collaborator);
   t.is(member1.status, WorkspaceMemberStatus.Pending);
 
   const member2 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Accepted
   );
   t.is(member2.workspaceId, workspace.id);
   t.is(member2.userId, otherUser.id);
-  t.is(member2.type, Permission.Read);
+  t.is(member2.type, WorkspaceRole.Collaborator);
   t.is(member2.status, WorkspaceMemberStatus.Accepted);
 });
 
@@ -370,27 +364,27 @@ test('should grant new owner and change exists owner to admin', async t => {
   const member1 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Accepted
   );
   t.is(member1.workspaceId, workspace.id);
   t.is(member1.userId, otherUser.id);
-  t.is(member1.type, Permission.Read);
+  t.is(member1.type, WorkspaceRole.Collaborator);
   t.is(member1.status, WorkspaceMemberStatus.Accepted);
 
   const member2 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Owner,
+    WorkspaceRole.Owner,
     WorkspaceMemberStatus.Accepted
   );
   t.is(member2.workspaceId, workspace.id);
   t.is(member2.userId, otherUser.id);
-  t.is(member2.type, Permission.Owner);
+  t.is(member2.type, WorkspaceRole.Owner);
   t.is(member2.status, WorkspaceMemberStatus.Accepted);
   // check old owner
   const owner = await t.context.workspace.getMember(workspace.id, user.id);
-  t.is(owner!.type, Permission.Admin);
+  t.is(owner!.type, WorkspaceRole.Admin);
   t.is(owner!.status, WorkspaceMemberStatus.Accepted);
 });
 
@@ -405,23 +399,23 @@ test('should grant write permission on exists member', async t => {
   const member1 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Accepted
   );
   t.is(member1.workspaceId, workspace.id);
   t.is(member1.userId, otherUser.id);
-  t.is(member1.type, Permission.Read);
+  t.is(member1.type, WorkspaceRole.Collaborator);
   t.is(member1.status, WorkspaceMemberStatus.Accepted);
 
   const member2 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Write,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Accepted
   );
   t.is(member2.workspaceId, workspace.id);
   t.is(member2.userId, otherUser.id);
-  t.is(member2.type, Permission.Write);
+  t.is(member2.type, WorkspaceRole.Collaborator);
   t.is(member2.status, WorkspaceMemberStatus.Accepted);
 });
 
@@ -436,23 +430,23 @@ test('should grant UnderReview status member to Accepted status', async t => {
   const member1 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.UnderReview
   );
   t.is(member1.workspaceId, workspace.id);
   t.is(member1.userId, otherUser.id);
-  t.is(member1.type, Permission.Read);
+  t.is(member1.type, WorkspaceRole.Collaborator);
   t.is(member1.status, WorkspaceMemberStatus.UnderReview);
 
   const member2 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Accepted
   );
   t.is(member2.workspaceId, workspace.id);
   t.is(member2.userId, otherUser.id);
-  t.is(member2.type, Permission.Read);
+  t.is(member2.type, WorkspaceRole.Collaborator);
   t.is(member2.status, WorkspaceMemberStatus.Accepted);
 });
 
@@ -467,23 +461,23 @@ test('should grant NeedMoreSeat status member to Pending status', async t => {
   const member1 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.NeedMoreSeat
   );
   t.is(member1.workspaceId, workspace.id);
   t.is(member1.userId, otherUser.id);
-  t.is(member1.type, Permission.Read);
+  t.is(member1.type, WorkspaceRole.Collaborator);
   t.is(member1.status, WorkspaceMemberStatus.NeedMoreSeat);
 
   const member2 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Pending
   );
   t.is(member2.workspaceId, workspace.id);
   t.is(member2.userId, otherUser.id);
-  t.is(member2.type, Permission.Read);
+  t.is(member2.type, WorkspaceRole.Collaborator);
   t.is(member2.status, WorkspaceMemberStatus.Pending);
 });
 
@@ -498,23 +492,23 @@ test('should grant NeedMoreSeatAndReview status member to UnderReview status', a
   const member1 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.NeedMoreSeatAndReview
   );
   t.is(member1.workspaceId, workspace.id);
   t.is(member1.userId, otherUser.id);
-  t.is(member1.type, Permission.Read);
+  t.is(member1.type, WorkspaceRole.Collaborator);
   t.is(member1.status, WorkspaceMemberStatus.NeedMoreSeatAndReview);
 
   const member2 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.UnderReview
   );
   t.is(member2.workspaceId, workspace.id);
   t.is(member2.userId, otherUser.id);
-  t.is(member2.type, Permission.Read);
+  t.is(member2.type, WorkspaceRole.Collaborator);
   t.is(member2.status, WorkspaceMemberStatus.UnderReview);
 });
 
@@ -532,19 +526,19 @@ test('should grant Pending status member to write permission and Accepted status
   );
   t.is(member1.workspaceId, workspace.id);
   t.is(member1.userId, otherUser.id);
-  t.is(member1.type, Permission.Read);
+  t.is(member1.type, WorkspaceRole.Collaborator);
   t.is(member1.status, WorkspaceMemberStatus.Pending);
 
   const member2 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Write,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Accepted
   );
   t.is(member2.workspaceId, workspace.id);
   t.is(member2.userId, otherUser.id);
   // TODO(fengmk2): fix this
-  // t.is(member2.type, Permission.Write);
+  // t.is(member2.type, WorkspaceRole.Collaborator);
   t.is(member2.status, WorkspaceMemberStatus.Accepted);
 });
 
@@ -559,23 +553,23 @@ test('should grant no thing on invalid status', async t => {
   const member1 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.NeedMoreSeat
   );
   t.is(member1.workspaceId, workspace.id);
   t.is(member1.userId, otherUser.id);
-  t.is(member1.type, Permission.Read);
+  t.is(member1.type, WorkspaceRole.Collaborator);
   t.is(member1.status, WorkspaceMemberStatus.NeedMoreSeat);
 
   const member2 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Accepted
   );
   t.is(member2.workspaceId, workspace.id);
   t.is(member2.userId, otherUser.id);
-  t.is(member2.type, Permission.Read);
+  t.is(member2.type, WorkspaceRole.Collaborator);
   t.is(member2.status, WorkspaceMemberStatus.NeedMoreSeat);
 });
 
@@ -590,7 +584,7 @@ test('should get the accepted status workspace member', async t => {
   await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Accepted
   );
   const member = await t.context.workspace.getMember(
@@ -599,7 +593,7 @@ test('should get the accepted status workspace member', async t => {
   );
   t.is(member!.workspaceId, workspace.id);
   t.is(member!.userId, otherUser.id);
-  t.is(member!.type, Permission.Read);
+  t.is(member!.type, WorkspaceRole.Collaborator);
   t.is(member!.status, WorkspaceMemberStatus.Accepted);
 });
 
@@ -614,7 +608,7 @@ test('should get any status workspace member, including pending and accepted', a
   await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Pending
   );
   const member = await t.context.workspace.getMemberInAnyStatus(
@@ -623,7 +617,7 @@ test('should get any status workspace member, including pending and accepted', a
   );
   t.is(member!.workspaceId, workspace.id);
   t.is(member!.userId, otherUser.id);
-  t.is(member!.type, Permission.Read);
+  t.is(member!.type, WorkspaceRole.Collaborator);
   t.is(member!.status, WorkspaceMemberStatus.Pending);
 });
 
@@ -635,7 +629,7 @@ test('should get workspace owner by workspace id', async t => {
   const owner = await t.context.workspace.getOwner(workspace.id);
   t.is(owner!.workspaceId, workspace.id);
   t.is(owner!.userId, user.id);
-  t.is(owner!.type, Permission.Owner);
+  t.is(owner!.type, WorkspaceRole.Owner);
   t.is(owner!.status, WorkspaceMemberStatus.Accepted);
   t.truthy(owner!.user);
   t.deepEqual(owner!.user, user);
@@ -658,27 +652,27 @@ test('should find workspace admin by workspace id', async t => {
   await t.context.workspace.grantMember(
     workspace.id,
     otherUser1.id,
-    Permission.Admin,
+    WorkspaceRole.Admin,
     WorkspaceMemberStatus.Accepted
   );
   await t.context.workspace.grantMember(
     workspace.id,
     otherUser2.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Accepted
   );
   // pending member should not be admin
   await t.context.workspace.grantMember(
     workspace.id,
     otherUser3.id,
-    Permission.Admin,
+    WorkspaceRole.Admin,
     WorkspaceMemberStatus.Pending
   );
   const members = await t.context.workspace.findAdmins(workspace.id);
   t.is(members.length, 1);
   t.is(members[0].workspaceId, workspace.id);
   t.is(members[0].userId, otherUser1.id);
-  t.is(members[0].type, Permission.Admin);
+  t.is(members[0].type, WorkspaceRole.Admin);
   t.is(members[0].status, WorkspaceMemberStatus.Accepted);
 });
 
@@ -710,13 +704,13 @@ test('should the workspace member total count, including pending and accepted',
   await t.context.workspace.grantMember(
     workspace.id,
     otherUser1.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Pending
   );
   await t.context.workspace.grantMember(
     workspace.id,
     otherUser2.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Accepted
   );
   const count = await t.context.workspace.getMemberTotalCount(workspace.id);
@@ -737,13 +731,13 @@ test('should the workspace member used count, only count the accepted member', a
   await t.context.workspace.grantMember(
     workspace.id,
     otherUser1.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Pending
   );
   await t.context.workspace.grantMember(
     workspace.id,
     otherUser2.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Accepted
   );
   const count = await t.context.workspace.getMemberUsedCount(workspace.id);
@@ -855,7 +849,7 @@ test('should delete workspace member in Pending, Accepted status', async t => {
   const member2 = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Accepted
   );
   t.is(member2.status, WorkspaceMemberStatus.Accepted);
@@ -874,7 +868,7 @@ test('should trigger workspace.members.requestDeclined event when delete workspa
   const member = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.UnderReview
   );
   t.is(member.status, WorkspaceMemberStatus.UnderReview);
@@ -919,7 +913,7 @@ test('should trigger workspace.members.requestDeclined event when delete workspa
   const member = await t.context.workspace.grantMember(
     workspace.id,
     otherUser.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.NeedMoreSeatAndReview
   );
   t.is(member.status, WorkspaceMemberStatus.NeedMoreSeatAndReview);
@@ -970,19 +964,19 @@ test('should refresh member seat status', async t => {
   await t.context.workspace.grantMember(
     workspace.id,
     otherUser1.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.NeedMoreSeatAndReview
   );
   await t.context.workspace.grantMember(
     workspace.id,
     otherUser2.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.Pending
   );
   await t.context.workspace.grantMember(
     workspace.id,
     otherUser3.id,
-    Permission.Read,
+    WorkspaceRole.Collaborator,
     WorkspaceMemberStatus.NeedMoreSeat
   );
   let count = await t.context.db.workspaceUserPermission.count({
@@ -1043,30 +1037,30 @@ test('should find the workspace members order by type:desc and createdAt:asc', a
     await t.context.workspace.grantMember(
       workspace.id,
       otherUser.id,
-      Permission.Read,
+      WorkspaceRole.Collaborator,
       WorkspaceMemberStatus.Accepted
     );
   }
   let members = await t.context.workspace.findMembers(workspace.id);
   t.is(members.length, 8);
-  t.is(members[0].type, Permission.Owner);
+  t.is(members[0].type, WorkspaceRole.Owner);
   t.is(members[0].status, WorkspaceMemberStatus.Accepted);
   for (let i = 1; i < 8; i++) {
-    t.is(members[i].type, Permission.Read);
+    t.is(members[i].type, WorkspaceRole.Collaborator);
     t.is(members[i].status, WorkspaceMemberStatus.Accepted);
   }
   members = await t.context.workspace.findMembers(workspace.id, { take: 100 });
   t.is(members.length, 11);
-  t.is(members[0].type, Permission.Owner);
+  t.is(members[0].type, WorkspaceRole.Owner);
   t.is(members[0].status, WorkspaceMemberStatus.Accepted);
   for (let i = 1; i < 11; i++) {
-    t.is(members[i].type, Permission.Read);
+    t.is(members[i].type, WorkspaceRole.Collaborator);
     t.is(members[i].status, WorkspaceMemberStatus.Accepted);
   }
   // skip should work
   members = await t.context.workspace.findMembers(workspace.id, { skip: 5 });
   t.is(members.length, 6);
-  t.is(members[0].type, Permission.Read);
+  t.is(members[0].type, WorkspaceRole.Collaborator);
 });
 
 test('should get the workspace member invitation', async t => {
diff --git a/packages/backend/server/src/__tests__/team.e2e.ts b/packages/backend/server/src/__tests__/team.e2e.ts
index 87c0c68c226ca..e48f3d50e1d45 100644
--- a/packages/backend/server/src/__tests__/team.e2e.ts
+++ b/packages/backend/server/src/__tests__/team.e2e.ts
@@ -13,7 +13,7 @@ import { AppModule } from '../app.module';
 import { EventBus } from '../base';
 import { AuthService } from '../core/auth';
 import { DocContentService } from '../core/doc-renderer';
-import { Permission, PermissionService } from '../core/permission';
+import { PermissionService, WorkspaceRole } from '../core/permission';
 import { QuotaManagementService, QuotaService, QuotaType } from '../core/quota';
 import { WorkspaceType } from '../core/workspaces';
 import {
@@ -29,7 +29,6 @@ import {
   inviteUser,
   inviteUsers,
   leaveWorkspace,
-  PermissionEnum,
   revokeInviteLink,
   revokeMember,
   revokeUser,
@@ -105,7 +104,7 @@ const init = async (
 
   const invite = async (
     email: string,
-    permission: PermissionEnum = 'Write',
+    permission: WorkspaceRole = WorkspaceRole.Collaborator,
     shouldSendEmail: boolean = false
   ) => {
     const member = await signUp(app, email.split('@')[0], email, '123456');
@@ -193,9 +192,12 @@ const init = async (
     ] as const;
   };
 
-  const admin = await invite(`${prefix}admin@affine.pro`, 'Admin');
+  const admin = await invite(`${prefix}admin@affine.pro`, WorkspaceRole.Admin);
   const write = await invite(`${prefix}write@affine.pro`);
-  const read = await invite(`${prefix}read@affine.pro`, 'Read');
+  const read = await invite(
+    `${prefix}read@affine.pro`,
+    WorkspaceRole.Collaborator
+  );
 
   return {
     invite,
@@ -268,7 +270,7 @@ test('should be able to check seat limit', async t => {
   {
     // invite
     await t.throwsAsync(
-      invite('member3@affine.pro', 'Read'),
+      invite('member3@affine.pro', WorkspaceRole.Collaborator),
       { message: 'You have exceeded your workspace member quota.' },
       'should throw error if exceed member limit'
     );
@@ -276,7 +278,7 @@ test('should be able to check seat limit', async t => {
       memberLimit: 5,
     });
     await t.notThrowsAsync(
-      invite('member4@affine.pro', 'Read'),
+      invite('member4@affine.pro', WorkspaceRole.Collaborator),
       'should not throw error if not exceed member limit'
     );
   }
@@ -324,17 +326,35 @@ test('should be able to grant team member permission', async t => {
   const { owner, teamWorkspace: ws, admin, write, read } = await init(app);
 
   await t.throwsAsync(
-    grantMember(app, read.token.token, ws.id, write.id, 'Write'),
+    grantMember(
+      app,
+      read.token.token,
+      ws.id,
+      write.id,
+      WorkspaceRole.Collaborator
+    ),
     { instanceOf: Error },
     'should throw error if not owner'
   );
   await t.throwsAsync(
-    grantMember(app, write.token.token, ws.id, read.id, 'Write'),
+    grantMember(
+      app,
+      write.token.token,
+      ws.id,
+      read.id,
+      WorkspaceRole.Collaborator
+    ),
     { instanceOf: Error },
     'should throw error if not owner'
   );
   await t.throwsAsync(
-    grantMember(app, admin.token.token, ws.id, read.id, 'Write'),
+    grantMember(
+      app,
+      admin.token.token,
+      ws.id,
+      read.id,
+      WorkspaceRole.Collaborator
+    ),
     { instanceOf: Error },
     'should throw error if not owner'
   );
@@ -342,15 +362,29 @@ test('should be able to grant team member permission', async t => {
   {
     // owner should be able to grant permission
     t.true(
-      await permissions.tryCheckWorkspaceIs(ws.id, read.id, Permission.Read),
+      await permissions.tryCheckWorkspaceIs(
+        ws.id,
+        read.id,
+        WorkspaceRole.Collaborator
+      ),
       'should be able to check permission'
     );
     t.truthy(
-      await grantMember(app, owner.token.token, ws.id, read.id, 'Admin'),
+      await grantMember(
+        app,
+        owner.token.token,
+        ws.id,
+        read.id,
+        WorkspaceRole.Admin
+      ),
       'should be able to grant permission'
     );
     t.true(
-      await permissions.tryCheckWorkspaceIs(ws.id, read.id, Permission.Admin),
+      await permissions.tryCheckWorkspaceIs(
+        ws.id,
+        read.id,
+        WorkspaceRole.Admin
+      ),
       'should be able to check permission'
     );
   }
@@ -692,17 +726,33 @@ test('should be able to emit events', async t => {
 
   {
     const { teamWorkspace: tws, owner, read } = await init(app);
-    await grantMember(app, owner.token.token, tws.id, read.id, 'Admin');
+    await grantMember(
+      app,
+      owner.token.token,
+      tws.id,
+      read.id,
+      WorkspaceRole.Admin
+    );
     t.deepEqual(
       event.emit.lastCall.args,
       [
         'workspace.members.roleChanged',
-        { userId: read.id, workspaceId: tws.id, permission: Permission.Admin },
+        {
+          userId: read.id,
+          workspaceId: tws.id,
+          permission: WorkspaceRole.Admin,
+        },
       ],
       'should emit role changed event'
     );
 
-    await grantMember(app, owner.token.token, tws.id, read.id, 'Owner');
+    await grantMember(
+      app,
+      owner.token.token,
+      tws.id,
+      read.id,
+      WorkspaceRole.Owner
+    );
     const [ownershipTransferred] = event.emit
       .getCalls()
       .map(call => call.args)
diff --git a/packages/backend/server/src/__tests__/utils/feature.ts b/packages/backend/server/src/__tests__/utils/feature.ts
index af728f6df075b..c345a9df8eb21 100644
--- a/packages/backend/server/src/__tests__/utils/feature.ts
+++ b/packages/backend/server/src/__tests__/utils/feature.ts
@@ -1,7 +1,7 @@
 import { Injectable } from '@nestjs/common';
 import { PrismaClient, WorkspaceMemberStatus } from '@prisma/client';
 
-import { Permission } from '../../core/permission';
+import { WorkspaceRole } from '../../core/permission';
 import { UserType } from '../../core/user/types';
 
 @Injectable()
@@ -14,7 +14,7 @@ export class WorkspaceResolverMock {
         public: false,
         permissions: {
           create: {
-            type: Permission.Owner,
+            type: WorkspaceRole.Owner,
             userId: user.id,
             accepted: true,
             status: WorkspaceMemberStatus.Accepted,
diff --git a/packages/backend/server/src/__tests__/utils/utils.ts b/packages/backend/server/src/__tests__/utils/utils.ts
index d1261af230bfd..cb507b4c2e880 100644
--- a/packages/backend/server/src/__tests__/utils/utils.ts
+++ b/packages/backend/server/src/__tests__/utils/utils.ts
@@ -1,4 +1,8 @@
-import { INestApplication, ModuleMetadata } from '@nestjs/common';
+import {
+  ConsoleLogger,
+  INestApplication,
+  ModuleMetadata,
+} from '@nestjs/common';
 import { APP_GUARD } from '@nestjs/core';
 import { Query, Resolver } from '@nestjs/graphql';
 import { Test, TestingModuleBuilder } from '@nestjs/testing';
@@ -15,8 +19,6 @@ import { AuthGuard, AuthModule } from '../../core/auth';
 import { UserFeaturesInit1698652531198 } from '../../data/migrations/1698652531198-user-features-init';
 import { ModelsModule } from '../../models';
 
-export type PermissionEnum = 'Owner' | 'Admin' | 'Write' | 'Read';
-
 async function flushDB(client: PrismaClient) {
   const result: { tablename: string }[] =
     await client.$queryRaw`SELECT tablename
@@ -133,8 +135,11 @@ export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) {
     cors: true,
     bodyParser: true,
     rawBody: true,
-    logger: ['fatal'],
   });
+  const logger = new ConsoleLogger();
+
+  logger.setLogLevels(['fatal']);
+  app.useLogger(logger);
 
   app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter()));
   app.use(
diff --git a/packages/backend/server/src/__tests__/utils/workspace.ts b/packages/backend/server/src/__tests__/utils/workspace.ts
index 0e5ae9acb87af..93765687c5811 100644
--- a/packages/backend/server/src/__tests__/utils/workspace.ts
+++ b/packages/backend/server/src/__tests__/utils/workspace.ts
@@ -1,9 +1,9 @@
 import type { INestApplication } from '@nestjs/common';
 import request from 'supertest';
 
+import { WorkspaceRole } from '../../core/permission/types';
 import type { WorkspaceType } from '../../core/workspaces';
 import { gql } from './common';
-import { PermissionEnum } from './utils';
 
 export async function createWorkspace(
   app: INestApplication,
@@ -157,7 +157,7 @@ export async function grantMember(
   token: string,
   workspaceId: string,
   userId: string,
-  permission: PermissionEnum
+  permission: WorkspaceRole
 ) {
   const res = await request(app.getHttpServer())
     .post(gql)
@@ -169,7 +169,7 @@ export async function grantMember(
             grantMember(
               workspaceId: "${workspaceId}"
               userId: "${userId}"
-              permission: ${permission}
+              permission: ${WorkspaceRole[permission]}
             )
           }
           `,
diff --git a/packages/backend/server/src/__tests__/workspace.e2e.ts b/packages/backend/server/src/__tests__/workspace.e2e.ts
index 0ffa0786cd125..2d269f6b95502 100644
--- a/packages/backend/server/src/__tests__/workspace.e2e.ts
+++ b/packages/backend/server/src/__tests__/workspace.e2e.ts
@@ -5,11 +5,13 @@ import ava from 'ava';
 import request from 'supertest';
 
 import { AppModule } from '../app.module';
+import { WorkspaceRole } from '../core/permission/types';
 import {
   acceptInviteById,
   createTestingApp,
   createWorkspace,
   getWorkspacePublicPages,
+  grantMember,
   inviteUser,
   publishPage,
   revokePublicPage,
@@ -116,7 +118,7 @@ test('should share a page', async t => {
   const msg1 = await publishPage(app, u2.token.token, 'not_exists_ws', 'page2');
   t.is(
     msg1,
-    'You do not have permission to access Space not_exists_ws.',
+    'You do not have permission to access doc page2 under Space not_exists_ws.',
     'unauthorized user can share page'
   );
   const msg2 = await revokePublicPage(
@@ -127,7 +129,7 @@ test('should share a page', async t => {
   );
   t.is(
     msg2,
-    'You do not have permission to access Space not_exists_ws.',
+    'You do not have permission to access doc page2 under Space not_exists_ws.',
     'unauthorized user can share page'
   );
 
@@ -136,6 +138,21 @@ test('should share a page', async t => {
     workspace.id,
     await inviteUser(app, u1.token.token, workspace.id, u2.email)
   );
+  const msg3 = await publishPage(app, u2.token.token, workspace.id, 'page2');
+  t.is(
+    msg3,
+    `You do not have permission to access doc page2 under Space ${workspace.id}.`,
+    'WorkspaceRole and PageRole is lower than required'
+  );
+
+  await grantMember(
+    app,
+    u1.token.token,
+    workspace.id,
+    u2.id,
+    WorkspaceRole.Admin
+  );
+
   const invited = await publishPage(app, u2.token.token, workspace.id, 'page2');
   t.is(invited.id, 'page2', 'failed to share page');
 
@@ -154,21 +171,21 @@ test('should share a page', async t => {
   t.is(pages2.length, 1, 'failed to get shared pages');
   t.is(pages2[0].id, 'page2', 'failed to get shared page: page2');
 
-  const msg3 = await revokePublicPage(
+  const msg4 = await revokePublicPage(
     app,
     u1.token.token,
     workspace.id,
     'page3'
   );
-  t.is(msg3, 'Page is not public');
+  t.is(msg4, 'Page is not public');
 
-  const msg4 = await revokePublicPage(
+  const revoked = await revokePublicPage(
     app,
     u1.token.token,
     workspace.id,
     'page2'
   );
-  t.false(msg4.public, 'failed to revoke page');
+  t.false(revoked.public, 'failed to revoke page');
   const page3 = await getWorkspacePublicPages(
     app,
     u1.token.token,
@@ -177,7 +194,7 @@ test('should share a page', async t => {
   t.is(page3.length, 0, 'failed to get shared pages');
 });
 
-test('should can get workspace doc', async t => {
+test('should be able to get workspace doc', async t => {
   const { app } = t.context;
   const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
   const u2 = await signUp(app, 'u2', 'u2@affine.pro', '2');
diff --git a/packages/backend/server/src/base/error/def.ts b/packages/backend/server/src/base/error/def.ts
index 4ad588e6d0d5a..2c1b85e3ea012 100644
--- a/packages/backend/server/src/base/error/def.ts
+++ b/packages/backend/server/src/base/error/def.ts
@@ -363,6 +363,11 @@ export const USER_FRIENDLY_ERRORS = {
   },
 
   // Workspace & Userspace & Doc & Sync errors
+  workspace_permission_not_found: {
+    type: 'internal_server_error',
+    args: { spaceId: 'string' },
+    message: ({ spaceId }) => `Space ${spaceId} permission not found.`,
+  },
   space_not_found: {
     type: 'resource_not_found',
     args: { spaceId: 'string' },
@@ -395,6 +400,11 @@ export const USER_FRIENDLY_ERRORS = {
     args: { spaceId: 'string' },
     message: ({ spaceId }) => `Owner of Space ${spaceId} not found.`,
   },
+  space_should_have_only_one_owner: {
+    type: 'invalid_input',
+    args: { spaceId: 'string' },
+    message: 'Space should have only one owner.',
+  },
   doc_not_found: {
     type: 'resource_not_found',
     args: { spaceId: 'string', docId: 'string' },
@@ -438,6 +448,24 @@ export const USER_FRIENDLY_ERRORS = {
     type: 'invalid_input',
     message: 'Expected to revoke a public page, not a Space.',
   },
+  expect_to_grant_doc_user_roles: {
+    type: 'invalid_input',
+    args: { spaceId: 'string', docId: 'string' },
+    message: ({ spaceId, docId }) =>
+      `Expect grant roles on doc ${docId} under Space ${spaceId}, not a Space.`,
+  },
+  expect_to_revoke_doc_user_roles: {
+    type: 'invalid_input',
+    args: { spaceId: 'string', docId: 'string' },
+    message: ({ spaceId, docId }) =>
+      `Expect revoke roles on doc ${docId} under Space ${spaceId}, not a Space.`,
+  },
+  expect_to_update_doc_user_role: {
+    type: 'invalid_input',
+    args: { spaceId: 'string', docId: 'string' },
+    message: ({ spaceId, docId }) =>
+      `Expect update roles on doc ${docId} under Space ${spaceId}, not a Space.`,
+  },
   page_is_not_public: {
     type: 'bad_request',
     message: 'Page is not public.',
diff --git a/packages/backend/server/src/base/error/errors.gen.ts b/packages/backend/server/src/base/error/errors.gen.ts
index 2d141f77abf61..e578cd9fc8151 100644
--- a/packages/backend/server/src/base/error/errors.gen.ts
+++ b/packages/backend/server/src/base/error/errors.gen.ts
@@ -191,6 +191,16 @@ export class EmailVerificationRequired extends UserFriendlyError {
   }
 }
 @ObjectType()
+class WorkspacePermissionNotFoundDataType {
+  @Field() spaceId!: string
+}
+
+export class WorkspacePermissionNotFound extends UserFriendlyError {
+  constructor(args: WorkspacePermissionNotFoundDataType, message?: string | ((args: WorkspacePermissionNotFoundDataType) => string)) {
+    super('internal_server_error', 'workspace_permission_not_found', message, args);
+  }
+}
+@ObjectType()
 class SpaceNotFoundDataType {
   @Field() spaceId!: string
 }
@@ -251,6 +261,16 @@ export class SpaceOwnerNotFound extends UserFriendlyError {
   }
 }
 @ObjectType()
+class SpaceShouldHaveOnlyOneOwnerDataType {
+  @Field() spaceId!: string
+}
+
+export class SpaceShouldHaveOnlyOneOwner extends UserFriendlyError {
+  constructor(args: SpaceShouldHaveOnlyOneOwnerDataType, message?: string | ((args: SpaceShouldHaveOnlyOneOwnerDataType) => string)) {
+    super('invalid_input', 'space_should_have_only_one_owner', message, args);
+  }
+}
+@ObjectType()
 class DocNotFoundDataType {
   @Field() spaceId!: string
   @Field() docId!: string
@@ -328,6 +348,39 @@ export class ExpectToRevokePublicPage extends UserFriendlyError {
     super('invalid_input', 'expect_to_revoke_public_page', message);
   }
 }
+@ObjectType()
+class ExpectToGrantDocUserRolesDataType {
+  @Field() spaceId!: string
+  @Field() docId!: string
+}
+
+export class ExpectToGrantDocUserRoles extends UserFriendlyError {
+  constructor(args: ExpectToGrantDocUserRolesDataType, message?: string | ((args: ExpectToGrantDocUserRolesDataType) => string)) {
+    super('invalid_input', 'expect_to_grant_doc_user_roles', message, args);
+  }
+}
+@ObjectType()
+class ExpectToRevokeDocUserRolesDataType {
+  @Field() spaceId!: string
+  @Field() docId!: string
+}
+
+export class ExpectToRevokeDocUserRoles extends UserFriendlyError {
+  constructor(args: ExpectToRevokeDocUserRolesDataType, message?: string | ((args: ExpectToRevokeDocUserRolesDataType) => string)) {
+    super('invalid_input', 'expect_to_revoke_doc_user_roles', message, args);
+  }
+}
+@ObjectType()
+class ExpectToUpdateDocUserRoleDataType {
+  @Field() spaceId!: string
+  @Field() docId!: string
+}
+
+export class ExpectToUpdateDocUserRole extends UserFriendlyError {
+  constructor(args: ExpectToUpdateDocUserRoleDataType, message?: string | ((args: ExpectToUpdateDocUserRoleDataType) => string)) {
+    super('invalid_input', 'expect_to_update_doc_user_role', message, args);
+  }
+}
 
 export class PageIsNotPublic extends UserFriendlyError {
   constructor(message?: string) {
@@ -679,12 +732,14 @@ export enum ErrorNames {
   ACTION_FORBIDDEN,
   ACCESS_DENIED,
   EMAIL_VERIFICATION_REQUIRED,
+  WORKSPACE_PERMISSION_NOT_FOUND,
   SPACE_NOT_FOUND,
   MEMBER_NOT_FOUND_IN_SPACE,
   NOT_IN_SPACE,
   ALREADY_IN_SPACE,
   SPACE_ACCESS_DENIED,
   SPACE_OWNER_NOT_FOUND,
+  SPACE_SHOULD_HAVE_ONLY_ONE_OWNER,
   DOC_NOT_FOUND,
   DOC_ACCESS_DENIED,
   VERSION_REJECTED,
@@ -693,6 +748,9 @@ export enum ErrorNames {
   BLOB_NOT_FOUND,
   EXPECT_TO_PUBLISH_PAGE,
   EXPECT_TO_REVOKE_PUBLIC_PAGE,
+  EXPECT_TO_GRANT_DOC_USER_ROLES,
+  EXPECT_TO_REVOKE_DOC_USER_ROLES,
+  EXPECT_TO_UPDATE_DOC_USER_ROLE,
   PAGE_IS_NOT_PUBLIC,
   FAILED_TO_SAVE_UPDATES,
   FAILED_TO_UPSERT_SNAPSHOT,
@@ -746,5 +804,5 @@ registerEnumType(ErrorNames, {
 export const ErrorDataUnionType = createUnionType({
   name: 'ErrorDataUnion',
   types: () =>
-    [QueryTooLongDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, DocNotFoundDataType, DocAccessDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseUpdateParamsDataType, WorkspaceMembersExceedLimitToDowngradeDataType] as const,
+    [QueryTooLongDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocAccessDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseUpdateParamsDataType, WorkspaceMembersExceedLimitToDowngradeDataType] as const,
 });
diff --git a/packages/backend/server/src/core/features/service.ts b/packages/backend/server/src/core/features/service.ts
index c6e961987debb..a5a9fec2ef1f1 100644
--- a/packages/backend/server/src/core/features/service.ts
+++ b/packages/backend/server/src/core/features/service.ts
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
 import { PrismaClient } from '@prisma/client';
 
 import { CannotDeleteAllAdminAccount } from '../../base';
-import { WorkspaceType } from '../workspaces/types';
+import { WorkspaceFeatureType } from '../workspaces/types';
 import { FeatureConfigType, getFeature } from './feature';
 import { FeatureKind, FeatureType } from './types';
 
@@ -20,7 +20,7 @@ export class FeatureService {
     if (data) {
       return getFeature(this.prisma, data.id) as Promise<FeatureConfigType<F>>;
     }
-    return undefined;
+    return;
   }
 
   // ======== User Features ========
@@ -315,7 +315,7 @@ export class FeatureService {
 
   async listWorkspacesByFeature(
     feature: FeatureType
-  ): Promise<WorkspaceType[]> {
+  ): Promise<WorkspaceFeatureType[]> {
     return this.prisma.workspaceFeature
       .findMany({
         where: {
@@ -335,7 +335,7 @@ export class FeatureService {
           },
         },
       })
-      .then(wss => wss.map(ws => ws.workspace as WorkspaceType));
+      .then(wss => wss.map(ws => ws.workspace));
   }
 
   async hasWorkspaceFeature(workspaceId: string, feature: FeatureType) {
diff --git a/packages/backend/server/src/core/permission/__tests__/__snapshots__/role.spec.ts.md b/packages/backend/server/src/core/permission/__tests__/__snapshots__/role.spec.ts.md
new file mode 100644
index 0000000000000..de420b5ae38e6
--- /dev/null
+++ b/packages/backend/server/src/core/permission/__tests__/__snapshots__/role.spec.ts.md
@@ -0,0 +1,665 @@
+# Snapshot report for `src/core/permission/__tests__/role.spec.ts`
+
+The actual snapshot is saved in `role.spec.ts.snap`.
+
+Generated by [AVA](https://avajs.dev).
+
+## should be able to get correct permissions from WorkspaceRole: External and DocRole: External
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: false,
+      Doc_Duplicate: false,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: false,
+      Doc_Publish: false,
+      Doc_Read: true,
+      Doc_Restore: false,
+      Doc_TransferOwner: false,
+      Doc_Trash: false,
+      Doc_Update: false,
+      Doc_Users_Manage: false,
+      Doc_Users_Read: false,
+      Workspace_CreateDoc: false,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: false,
+      Workspace_Properties_Delete: false,
+      Workspace_Properties_Read: false,
+      Workspace_Properties_Update: false,
+      Workspace_Settings_Read: false,
+      Workspace_Settings_Update: false,
+      Workspace_Sync: false,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: false,
+      Workspace_Users_Read: false,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: External and DocRole: Reader
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: false,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: false,
+      Doc_Publish: false,
+      Doc_Read: true,
+      Doc_Restore: false,
+      Doc_TransferOwner: false,
+      Doc_Trash: false,
+      Doc_Update: false,
+      Doc_Users_Manage: false,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: false,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: false,
+      Workspace_Properties_Delete: false,
+      Workspace_Properties_Read: false,
+      Workspace_Properties_Update: false,
+      Workspace_Settings_Read: false,
+      Workspace_Settings_Update: false,
+      Workspace_Sync: false,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: false,
+      Workspace_Users_Read: false,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: External and DocRole: Editor
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: false,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: false,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: false,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: false,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: false,
+      Workspace_Properties_Delete: false,
+      Workspace_Properties_Read: false,
+      Workspace_Properties_Update: false,
+      Workspace_Settings_Read: false,
+      Workspace_Settings_Update: false,
+      Workspace_Sync: false,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: false,
+      Workspace_Users_Read: false,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: External and DocRole: Manager
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: true,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: false,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: true,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: false,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: false,
+      Workspace_Properties_Delete: false,
+      Workspace_Properties_Read: false,
+      Workspace_Properties_Update: false,
+      Workspace_Settings_Read: false,
+      Workspace_Settings_Update: false,
+      Workspace_Sync: false,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: false,
+      Workspace_Users_Read: false,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: External and DocRole: Owner
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: true,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: true,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: true,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: false,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: false,
+      Workspace_Properties_Delete: false,
+      Workspace_Properties_Read: false,
+      Workspace_Properties_Update: false,
+      Workspace_Settings_Read: false,
+      Workspace_Settings_Update: false,
+      Workspace_Sync: false,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: false,
+      Workspace_Users_Read: false,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Collaborator and DocRole: External
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: false,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: false,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: false,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: false,
+      Workspace_Properties_Delete: false,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: false,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: false,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: false,
+      Workspace_Users_Read: true,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Collaborator and DocRole: Reader
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: false,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: false,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: false,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: false,
+      Workspace_Properties_Delete: false,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: false,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: false,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: false,
+      Workspace_Users_Read: true,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Collaborator and DocRole: Editor
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: false,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: false,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: false,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: false,
+      Workspace_Properties_Delete: false,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: false,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: false,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: false,
+      Workspace_Users_Read: true,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Collaborator and DocRole: Manager
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: true,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: false,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: true,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: false,
+      Workspace_Properties_Delete: false,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: false,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: false,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: false,
+      Workspace_Users_Read: true,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Collaborator and DocRole: Owner
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: true,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: true,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: true,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: false,
+      Workspace_Properties_Delete: false,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: false,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: false,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: false,
+      Workspace_Users_Read: true,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Admin and DocRole: External
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: true,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: false,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: true,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: true,
+      Workspace_Properties_Delete: true,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: true,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: true,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: true,
+      Workspace_Users_Read: true,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Admin and DocRole: Reader
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: true,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: false,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: true,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: true,
+      Workspace_Properties_Delete: true,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: true,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: true,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: true,
+      Workspace_Users_Read: true,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Admin and DocRole: Editor
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: true,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: false,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: true,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: true,
+      Workspace_Properties_Delete: true,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: true,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: true,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: true,
+      Workspace_Users_Read: true,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Admin and DocRole: Manager
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: true,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: false,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: true,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: true,
+      Workspace_Properties_Delete: true,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: true,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: true,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: true,
+      Workspace_Users_Read: true,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Admin and DocRole: Owner
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: true,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: true,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: true,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: false,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: true,
+      Workspace_Properties_Delete: true,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: true,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: true,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: false,
+      Workspace_Users_Manage: true,
+      Workspace_Users_Read: true,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Owner and DocRole: External
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: true,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: false,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: true,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: true,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: true,
+      Workspace_Properties_Delete: true,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: true,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: true,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: true,
+      Workspace_Users_Manage: true,
+      Workspace_Users_Read: true,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Owner and DocRole: Reader
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: true,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: false,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: true,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: true,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: true,
+      Workspace_Properties_Delete: true,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: true,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: true,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: true,
+      Workspace_Users_Manage: true,
+      Workspace_Users_Read: true,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Owner and DocRole: Editor
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: true,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: false,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: true,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: true,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: true,
+      Workspace_Properties_Delete: true,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: true,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: true,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: true,
+      Workspace_Users_Manage: true,
+      Workspace_Users_Read: true,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Owner and DocRole: Manager
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: true,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: false,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: true,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: true,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: true,
+      Workspace_Properties_Delete: true,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: true,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: true,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: true,
+      Workspace_Users_Manage: true,
+      Workspace_Users_Read: true,
+    }
+
+## should be able to get correct permissions from WorkspaceRole: Owner and DocRole: Owner
+
+> Snapshot 1
+
+    {
+      Doc_Copy: true,
+      Doc_Delete: true,
+      Doc_Duplicate: true,
+      Doc_Properties_Read: true,
+      Doc_Properties_Update: true,
+      Doc_Publish: true,
+      Doc_Read: true,
+      Doc_Restore: true,
+      Doc_TransferOwner: true,
+      Doc_Trash: true,
+      Doc_Update: true,
+      Doc_Users_Manage: true,
+      Doc_Users_Read: true,
+      Workspace_CreateDoc: true,
+      Workspace_Delete: true,
+      Workspace_Organize_Read: true,
+      Workspace_Properties_Create: true,
+      Workspace_Properties_Delete: true,
+      Workspace_Properties_Read: true,
+      Workspace_Properties_Update: true,
+      Workspace_Settings_Read: true,
+      Workspace_Settings_Update: true,
+      Workspace_Sync: true,
+      Workspace_TransferOwner: true,
+      Workspace_Users_Manage: true,
+      Workspace_Users_Read: true,
+    }
diff --git a/packages/backend/server/src/core/permission/__tests__/__snapshots__/role.spec.ts.snap b/packages/backend/server/src/core/permission/__tests__/__snapshots__/role.spec.ts.snap
new file mode 100644
index 0000000000000..78c71e36c5e42
Binary files /dev/null and b/packages/backend/server/src/core/permission/__tests__/__snapshots__/role.spec.ts.snap differ
diff --git a/packages/backend/server/src/core/permission/__tests__/role.spec.ts b/packages/backend/server/src/core/permission/__tests__/role.spec.ts
new file mode 100644
index 0000000000000..88f0b837da6fd
--- /dev/null
+++ b/packages/backend/server/src/core/permission/__tests__/role.spec.ts
@@ -0,0 +1,36 @@
+import test from 'ava';
+
+import { DocRole, WorkspaceRole } from '../index';
+import { Actions, ActionsKeys, mapRoleToActions } from '../types';
+
+// create a matrix representing the all possible permission of WorkspaceRole and DocRole
+const matrix = Object.values(WorkspaceRole)
+  .filter(r => typeof r !== 'string')
+  .flatMap(workspaceRole =>
+    Object.values(DocRole)
+      .filter(r => typeof r !== 'string')
+      .map(docRole => ({
+        workspaceRole,
+        docRole,
+      }))
+  );
+
+for (const { workspaceRole, docRole } of matrix) {
+  const permission = mapRoleToActions(workspaceRole, docRole);
+  test(`should be able to get correct permissions from WorkspaceRole: ${WorkspaceRole[workspaceRole]} and DocRole: ${DocRole[docRole]}`, t => {
+    t.snapshot(permission);
+  });
+}
+
+test('ActionsKeys value should be the same order of the Actions objects', t => {
+  for (const [index, value] of ActionsKeys.entries()) {
+    const [k, k1, k2] = value.split('.');
+    if (k2) {
+      // @ts-expect-error
+      t.is(Actions[k][k1][k2], index);
+    } else {
+      // @ts-expect-error
+      t.is(Actions[k][k1], index);
+    }
+  }
+});
diff --git a/packages/backend/server/src/core/permission/index.ts b/packages/backend/server/src/core/permission/index.ts
index 01f47d7c330c4..5747358780df2 100644
--- a/packages/backend/server/src/core/permission/index.ts
+++ b/packages/backend/server/src/core/permission/index.ts
@@ -9,4 +9,4 @@ import { PermissionService } from './service';
 export class PermissionModule {}
 
 export { PermissionService } from './service';
-export { Permission, PublicPageMode } from './types';
+export { DocRole, PublicPageMode, WorkspaceRole } from './types';
diff --git a/packages/backend/server/src/core/permission/service.ts b/packages/backend/server/src/core/permission/service.ts
index 489453c8776ee..0c9a628038ed2 100644
--- a/packages/backend/server/src/core/permission/service.ts
+++ b/packages/backend/server/src/core/permission/service.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, Logger } from '@nestjs/common';
 import type { Prisma } from '@prisma/client';
 import { PrismaClient, WorkspaceMemberStatus } from '@prisma/client';
 import { groupBy } from 'lodash-es';
@@ -8,11 +8,22 @@ import {
   EventBus,
   SpaceAccessDenied,
   SpaceOwnerNotFound,
+  SpaceShouldHaveOnlyOneOwner,
+  WorkspacePermissionNotFound,
 } from '../../base';
-import { Permission, PublicPageMode } from './types';
+import {
+  AllPossibleGraphQLDocActionsKeys,
+  DocRole,
+  findMinimalDocRole,
+  PublicPageMode,
+  requiredWorkspaceRoleByDocRole,
+  WorkspaceRole,
+} from './types';
 
 @Injectable()
 export class PermissionService {
+  private readonly logger = new Logger(PermissionService.name);
+
   constructor(
     private readonly prisma: PrismaClient,
     private readonly event: EventBus
@@ -30,7 +41,7 @@ export class PermissionService {
   }
 
   /// Start regin: workspace permission
-  async get(ws: string, user: string) {
+  async get(ws: string, user: string): Promise<WorkspaceRole> {
     const data = await this.prisma.workspaceUserPermission.findFirst({
       where: {
         workspaceId: ws,
@@ -39,7 +50,11 @@ export class PermissionService {
       },
     });
 
-    return data?.type as Permission;
+    if (!data) {
+      throw new WorkspacePermissionNotFound({ spaceId: ws });
+    }
+
+    return data.type;
   }
 
   /**
@@ -63,7 +78,7 @@ export class PermissionService {
       .findMany({
         where: {
           userId,
-          type: Permission.Owner,
+          type: WorkspaceRole.Owner,
           OR: this.acceptedCondition,
         },
         select: {
@@ -77,7 +92,7 @@ export class PermissionService {
     const owner = await this.prisma.workspaceUserPermission.findFirst({
       where: {
         workspaceId,
-        type: Permission.Owner,
+        type: WorkspaceRole.Owner,
       },
       include: {
         user: true,
@@ -95,7 +110,7 @@ export class PermissionService {
     const admin = await this.prisma.workspaceUserPermission.findMany({
       where: {
         workspaceId,
-        type: Permission.Admin,
+        type: WorkspaceRole.Admin,
       },
       include: {
         user: true,
@@ -117,7 +132,7 @@ export class PermissionService {
     return this.prisma.workspaceUserPermission.findFirst({
       where: {
         workspaceId,
-        type: Permission.Owner,
+        type: WorkspaceRole.Owner,
       },
       include: {
         user: true,
@@ -136,7 +151,7 @@ export class PermissionService {
     if (ws === id) {
       // if workspace is public or have any public page, then allow to access
       const [isPublicWorkspace, publicPages] = await Promise.all([
-        this.tryCheckWorkspace(ws, user, Permission.Read),
+        this.tryCheckWorkspace(ws, user, WorkspaceRole.Collaborator),
         this.prisma.workspacePage.count({
           where: {
             workspaceId: ws,
@@ -147,7 +162,7 @@ export class PermissionService {
       return isPublicWorkspace || publicPages > 0;
     }
 
-    return this.tryCheckPage(ws, id, user);
+    return this.tryCheckPage(ws, id, 'Doc_Read', user);
   }
 
   async getWorkspaceMemberStatus(ws: string, user: string) {
@@ -168,7 +183,7 @@ export class PermissionService {
   async isWorkspaceMember(
     ws: string,
     user: string,
-    permission: Permission = Permission.Read
+    permission: WorkspaceRole = WorkspaceRole.Collaborator
   ): Promise<boolean> {
     const count = await this.prisma.workspaceUserPermission.count({
       where: {
@@ -193,7 +208,7 @@ export class PermissionService {
   async checkCloudWorkspace(
     workspaceId: string,
     userId?: string,
-    permission: Permission = Permission.Read
+    permission: WorkspaceRole = WorkspaceRole.Collaborator
   ) {
     const hasWorkspace = await this.hasWorkspace(workspaceId);
     if (hasWorkspace) {
@@ -204,7 +219,7 @@ export class PermissionService {
   async checkWorkspace(
     ws: string,
     user?: string,
-    permission: Permission = Permission.Read
+    permission: WorkspaceRole = WorkspaceRole.Collaborator
   ) {
     if (!(await this.tryCheckWorkspace(ws, user, permission))) {
       throw new SpaceAccessDenied({ spaceId: ws });
@@ -214,10 +229,10 @@ export class PermissionService {
   async tryCheckWorkspace(
     ws: string,
     user?: string,
-    permission: Permission = Permission.Read
+    permission: WorkspaceRole = WorkspaceRole.Collaborator
   ) {
     // If the permission is read, we should check if the workspace is public
-    if (permission === Permission.Read) {
+    if (permission === WorkspaceRole.Collaborator) {
       const count = await this.prisma.workspace.count({
         where: { id: ws, public: true },
       });
@@ -242,7 +257,15 @@ export class PermissionService {
         },
       });
 
-      return count > 0;
+      if (count > 0) {
+        return true;
+      } else {
+        this.logger.log("User's WorkspaceRole is lower than required", {
+          workspaceId: ws,
+          userId: user,
+          requiredRole: WorkspaceRole[permission],
+        });
+      }
     }
 
     // unsigned in, workspace is not public
@@ -253,7 +276,7 @@ export class PermissionService {
   async checkWorkspaceIs(
     ws: string,
     user: string,
-    permission: Permission = Permission.Read
+    permission: WorkspaceRole = WorkspaceRole.Collaborator
   ) {
     if (!(await this.tryCheckWorkspaceIs(ws, user, permission))) {
       throw new SpaceAccessDenied({ spaceId: ws });
@@ -263,7 +286,7 @@ export class PermissionService {
   async tryCheckWorkspaceIs(
     ws: string,
     user: string,
-    permission: Permission = Permission.Read
+    permission: WorkspaceRole = WorkspaceRole.Collaborator
   ) {
     const count = await this.prisma.workspaceUserPermission.count({
       where: {
@@ -307,7 +330,7 @@ export class PermissionService {
   async grant(
     ws: string,
     user: string,
-    permission: Permission = Permission.Read,
+    permission: WorkspaceRole = WorkspaceRole.Collaborator,
     status: WorkspaceMemberStatus = WorkspaceMemberStatus.Pending
   ): Promise<string> {
     const data = await this.prisma.workspaceUserPermission.findFirst({
@@ -315,7 +338,7 @@ export class PermissionService {
     });
 
     if (data) {
-      const toBeOwner = permission === Permission.Owner;
+      const toBeOwner = permission === WorkspaceRole.Owner;
       if (data.accepted && data.status === WorkspaceMemberStatus.Accepted) {
         const [p] = await this.prisma.$transaction(
           [
@@ -331,10 +354,10 @@ export class PermissionService {
               ? this.prisma.workspaceUserPermission.updateMany({
                   where: {
                     workspaceId: ws,
-                    type: Permission.Owner,
+                    type: WorkspaceRole.Owner,
                     userId: { not: user },
                   },
-                  data: { type: Permission.Admin },
+                  data: { type: WorkspaceRole.Admin },
                 })
               : null,
           ].filter(Boolean) as Prisma.PrismaPromise<any>[]
@@ -441,7 +464,7 @@ export class PermissionService {
 
     // We shouldn't revoke owner permission
     // should auto deleted by workspace/user delete cascading
-    if (!permission || permission.type === Permission.Owner) {
+    if (!permission || permission.type === WorkspaceRole.Owner) {
       return false;
     }
 
@@ -490,22 +513,22 @@ export class PermissionService {
   async checkCloudPagePermission(
     workspaceId: string,
     pageId: string,
-    userId?: string,
-    permission = Permission.Read
+    action: AllPossibleGraphQLDocActionsKeys,
+    userId?: string
   ) {
     const hasWorkspace = await this.hasWorkspace(workspaceId);
     if (hasWorkspace) {
-      await this.checkPagePermission(workspaceId, pageId, userId, permission);
+      await this.checkPagePermission(workspaceId, pageId, action, userId);
     }
   }
 
   async checkPagePermission(
     ws: string,
     page: string,
-    user?: string,
-    permission = Permission.Read
+    action: AllPossibleGraphQLDocActionsKeys,
+    user?: string
   ) {
-    if (!(await this.tryCheckPage(ws, page, user, permission))) {
+    if (!(await this.tryCheckPage(ws, page, action, user))) {
       throw new DocAccessDenied({ spaceId: ws, docId: page });
     }
   }
@@ -513,11 +536,12 @@ export class PermissionService {
   async tryCheckPage(
     ws: string,
     page: string,
-    user?: string,
-    permission = Permission.Read
+    action: AllPossibleGraphQLDocActionsKeys,
+    user?: string
   ) {
+    const role = findMinimalDocRole(action);
     // check whether page is public
-    if (permission === Permission.Read) {
+    if (action === 'Doc_Read') {
       const count = await this.prisma.workspacePage.count({
         where: {
           workspaceId: ws,
@@ -541,7 +565,7 @@ export class PermissionService {
           userId: user,
           accepted: true,
           type: {
-            gte: permission,
+            gte: role,
           },
         },
       });
@@ -550,11 +574,23 @@ export class PermissionService {
       // accessible
       if (count > 0) {
         return true;
+      } else {
+        this.logger.log("User's PageRole is lower than required", {
+          workspaceId: ws,
+          pageId: page,
+          userId: user,
+          requiredRole: DocRole[role],
+          action,
+        });
       }
     }
 
     // check whether user has workspace related permission
-    return this.tryCheckWorkspace(ws, user, permission);
+    return this.tryCheckWorkspace(
+      ws,
+      user,
+      requiredWorkspaceRoleByDocRole(role)
+    );
   }
 
   async isPublicPage(ws: string, page: string) {
@@ -613,8 +649,8 @@ export class PermissionService {
     ws: string,
     page: string,
     user: string,
-    permission: Permission = Permission.Read
-  ) {
+    permission: DocRole
+  ): Promise<string> {
     const data = await this.prisma.workspacePageUserPermission.findFirst({
       where: {
         workspaceId: ws,
@@ -637,18 +673,18 @@ export class PermissionService {
           }),
 
           // If the new permission is owner, we need to revoke old owner
-          permission === Permission.Owner
+          permission === DocRole.Owner
             ? this.prisma.workspacePageUserPermission.updateMany({
                 where: {
                   workspaceId: ws,
                   pageId: page,
-                  type: Permission.Owner,
+                  type: DocRole.Owner,
                   userId: {
                     not: user,
                   },
                 },
                 data: {
-                  type: Permission.Admin,
+                  type: DocRole.Manager,
                 },
               })
             : null,
@@ -670,20 +706,93 @@ export class PermissionService {
       .then(p => p.id);
   }
 
-  async revokePage(ws: string, page: string, user: string) {
+  async revokePage(ws: string, page: string, users: string[]) {
     const result = await this.prisma.workspacePageUserPermission.deleteMany({
       where: {
         workspaceId: ws,
         pageId: page,
-        userId: user,
+        userId: {
+          in: users,
+        },
         type: {
           // We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading
-          not: Permission.Owner,
+          not: DocRole.Owner,
         },
       },
     });
 
     return result.count > 0;
   }
-  /// End regin: page permission
+
+  async grantPagePermission(
+    workspaceId: string,
+    pageId: string,
+    userIds: string[],
+    role: DocRole
+  ) {
+    if (userIds.length === 0) {
+      return [];
+    }
+    if (role === DocRole.Owner) {
+      if (userIds.length > 1) {
+        throw new SpaceShouldHaveOnlyOneOwner({ spaceId: workspaceId });
+      }
+      return [await this.grantPage(workspaceId, pageId, userIds[0], role)];
+    }
+
+    const ret = await this.prisma.$transaction(async tx =>
+      Promise.all(
+        userIds.map(id =>
+          tx.workspacePageUserPermission.upsert({
+            where: {
+              workspaceId_pageId_userId: {
+                workspaceId,
+                pageId,
+                userId: id,
+              },
+            },
+            create: {
+              workspaceId,
+              pageId,
+              userId: id,
+              type: role,
+            },
+            update: {
+              type: role,
+            },
+          })
+        )
+      )
+    );
+    return ret.map(p => p.id);
+  }
+
+  async updatePagePermission(
+    workspaceId: string,
+    pageId: string,
+    userId: string,
+    role: DocRole
+  ) {
+    const permission = await this.prisma.workspacePageUserPermission.findFirst({
+      where: {
+        workspaceId,
+        pageId,
+        userId,
+      },
+    });
+
+    if (!permission) {
+      return this.grantPage(workspaceId, pageId, userId, role);
+    }
+
+    const { id } = await this.prisma.workspacePageUserPermission.update({
+      where: {
+        id: permission.id,
+      },
+      data: {
+        type: role,
+      },
+    });
+    return id;
+  }
 }
diff --git a/packages/backend/server/src/core/permission/types.ts b/packages/backend/server/src/core/permission/types.ts
index 05ec00c2b524f..b34588ead55d5 100644
--- a/packages/backend/server/src/core/permission/types.ts
+++ b/packages/backend/server/src/core/permission/types.ts
@@ -1,11 +1,328 @@
-export enum Permission {
-  Read = 0,
-  Write = 1,
-  Admin = 10,
-  Owner = 99,
-}
+import assert from 'node:assert';
 
 export enum PublicPageMode {
   Page,
   Edgeless,
 }
+
+export enum DocRole {
+  External = 0,
+  Reader = 10,
+  Editor = 20,
+  Manager = 30,
+  Owner = 99,
+}
+
+export enum WorkspaceRole {
+  External = -99,
+  Collaborator = 1,
+  Admin = 10,
+  Owner = 99,
+}
+
+export const Actions = {
+  Workspace: {
+    Sync: 1,
+    CreateDoc: 2,
+    Delete: 11,
+    TransferOwner: 12,
+    Organize: {
+      Read: 0,
+    },
+    Users: {
+      Read: 3,
+      Manage: 6,
+    },
+    Properties: {
+      Read: 4,
+      Create: 8,
+      Update: 9,
+      Delete: 10,
+    },
+    Settings: {
+      Read: 5,
+      Update: 7,
+    },
+  },
+  Doc: {
+    Read: 13,
+    Copy: 14,
+    Duplicate: 17,
+    Trash: 18,
+    Restore: 19,
+    Delete: 20,
+    Update: 22,
+    Publish: 23,
+    TransferOwner: 25,
+    Properties: {
+      Read: 15,
+      Update: 21,
+    },
+    Users: {
+      Read: 16,
+      Manage: 24,
+    },
+  },
+} as const;
+
+type ActionsKeysUnion = typeof Actions extends {
+  [k in infer _K extends string]: infer _V;
+}
+  ? _V extends {
+      [k1 in infer _K1 extends string]: infer _V1;
+    }
+    ? _V1 extends {
+        [k2 in infer _K2 extends string]: number;
+      }
+      ? _K1 extends keyof (typeof Actions)[_K]
+        ? _K2 extends keyof (typeof Actions)[_K][_K1]
+          ? `${_K}.${_K1}.${_K2}`
+          : never
+        : never
+      : _V1 extends number
+        ? `${_K}.${_K1}`
+        : never
+    : never
+  : never;
+
+type ExcludeObjectKeys<
+  T,
+  Key extends keyof typeof Actions,
+  Split extends string,
+> = T extends `${infer _K extends Key}.${infer _K1}.${infer _K2}`
+  ? _K1 extends keyof (typeof Actions)[_K]
+    ? _K2 extends keyof (typeof Actions)[_K][_K1]
+      ? `${_K}${Split}${_K1}${Split}${_K2}`
+      : never
+    : never
+  : T extends `${infer _K extends Key}.${infer _K1}`
+    ? _K1 extends keyof (typeof Actions)[_K]
+      ? (typeof Actions)[_K][_K1] extends number
+        ? `${_K}${Split}${_K1}`
+        : never
+      : never
+    : never;
+
+export type AllPossibleActionsKeys = ExcludeObjectKeys<
+  ActionsKeysUnion,
+  keyof typeof Actions,
+  '.'
+>;
+
+export type AllPossibleGraphQLWorkspaceActionsKeys = ExcludeObjectKeys<
+  ActionsKeysUnion,
+  'Workspace',
+  '_'
+>;
+export type AllPossibleGraphQLDocActionsKeys = ExcludeObjectKeys<
+  ActionsKeysUnion,
+  'Doc',
+  '_'
+>;
+
+type AllPossibleGraphQLActionsKeys =
+  | AllPossibleGraphQLWorkspaceActionsKeys
+  | AllPossibleGraphQLDocActionsKeys;
+
+export const ActionsKeys: AllPossibleActionsKeys[] = [
+  'Workspace.Organize.Read',
+  'Workspace.Sync',
+  'Workspace.CreateDoc',
+  'Workspace.Users.Read',
+  'Workspace.Properties.Read',
+  'Workspace.Settings.Read',
+  'Workspace.Users.Manage',
+  'Workspace.Settings.Update',
+  'Workspace.Properties.Create',
+  'Workspace.Properties.Update',
+  'Workspace.Properties.Delete',
+  'Workspace.Delete',
+  'Workspace.TransferOwner',
+  'Doc.Read',
+  'Doc.Copy',
+  'Doc.Properties.Read',
+  'Doc.Users.Read',
+  'Doc.Duplicate',
+  'Doc.Trash',
+  'Doc.Restore',
+  'Doc.Delete',
+  'Doc.Properties.Update',
+  'Doc.Update',
+  'Doc.Publish',
+  'Doc.Users.Manage',
+  'Doc.TransferOwner',
+] as const;
+
+assert(
+  ActionsKeys.length === Actions.Doc.TransferOwner + 1,
+  'ActionsKeys length is not correct'
+);
+
+function permissionKeyToGraphQLKey(key: string) {
+  const k = key.split('.');
+  return k.join('_') as keyof PermissionsList;
+}
+
+const DefaultActionsMap = Object.fromEntries(
+  ActionsKeys.map(key => [permissionKeyToGraphQLKey(key), false])
+) as PermissionsList;
+
+export type WorkspacePermissionsList = {
+  [k in AllPossibleGraphQLWorkspaceActionsKeys]: boolean;
+};
+
+export type PermissionsList = {
+  [key in AllPossibleGraphQLActionsKeys]: boolean;
+};
+
+export function mapWorkspaceRoleToWorkspaceActions(
+  workspaceRole: WorkspaceRole
+) {
+  const permissionList = { ...DefaultActionsMap };
+  (RoleActionsMap.WorkspaceRole[workspaceRole] ?? []).forEach(action => {
+    permissionList[permissionKeyToGraphQLKey(ActionsKeys[action])] = true;
+  });
+  return Object.fromEntries(
+    Object.entries(permissionList).filter(([k, _]) =>
+      k.startsWith('Workspace_')
+    )
+  );
+}
+
+export function mapRoleToActions(
+  workspaceRole?: WorkspaceRole,
+  docRole?: DocRole
+) {
+  const workspaceActions = workspaceRole
+    ? (RoleActionsMap.WorkspaceRole[workspaceRole] ?? [])
+    : [];
+  const docActions = (function () {
+    // Doc owner/manager permission can not be overridden by workspace role
+    if (docRole !== undefined && docRole >= DocRole.Manager) {
+      return RoleActionsMap.DocRole[docRole];
+    }
+    switch (workspaceRole) {
+      case WorkspaceRole.Admin:
+      case WorkspaceRole.Owner:
+        return RoleActionsMap.DocRole[DocRole.Manager];
+      case WorkspaceRole.Collaborator:
+        return RoleActionsMap.DocRole[DocRole.Editor];
+      default:
+        return docRole !== undefined
+          ? (RoleActionsMap.DocRole[docRole] ?? [])
+          : [];
+    }
+  })();
+  const permissionList = { ...DefaultActionsMap };
+  [...workspaceActions, ...docActions].forEach(action => {
+    permissionList[permissionKeyToGraphQLKey(ActionsKeys[action])] = true;
+  });
+  return permissionList;
+}
+
+export function findMinimalDocRole(
+  action: AllPossibleGraphQLDocActionsKeys
+): DocRole {
+  const [_, actionKey, actionKey2] = action.split('_');
+
+  const actionValue: number = actionKey2
+    ? // @ts-expect-error Actions[actionKey] exists
+      Actions.Doc[actionKey][actionKey2]
+    : // @ts-expect-error Actions[actionKey] exists
+      Actions.Doc[actionKey];
+  if (actionValue <= Actions.Doc.Properties.Read) {
+    return DocRole.External;
+  }
+  if (actionValue <= Actions.Doc.Duplicate) {
+    return DocRole.Reader;
+  }
+  if (actionValue <= Actions.Doc.Update) {
+    return DocRole.Editor;
+  }
+  if (actionValue <= Actions.Doc.Users.Manage) {
+    return DocRole.Manager;
+  }
+  return DocRole.Owner;
+}
+
+export function requiredWorkspaceRoleByDocRole(
+  docRole: DocRole
+): WorkspaceRole {
+  switch (docRole) {
+    case DocRole.Owner:
+      return WorkspaceRole.Owner;
+    case DocRole.Manager:
+      return WorkspaceRole.Admin;
+    case DocRole.Editor:
+    case DocRole.Reader:
+    case DocRole.External:
+      return WorkspaceRole.Collaborator;
+  }
+}
+
+export const RoleActionsMap = {
+  WorkspaceRole: {
+    get [WorkspaceRole.External]() {
+      return [Actions.Workspace.Organize.Read];
+    },
+    get [WorkspaceRole.Collaborator]() {
+      return [
+        ...this[WorkspaceRole.External],
+        Actions.Workspace.Sync,
+        Actions.Workspace.CreateDoc,
+        Actions.Workspace.Users.Read,
+        Actions.Workspace.Properties.Read,
+        Actions.Workspace.Settings.Read,
+      ];
+    },
+    get [WorkspaceRole.Admin]() {
+      return [
+        ...this[WorkspaceRole.Collaborator],
+        Actions.Workspace.Users.Manage,
+        Actions.Workspace.Settings.Update,
+        Actions.Workspace.Properties.Create,
+        Actions.Workspace.Properties.Update,
+        Actions.Workspace.Properties.Delete,
+      ];
+    },
+    get [WorkspaceRole.Owner]() {
+      return [
+        ...this[WorkspaceRole.Admin],
+        Actions.Workspace.Delete,
+        Actions.Workspace.TransferOwner,
+      ];
+    },
+  },
+  DocRole: {
+    get [DocRole.External]() {
+      return [Actions.Doc.Read, Actions.Doc.Copy, Actions.Doc.Properties.Read];
+    },
+    get [DocRole.Reader]() {
+      return [
+        ...this[DocRole.External],
+        Actions.Doc.Users.Read,
+        Actions.Doc.Duplicate,
+      ];
+    },
+    get [DocRole.Editor]() {
+      return [
+        ...this[DocRole.Reader],
+        Actions.Doc.Trash,
+        Actions.Doc.Restore,
+        Actions.Doc.Delete,
+        Actions.Doc.Properties.Update,
+        Actions.Doc.Update,
+      ];
+    },
+    get [DocRole.Manager]() {
+      return [
+        ...this[DocRole.Editor],
+        Actions.Doc.Publish,
+        Actions.Doc.Users.Manage,
+      ];
+    },
+    get [DocRole.Owner]() {
+      return [...this[DocRole.Manager], Actions.Doc.TransferOwner];
+    },
+  },
+} as const;
diff --git a/packages/backend/server/src/core/sync/gateway.ts b/packages/backend/server/src/core/sync/gateway.ts
index d0e47fa0856f0..d2456621b8309 100644
--- a/packages/backend/server/src/core/sync/gateway.ts
+++ b/packages/backend/server/src/core/sync/gateway.ts
@@ -27,7 +27,7 @@ import {
   PgUserspaceDocStorageAdapter,
   PgWorkspaceDocStorageAdapter,
 } from '../doc';
-import { Permission, PermissionService } from '../permission';
+import { PermissionService, WorkspaceRole } from '../permission';
 import { DocID } from '../utils/doc';
 
 const SubscribeMessage = (event: string) =>
@@ -615,7 +615,7 @@ abstract class SyncSocketAdapter {
 
   async join(userId: string, spaceId: string, roomType: RoomType = 'sync') {
     this.assertNotIn(spaceId, roomType);
-    await this.assertAccessible(spaceId, userId, Permission.Read);
+    await this.assertAccessible(spaceId, userId, WorkspaceRole.Collaborator);
     return this.client.join(this.room(spaceId, roomType));
   }
 
@@ -643,7 +643,7 @@ abstract class SyncSocketAdapter {
   abstract assertAccessible(
     spaceId: string,
     userId: string,
-    permission?: Permission
+    permission?: WorkspaceRole
   ): Promise<void>;
 
   push(spaceId: string, docId: string, updates: Buffer[], editorId: string) {
@@ -694,7 +694,7 @@ class WorkspaceSyncAdapter extends SyncSocketAdapter {
   async assertAccessible(
     spaceId: string,
     userId: string,
-    permission: Permission = Permission.Read
+    permission: WorkspaceRole = WorkspaceRole.Collaborator
   ) {
     if (
       !(await this.permission.isWorkspaceMember(spaceId, userId, permission))
@@ -712,7 +712,7 @@ class UserspaceSyncAdapter extends SyncSocketAdapter {
   async assertAccessible(
     spaceId: string,
     userId: string,
-    _permission: Permission = Permission.Read
+    _permission: WorkspaceRole = WorkspaceRole.Collaborator
   ) {
     if (spaceId !== userId) {
       throw new SpaceAccessDenied({ spaceId });
diff --git a/packages/backend/server/src/core/workspaces/controller.ts b/packages/backend/server/src/core/workspaces/controller.ts
index a6a403f86ea41..0d4e8eaaa2538 100644
--- a/packages/backend/server/src/core/workspaces/controller.ts
+++ b/packages/backend/server/src/core/workspaces/controller.ts
@@ -13,7 +13,7 @@ import {
 } from '../../base';
 import { CurrentUser, Public } from '../auth';
 import { PgWorkspaceDocStorageAdapter } from '../doc';
-import { Permission, PermissionService, PublicPageMode } from '../permission';
+import { PermissionService, PublicPageMode } from '../permission';
 import { WorkspaceBlobStorage } from '../storage';
 import { DocID } from '../utils/doc';
 
@@ -147,8 +147,8 @@ export class WorkspacesController {
     await this.permission.checkPagePermission(
       docId.workspace,
       docId.guid,
-      user.id,
-      Permission.Write
+      'Doc_Read',
+      user.id
     );
 
     const history = await this.workspace.getDocHistory(
diff --git a/packages/backend/server/src/core/workspaces/management.ts b/packages/backend/server/src/core/workspaces/management.ts
index 1c6282950fa99..95209c87b9f6e 100644
--- a/packages/backend/server/src/core/workspaces/management.ts
+++ b/packages/backend/server/src/core/workspaces/management.ts
@@ -13,7 +13,7 @@ import { CurrentUser } from '../auth';
 import { Admin } from '../common';
 import { FeatureManagementService, FeatureType } from '../features';
 import { PermissionService } from '../permission';
-import { WorkspaceType } from './types';
+import { WorkspaceFeatureType, WorkspaceType } from './types';
 
 @Resolver(() => WorkspaceType)
 export class WorkspaceManagementResolver {
@@ -41,10 +41,10 @@ export class WorkspaceManagementResolver {
   }
 
   @Admin()
-  @Query(() => [WorkspaceType])
+  @Query(() => [WorkspaceFeatureType])
   async listWorkspaceFeatures(
     @Args('feature', { type: () => FeatureType }) feature: FeatureType
-  ): Promise<WorkspaceType[]> {
+  ): Promise<WorkspaceFeatureType[]> {
     return this.feature.listFeatureWorkspaces(feature);
   }
 
diff --git a/packages/backend/server/src/core/workspaces/resolvers/blob.ts b/packages/backend/server/src/core/workspaces/resolvers/blob.ts
index 133cc725c4835..dac1d747de333 100644
--- a/packages/backend/server/src/core/workspaces/resolvers/blob.ts
+++ b/packages/backend/server/src/core/workspaces/resolvers/blob.ts
@@ -15,7 +15,7 @@ import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
 import type { FileUpload } from '../../../base';
 import { BlobQuotaExceeded, CloudThrottlerGuard } from '../../../base';
 import { CurrentUser } from '../../auth';
-import { Permission, PermissionService } from '../../permission';
+import { PermissionService, WorkspaceRole } from '../../permission';
 import { QuotaManagementService } from '../../quota';
 import { WorkspaceBlobStorage } from '../../storage';
 import { WorkspaceBlobSizes, WorkspaceType } from '../types';
@@ -102,7 +102,7 @@ export class WorkspaceBlobResolver {
     await this.permissions.checkWorkspace(
       workspaceId,
       user.id,
-      Permission.Write
+      WorkspaceRole.Collaborator
     );
 
     const checkExceeded =
@@ -174,7 +174,7 @@ export class WorkspaceBlobResolver {
     await this.permissions.checkWorkspace(
       workspaceId,
       user.id,
-      Permission.Write
+      WorkspaceRole.Collaborator
     );
 
     await this.storage.release(workspaceId);
diff --git a/packages/backend/server/src/core/workspaces/resolvers/history.ts b/packages/backend/server/src/core/workspaces/resolvers/history.ts
index 8812d6bb3ca81..d5afdf0a9eaab 100644
--- a/packages/backend/server/src/core/workspaces/resolvers/history.ts
+++ b/packages/backend/server/src/core/workspaces/resolvers/history.ts
@@ -13,7 +13,7 @@ import type { SnapshotHistory } from '@prisma/client';
 
 import { CurrentUser } from '../../auth';
 import { PgWorkspaceDocStorageAdapter } from '../../doc';
-import { Permission, PermissionService } from '../../permission';
+import { PermissionService } from '../../permission';
 import { DocID } from '../../utils/doc';
 import { WorkspaceType } from '../types';
 import { EditorType } from './workspace';
@@ -79,8 +79,8 @@ export class DocHistoryResolver {
     await this.permission.checkPagePermission(
       docId.workspace,
       docId.guid,
-      user.id,
-      Permission.Write
+      'Doc_Restore',
+      user.id
     );
 
     await this.workspace.rollbackDoc(
diff --git a/packages/backend/server/src/core/workspaces/resolvers/page.ts b/packages/backend/server/src/core/workspaces/resolvers/page.ts
index a5497ff242187..fd3996a0d435a 100644
--- a/packages/backend/server/src/core/workspaces/resolvers/page.ts
+++ b/packages/backend/server/src/core/workspaces/resolvers/page.ts
@@ -1,6 +1,9 @@
+import { Logger } from '@nestjs/common';
 import {
   Args,
   Field,
+  InputType,
+  Int,
   Mutation,
   ObjectType,
   Parent,
@@ -12,18 +15,25 @@ import type { WorkspacePage as PrismaWorkspacePage } from '@prisma/client';
 import { PrismaClient } from '@prisma/client';
 
 import {
+  ExpectToGrantDocUserRoles,
   ExpectToPublishPage,
+  ExpectToRevokeDocUserRoles,
   ExpectToRevokePublicPage,
+  ExpectToUpdateDocUserRole,
   PageIsNotPublic,
 } from '../../../base';
 import { CurrentUser } from '../../auth';
 import {
-  Permission,
+  DocRole,
   PermissionService,
   PublicPageMode,
+  WorkspaceRole,
 } from '../../permission';
+import { mapRoleToActions, PermissionsList } from '../../permission/types';
+import { UserType } from '../../user';
 import { DocID } from '../../utils/doc';
 import { WorkspaceType } from '../types';
+import { WorkspacePermissions } from './workspace';
 
 registerEnumType(PublicPageMode, {
   name: 'PublicPageMode',
@@ -45,8 +55,133 @@ class WorkspacePage implements Partial<PrismaWorkspacePage> {
   public!: boolean;
 }
 
+@InputType()
+class GrantDocUserRolesInput {
+  @Field(() => String)
+  docId!: string;
+
+  @Field(() => String)
+  workspaceId!: string;
+
+  @Field(() => DocRole)
+  role!: DocRole;
+
+  @Field(() => [String])
+  userIds!: string[];
+}
+
+@InputType()
+class PageGrantedUsersInput {
+  @Field(() => Int)
+  first!: number;
+
+  @Field(() => Int)
+  offset?: number;
+
+  @Field(() => String, { description: 'Cursor', nullable: true })
+  after?: string;
+
+  @Field(() => String, { description: 'Cursor', nullable: true })
+  before?: string;
+}
+
+@ObjectType()
+class GrantedDocUserType {
+  @Field(() => UserType)
+  user!: UserType;
+
+  @Field(() => DocRole)
+  role!: DocRole;
+}
+
+@ObjectType()
+class PageInfo {
+  @Field(() => String, { nullable: true })
+  startCursor?: string;
+
+  @Field(() => String, { nullable: true })
+  endCursor?: string;
+
+  @Field(() => Boolean)
+  hasNextPage!: boolean;
+
+  @Field(() => Boolean)
+  hasPreviousPage!: boolean;
+}
+
+@ObjectType()
+class GrantedDocUserEdge {
+  @Field(() => GrantedDocUserType)
+  user!: GrantedDocUserType;
+
+  @Field(() => String)
+  cursor!: string;
+}
+
+@ObjectType()
+class GrantedDocUsersConnection {
+  @Field(() => Int)
+  totalCount!: number;
+
+  @Field(() => [GrantedDocUserEdge])
+  edges!: GrantedDocUserEdge[];
+
+  @Field(() => PageInfo)
+  pageInfo!: PageInfo;
+}
+
+@ObjectType()
+export class RolePermissions
+  extends WorkspacePermissions
+  implements PermissionsList
+{
+  @Field()
+  Doc_Read!: boolean;
+  @Field()
+  Doc_Copy!: boolean;
+  @Field()
+  Doc_Properties_Read!: boolean;
+  @Field()
+  Doc_Users_Read!: boolean;
+  @Field()
+  Doc_Duplicate!: boolean;
+  @Field()
+  Doc_Trash!: boolean;
+  @Field()
+  Doc_Restore!: boolean;
+  @Field()
+  Doc_Delete!: boolean;
+  @Field()
+  Doc_Properties_Update!: boolean;
+  @Field()
+  Doc_Update!: boolean;
+  @Field()
+  Doc_Publish!: boolean;
+  @Field()
+  Doc_Users_Manage!: boolean;
+  @Field()
+  Doc_TransferOwner!: boolean;
+}
+
+@ObjectType()
+class DocType {
+  @Field(() => String)
+  id!: string;
+
+  @Field(() => Boolean)
+  public!: boolean;
+
+  @Field(() => DocRole)
+  role!: DocRole;
+
+  @Field(() => RolePermissions)
+  permissions!: RolePermissions;
+}
+
 @Resolver(() => WorkspaceType)
 export class PagePermissionResolver {
+  private readonly logger = new Logger(PagePermissionResolver.name);
+
   constructor(
     private readonly prisma: PrismaClient,
     private readonly permission: PermissionService
@@ -102,6 +237,122 @@ export class PagePermissionResolver {
     });
   }
 
+  @ResolveField(() => DocType, {
+    description: 'Check if current user has permission to access the page',
+    complexity: 2,
+  })
+  async pagePermission(
+    @Parent() workspace: WorkspaceType,
+    @Args('pageId') pageId: string,
+    @CurrentUser() user: CurrentUser
+  ): Promise<DocType> {
+    const page = await this.prisma.workspacePage.findFirst({
+      where: {
+        workspaceId: workspace.id,
+        pageId,
+      },
+      select: {
+        public: true,
+      },
+    });
+
+    const [permission, workspacePermission] = await this.prisma.$transaction(
+      tx =>
+        Promise.all([
+          tx.workspacePageUserPermission.findFirst({
+            where: {
+              workspaceId: workspace.id,
+              pageId,
+              userId: user.id,
+            },
+          }),
+          tx.workspaceUserPermission.findFirst({
+            where: {
+              workspaceId: workspace.id,
+              userId: user.id,
+            },
+          }),
+        ])
+    );
+    return {
+      id: pageId,
+      public: page?.public ?? false,
+      role: permission?.type ?? DocRole.External,
+      permissions: mapRoleToActions(
+        workspacePermission?.type,
+        permission?.type
+      ),
+    };
+  }
+
+  @ResolveField(() => GrantedDocUsersConnection, {
+    description: 'Page granted users list',
+    complexity: 4,
+  })
+  async pageGrantedUsersList(
+    @Parent() workspace: WorkspaceType,
+    @Args('pageId') pageId: string,
+    @Args('pageGrantedUsersInput')
+    pageGrantedUsersInput: PageGrantedUsersInput
+  ): Promise<GrantedDocUsersConnection> {
+    const docId = new DocID(pageId, workspace.id);
+    const [permissions, totalCount] = await this.prisma.$transaction(tx => {
+      return Promise.all([
+        tx.workspacePageUserPermission.findMany({
+          where: {
+            workspaceId: workspace.id,
+            pageId: docId.guid,
+          },
+          include: {
+            user: true,
+          },
+          orderBy: {
+            createdAt: 'desc',
+          },
+          take: pageGrantedUsersInput.first,
+          skip: pageGrantedUsersInput.offset,
+          cursor: pageGrantedUsersInput.after
+            ? {
+                id: pageGrantedUsersInput.after,
+              }
+            : undefined,
+        }),
+        tx.workspacePageUserPermission.count({
+          where: {
+            workspaceId: workspace.id,
+            pageId: docId.guid,
+          },
+        }),
+      ]);
+    });
+
+    return {
+      totalCount,
+      edges: permissions.map(permission => ({
+        user: {
+          user: {
+            id: permission.user.id,
+            name: permission.user.name,
+            email: permission.user.email,
+            avatarUrl: permission.user.avatarUrl,
+            emailVerified: permission.user.emailVerifiedAt !== null,
+            hasPassword: permission.user.password !== null,
+          },
+          role: permission.type,
+        },
+        cursor: permission.id,
+      })),
+      pageInfo: {
+        startCursor: permissions.at(0)?.id,
+        endCursor: permissions.at(-1)?.id,
+        hasNextPage: totalCount > pageGrantedUsersInput.first,
+        hasPreviousPage:
+          pageGrantedUsersInput.offset !== undefined &&
+          pageGrantedUsersInput.offset > 0,
+      },
+    };
+  }
+
   /**
    * @deprecated
    */
@@ -134,15 +385,26 @@ export class PagePermissionResolver {
     const docId = new DocID(pageId, workspaceId);
 
     if (docId.isWorkspace) {
+      this.logger.error('Expect to publish page, but it is a workspace', {
+        workspaceId,
+        pageId,
+      });
       throw new ExpectToPublishPage();
     }
 
-    await this.permission.checkWorkspace(
+    await this.permission.checkPagePermission(
       docId.workspace,
-      user.id,
-      Permission.Write
+      docId.guid,
+      'Doc_Publish',
+      user.id
     );
 
+    this.logger.log('Publish page', {
+      workspaceId,
+      pageId,
+      mode,
+    });
+
     return this.permission.publishPage(docId.workspace, docId.guid, mode);
   }
 
@@ -171,13 +433,18 @@ export class PagePermissionResolver {
     const docId = new DocID(pageId, workspaceId);
 
     if (docId.isWorkspace) {
+      this.logger.error('Expect to revoke public page, but it is a workspace', {
+        workspaceId,
+        pageId,
+      });
       throw new ExpectToRevokePublicPage('Expect page not to be workspace');
     }
 
-    await this.permission.checkWorkspace(
+    await this.permission.checkPagePermission(
       docId.workspace,
-      user.id,
-      Permission.Write
+      docId.guid,
+      'Doc_Publish',
+      user.id
     );
 
     const isPublic = await this.permission.isPublicPage(
@@ -186,9 +453,148 @@ export class PagePermissionResolver {
     );
 
     if (!isPublic) {
+      this.logger.log('Expect to revoke public page, but it is not public', {
+        workspaceId,
+        pageId,
+      });
       throw new PageIsNotPublic('Page is not public');
     }
 
+    this.logger.log('Revoke public page', {
+      workspaceId,
+      pageId,
+    });
+
     return this.permission.revokePublicPage(docId.workspace, docId.guid);
   }
+
+  @Mutation(() => Boolean)
+  async grantDocUserRoles(
+    @CurrentUser() user: CurrentUser,
+    @Args('input') input: GrantDocUserRolesInput
+  ): Promise<boolean> {
+    const doc = new DocID(input.docId, input.workspaceId);
+    const pairs = {
+      spaceId: input.workspaceId,
+      docId: input.docId,
+    };
+    if (doc.isWorkspace) {
+      this.logger.error(
+        'Expect to grant doc user roles, but it is a workspace',
+        pairs
+      );
+      throw new ExpectToGrantDocUserRoles(
+        pairs,
+        'Expect doc not to be workspace'
+      );
+    }
+    await this.permission.checkPagePermission(
+      doc.workspace,
+      doc.guid,
+      'Doc_Users_Manage',
+      user.id
+    );
+    await this.permission.grantPagePermission(
+      doc.workspace,
+      doc.guid,
+      input.userIds,
+      input.role
+    );
+    this.logger.log('Grant doc user roles', {
+      ...pairs,
+      userIds: input.userIds,
+      role: input.role,
+    });
+    return true;
+  }
+
+  @Mutation(() => Boolean)
+  async revokeDocUserRoles(
+    @CurrentUser() user: CurrentUser,
+    @Args('docId') docId: string,
+    @Args('userIds', { type: () => [String] }) userIds: string[]
+  ): Promise<boolean> {
+    const doc = new DocID(docId);
+    const pairs = {
+      spaceId: doc.workspace,
+      docId: doc.guid,
+    };
+    if (doc.isWorkspace) {
+      this.logger.error(
+        'Expect to revoke doc user roles, but it is a workspace',
+        pairs
+      );
+      throw new ExpectToRevokeDocUserRoles(
+        pairs,
+        'Expect doc not to be workspace'
+      );
+    }
+    await this.permission.checkWorkspace(
+      doc.workspace,
+      user.id,
+      WorkspaceRole.Collaborator
+    );
+    await this.permission.revokePage(doc.workspace, doc.guid, userIds);
+    this.logger.log('Revoke doc user roles', {
+      ...pairs,
+      userIds: userIds,
+    });
+    return true;
+  }
+
+  @Mutation(() => Boolean)
+  async updateDocUserRole(
+    @CurrentUser() user: CurrentUser,
+    @Args('docId') docId: string,
+    @Args('userId') userId: string,
+    @Args('role', { type: () => DocRole }) role: DocRole
+  ): Promise<boolean> {
+    const doc = new DocID(docId);
+    const pairs = {
+      spaceId: doc.workspace,
+      docId: doc.guid,
+    };
+    if (doc.isWorkspace) {
+      this.logger.error(
+        'Expect to update doc user role, but it is a workspace',
+        pairs
+      );
+      throw new ExpectToUpdateDocUserRole(
+        pairs,
+        'Expect doc not to be workspace'
+      );
+    }
+    await this.permission.checkWorkspace(
+      doc.workspace,
+      user.id,
+      WorkspaceRole.Collaborator
+    );
+    if (role === DocRole.Owner) {
+      const ret = await this.permission.grantPagePermission(
+        doc.workspace,
+        doc.guid,
+        [userId],
+        role
+      );
+      this.logger.log('Transfer doc owner', {
+        ...pairs,
+        userId: userId,
+        role: role,
+      });
+      return ret.length > 0;
+    } else {
+      await this.permission.updatePagePermission(
+        doc.workspace,
+        doc.guid,
+        userId,
+        role
+      );
+      this.logger.log('Update doc user role', {
+        ...pairs,
+        userId: userId,
+        role: role,
+      });
+      return true;
+    }
+  }
 }
diff --git a/packages/backend/server/src/core/workspaces/resolvers/service.ts b/packages/backend/server/src/core/workspaces/resolvers/service.ts
index bffb8d16f2248..043039fe57043 100644
--- a/packages/backend/server/src/core/workspaces/resolvers/service.ts
+++ b/packages/backend/server/src/core/workspaces/resolvers/service.ts
@@ -11,7 +11,7 @@ import {
 } from '../../../base';
 import { Models } from '../../../models';
 import { DocContentService } from '../../doc-renderer';
-import { Permission, PermissionService } from '../../permission';
+import { PermissionService, WorkspaceRole } from '../../permission';
 import { WorkspaceBlobStorage } from '../../storage';
 
 export const defaultWorkspaceAvatar =
@@ -221,14 +221,14 @@ export class WorkspaceService {
 
   async sendRoleChangedEmail(
     userId: string,
-    ws: { id: string; role: Permission }
+    ws: { id: string; role: WorkspaceRole }
   ) {
     const user = await this.models.user.getPublicUser(userId);
     if (!user) throw new UserNotFound();
 
     const workspace = await this.getWorkspaceInfo(ws.id);
 
-    if (ws.role === Permission.Admin) {
+    if (ws.role === WorkspaceRole.Admin) {
       await this.mailer.sendTeamBecomeAdminMail(user.email, {
         workspace,
         url: this.url.link(`/workspace/${workspace.id}`),
diff --git a/packages/backend/server/src/core/workspaces/resolvers/team.ts b/packages/backend/server/src/core/workspaces/resolvers/team.ts
index 81848fb476406..51dc10e600fe8 100644
--- a/packages/backend/server/src/core/workspaces/resolvers/team.ts
+++ b/packages/backend/server/src/core/workspaces/resolvers/team.ts
@@ -21,7 +21,7 @@ import {
 } from '../../../base';
 import { Models } from '../../../models';
 import { CurrentUser } from '../../auth';
-import { Permission, PermissionService } from '../../permission';
+import { PermissionService, WorkspaceRole } from '../../permission';
 import { QuotaManagementService } from '../../quota';
 import {
   InviteLink,
@@ -71,7 +71,7 @@ export class TeamWorkspaceResolver {
     await this.permissions.checkWorkspace(
       workspaceId,
       user.id,
-      Permission.Admin
+      WorkspaceRole.Admin
     );
 
     if (emails.length > 512) {
@@ -113,7 +113,7 @@ export class TeamWorkspaceResolver {
         ret.inviteId = await this.permissions.grant(
           workspaceId,
           target.id,
-          Permission.Write,
+          WorkspaceRole.Collaborator,
           needMoreSeat
             ? WorkspaceMemberStatus.NeedMoreSeat
             : WorkspaceMemberStatus.Pending
@@ -159,7 +159,7 @@ export class TeamWorkspaceResolver {
     await this.permissions.checkWorkspace(
       workspace.id,
       user.id,
-      Permission.Admin
+      WorkspaceRole.Admin
     );
 
     const cacheId = `workspace:inviteLink:${workspace.id}`;
@@ -186,7 +186,7 @@ export class TeamWorkspaceResolver {
     await this.permissions.checkWorkspace(
       workspaceId,
       user.id,
-      Permission.Admin
+      WorkspaceRole.Admin
     );
     const cacheWorkspaceId = `workspace:inviteLink:${workspaceId}`;
     const invite = await this.cache.get<{ inviteId: string }>(cacheWorkspaceId);
@@ -222,7 +222,7 @@ export class TeamWorkspaceResolver {
     await this.permissions.checkWorkspace(
       workspaceId,
       user.id,
-      Permission.Admin
+      WorkspaceRole.Admin
     );
     const cacheId = `workspace:inviteLink:${workspaceId}`;
     return await this.cache.delete(cacheId);
@@ -237,7 +237,7 @@ export class TeamWorkspaceResolver {
     await this.permissions.checkWorkspace(
       workspaceId,
       user.id,
-      Permission.Admin
+      WorkspaceRole.Admin
     );
 
     try {
@@ -257,7 +257,7 @@ export class TeamWorkspaceResolver {
           const result = await this.permissions.grant(
             workspaceId,
             userId,
-            Permission.Write,
+            WorkspaceRole.Collaborator,
             WorkspaceMemberStatus.Accepted
           );
 
@@ -283,12 +283,12 @@ export class TeamWorkspaceResolver {
     @CurrentUser() user: CurrentUser,
     @Args('workspaceId') workspaceId: string,
     @Args('userId') userId: string,
-    @Args('permission', { type: () => Permission }) permission: Permission
+    @Args('permission', { type: () => WorkspaceRole }) permission: WorkspaceRole
   ) {
     await this.permissions.checkWorkspace(
       workspaceId,
       user.id,
-      Permission.Owner
+      WorkspaceRole.Owner
     );
 
     try {
@@ -311,7 +311,7 @@ export class TeamWorkspaceResolver {
         );
 
         if (result) {
-          if (permission === Permission.Owner) {
+          if (permission === WorkspaceRole.Owner) {
             this.event.emit('workspace.members.ownershipTransferred', {
               workspaceId,
               from: user.id,
diff --git a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts
index fbac828fd5763..f0d472ef4d1cd 100644
--- a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts
+++ b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts
@@ -1,4 +1,3 @@
-import { Logger } from '@nestjs/common';
 import {
   Args,
   Field,
@@ -15,6 +14,7 @@ import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
 
 import type { FileUpload } from '../../../base';
 import {
+  AFFiNELogger,
   AlreadyInSpace,
   Cache,
   DocNotFound,
@@ -33,7 +33,11 @@ import {
 import { Models } from '../../../models';
 import { CurrentUser, Public } from '../../auth';
 import { type Editor, PgWorkspaceDocStorageAdapter } from '../../doc';
-import { Permission, PermissionService } from '../../permission';
+import { PermissionService, WorkspaceRole } from '../../permission';
+import {
+  mapWorkspaceRoleToWorkspaceActions,
+  WorkspacePermissionsList,
+} from '../../permission/types';
 import { QuotaManagementService, QuotaQueryType } from '../../quota';
 import { UserType } from '../../user';
 import {
@@ -68,6 +72,45 @@ class WorkspacePageMeta {
   updatedBy!: EditorType | null;
 }
 
+@ObjectType()
+export class WorkspacePermissions implements WorkspacePermissionsList {
+  @Field()
+  Workspace_Organize_Read!: boolean;
+  @Field()
+  Workspace_Sync!: boolean;
+  @Field()
+  Workspace_CreateDoc!: boolean;
+  @Field()
+  Workspace_Users_Read!: boolean;
+  @Field()
+  Workspace_Properties_Read!: boolean;
+  @Field()
+  Workspace_Settings_Read!: boolean;
+  @Field()
+  Workspace_Users_Manage!: boolean;
+  @Field()
+  Workspace_Settings_Update!: boolean;
+  @Field()
+  Workspace_Properties_Create!: boolean;
+  @Field()
+  Workspace_Properties_Update!: boolean;
+  @Field()
+  Workspace_Properties_Delete!: boolean;
+  @Field()
+  Workspace_Delete!: boolean;
+  @Field()
+  Workspace_TransferOwner!: boolean;
+}
+
+@ObjectType()
+export class WorkspaceRolePermissions {
+  @Field(() => WorkspaceRole)
+  role!: WorkspaceRole;
+
+  @Field(() => WorkspacePermissions)
+  permissions!: WorkspacePermissions;
+}
+
 /**
  * Workspace resolver
  * Public apis rate limit: 10 req/m
@@ -75,8 +118,6 @@ class WorkspacePageMeta {
  */
 @Resolver(() => WorkspaceType)
 export class WorkspaceResolver {
-  private readonly logger = new Logger(WorkspaceResolver.name);
-
   constructor(
     private readonly cache: Cache,
     private readonly prisma: PrismaClient,
@@ -86,29 +127,32 @@ export class WorkspaceResolver {
     private readonly event: EventBus,
     private readonly mutex: RequestMutex,
     private readonly workspaceService: WorkspaceService,
-    private readonly workspaceStorage: PgWorkspaceDocStorageAdapter
-  ) {}
+    private readonly workspaceStorage: PgWorkspaceDocStorageAdapter,
+    private readonly logger: AFFiNELogger
+  ) {
+    logger.setContext(WorkspaceResolver.name);
+  }
 
-  @ResolveField(() => Permission, {
-    description: 'Permission of current signed in user in workspace',
+  @ResolveField(() => WorkspaceRole, {
+    description: 'Role of current signed in user in workspace',
     complexity: 2,
   })
-  async permission(
+  async role(
     @CurrentUser() user: CurrentUser,
     @Parent() workspace: WorkspaceType
   ) {
     // may applied in workspaces query
-    if ('permission' in workspace) {
-      return workspace.permission;
+    if ('role' in workspace) {
+      return workspace.role;
     }
 
-    const permission = await this.permissions.get(workspace.id, user.id);
+    const role = await this.permissions.get(workspace.id, user.id);
 
-    if (!permission) {
+    if (!role) {
       throw new SpaceAccessDenied({ spaceId: workspace.id });
     }
 
-    return permission;
+    return role;
   }
 
   @ResolveField(() => Int, {
@@ -249,7 +293,7 @@ export class WorkspaceResolver {
     return this.permissions.tryCheckWorkspaceIs(
       workspaceId,
       user.id,
-      Permission.Admin
+      WorkspaceRole.Admin
     );
   }
 
@@ -279,6 +323,7 @@ export class WorkspaceResolver {
       return {
         ...workspace,
         permission: type,
+        role: type,
       };
     });
   }
@@ -297,6 +342,25 @@ export class WorkspaceResolver {
     return workspace;
   }
 
+  @Query(() => WorkspaceRolePermissions, {
+    description: 'Get workspace role permissions',
+  })
+  async workspaceRolePermissions(
+    @CurrentUser() user: CurrentUser,
+    @Args('id') id: string
+  ) {
+    const workspace = await this.prisma.workspaceUserPermission.findFirst({
+      where: { workspaceId: id, userId: user.id },
+    });
+    if (!workspace) {
+      throw new SpaceAccessDenied({ spaceId: id });
+    }
+    return {
+      role: workspace.type,
+      permissions: mapWorkspaceRoleToWorkspaceActions(workspace.type),
+    };
+  }
+
   @Mutation(() => WorkspaceType, {
     description: 'Create a new workspace',
   })
@@ -312,7 +376,7 @@ export class WorkspaceResolver {
         public: false,
         permissions: {
           create: {
-            type: Permission.Owner,
+            type: WorkspaceRole.Owner,
             userId: user.id,
             accepted: true,
             status: WorkspaceMemberStatus.Accepted,
@@ -323,21 +387,18 @@ export class WorkspaceResolver {
 
     if (init) {
       // convert stream to buffer
-      const buffer = await new Promise<Buffer>(resolve => {
-        const stream = init.createReadStream();
-        const chunks: Uint8Array[] = [];
-        stream.on('data', chunk => {
+      const chunks: Uint8Array[] = [];
+      try {
+        for await (const chunk of init.createReadStream()) {
           chunks.push(chunk);
-        });
-        stream.on('error', () => {
-          resolve(Buffer.from([]));
-        });
-        stream.on('end', () => {
-          resolve(Buffer.concat(chunks));
-        });
-      });
+        }
+      } catch (e) {
+        this.logger.error('Failed to get file content from upload stream', e);
+        chunks.length = 0;
+      }
+      const buffer = chunks.length ? Buffer.concat(chunks) : null;
 
-      if (buffer.length) {
+      if (buffer) {
         await this.prisma.snapshot.create({
           data: {
             id: workspace.id,
@@ -364,7 +425,7 @@ export class WorkspaceResolver {
     await this.permissions.checkWorkspace(
       id,
       user.id,
-      isTeam ? Permission.Owner : Permission.Admin
+      isTeam ? WorkspaceRole.Owner : WorkspaceRole.Admin
     );
 
     return this.prisma.workspace.update({
@@ -380,7 +441,7 @@ export class WorkspaceResolver {
     @CurrentUser() user: CurrentUser,
     @Args('id') id: string
   ) {
-    await this.permissions.checkWorkspace(id, user.id, Permission.Owner);
+    await this.permissions.checkWorkspace(id, user.id, WorkspaceRole.Owner);
 
     await this.prisma.workspace.delete({
       where: {
@@ -401,16 +462,16 @@ export class WorkspaceResolver {
     @Args('email') email: string,
     @Args('sendInviteMail', { nullable: true }) sendInviteMail: boolean,
     @Args('permission', {
-      type: () => Permission,
+      type: () => WorkspaceRole,
       nullable: true,
       deprecationReason: 'never used',
     })
-    _permission?: Permission
+    _permission?: WorkspaceRole
   ) {
     await this.permissions.checkWorkspace(
       workspaceId,
       user.id,
-      Permission.Admin
+      WorkspaceRole.Admin
     );
 
     try {
@@ -418,7 +479,7 @@ export class WorkspaceResolver {
       const lockFlag = `invite:${workspaceId}`;
       await using lock = await this.mutex.acquire(lockFlag);
       if (!lock) {
-        return new TooManyRequest();
+        throw new TooManyRequest();
       }
 
       // member limit check
@@ -445,7 +506,7 @@ export class WorkspaceResolver {
       const inviteId = await this.permissions.grant(
         workspaceId,
         target.id,
-        Permission.Write
+        WorkspaceRole.Collaborator
       );
       if (sendInviteMail) {
         try {
@@ -474,10 +535,10 @@ export class WorkspaceResolver {
     } catch (e) {
       // pass through user friendly error
       if (e instanceof UserFriendlyError) {
-        return e;
+        throw e;
       }
       this.logger.error('failed to invite user', e);
-      return new TooManyRequest();
+      throw new TooManyRequest();
     }
   }
 
@@ -512,20 +573,20 @@ export class WorkspaceResolver {
     const isAdmin = await this.permissions.tryCheckWorkspaceIs(
       workspaceId,
       userId,
-      Permission.Admin
+      WorkspaceRole.Admin
     );
     if (isTeam && isAdmin) {
       // only owner can revoke team workspace admin
       await this.permissions.checkWorkspaceIs(
         workspaceId,
         user.id,
-        Permission.Owner
+        WorkspaceRole.Owner
       );
     } else {
       await this.permissions.checkWorkspace(
         workspaceId,
         user.id,
-        Permission.Admin
+        WorkspaceRole.Admin
       );
     }
 
@@ -543,7 +604,7 @@ export class WorkspaceResolver {
     const lockFlag = `invite:${workspaceId}`;
     await using lock = await this.mutex.acquire(lockFlag);
     if (!lock) {
-      return new TooManyRequest();
+      throw new TooManyRequest();
     }
 
     const isTeam = await this.quota.isTeamWorkspace(workspaceId);
@@ -553,7 +614,7 @@ export class WorkspaceResolver {
         user.id
       );
       if (status === WorkspaceMemberStatus.Accepted) {
-        return new AlreadyInSpace({ spaceId: workspaceId });
+        throw new AlreadyInSpace({ spaceId: workspaceId });
       }
 
       // invite link
@@ -568,7 +629,7 @@ export class WorkspaceResolver {
             await this.permissions.grant(
               workspaceId,
               user.id,
-              Permission.Write,
+              WorkspaceRole.Collaborator,
               WorkspaceMemberStatus.NeedMoreSeatAndReview
             );
             const memberCount =
@@ -579,7 +640,7 @@ export class WorkspaceResolver {
             });
             return true;
           } else if (!status) {
-            return new MemberQuotaExceeded();
+            throw new MemberQuotaExceeded();
           }
         } else {
           const inviteId = await this.permissions.grant(workspaceId, user.id);
diff --git a/packages/backend/server/src/core/workspaces/types.ts b/packages/backend/server/src/core/workspaces/types.ts
index be5a1961e4884..30c84099ff342 100644
--- a/packages/backend/server/src/core/workspaces/types.ts
+++ b/packages/backend/server/src/core/workspaces/types.ts
@@ -8,17 +8,28 @@ import {
   PickType,
   registerEnumType,
 } from '@nestjs/graphql';
-import { Workspace, WorkspaceMemberStatus } from '@prisma/client';
+import { WorkspaceMemberStatus } from '@prisma/client';
 import { SafeIntResolver } from 'graphql-scalars';
 
-import { Permission } from '../permission';
+import { DocRole, WorkspaceRole } from '../permission';
 import { UserType } from '../user/types';
 
-registerEnumType(Permission, {
+registerEnumType(WorkspaceRole, {
+  name: 'WorkspaceRole',
+  description: 'User role in workspace',
+});
+
+// @deprecated
+registerEnumType(WorkspaceRole, {
   name: 'Permission',
   description: 'User permission in workspace',
 });
 
+registerEnumType(DocRole, {
+  name: 'DocRole',
+  description: 'User permission in doc',
+});
+
 registerEnumType(WorkspaceMemberStatus, {
   name: 'WorkspaceMemberStatus',
   description: 'Member invite status in workspace',
@@ -33,8 +44,14 @@ export class InviteUserType extends OmitType(
   @Field(() => ID)
   id!: string;
 
-  @Field(() => Permission, { description: 'User permission in workspace' })
-  permission!: Permission;
+  @Field(() => WorkspaceRole, {
+    deprecationReason: 'Use role instead',
+    description: 'User permission in workspace',
+  })
+  permission!: WorkspaceRole;
+
+  @Field(() => WorkspaceRole, { description: 'User role in workspace' })
+  role!: WorkspaceRole;
 
   @Field({ description: 'Invite id' })
   inviteId!: string;
@@ -52,22 +69,25 @@ export class InviteUserType extends OmitType(
 }
 
 @ObjectType()
-export class WorkspaceType implements Partial<Workspace> {
+export class WorkspaceFeatureType {
   @Field(() => ID)
   id!: string;
 
   @Field({ description: 'is Public workspace' })
   public!: boolean;
 
+  @Field({ description: 'Workspace created date' })
+  createdAt!: Date;
+}
+
+@ObjectType()
+export class WorkspaceType extends WorkspaceFeatureType {
   @Field({ description: 'Enable AI' })
   enableAi!: boolean;
 
   @Field({ description: 'Enable url previous when sharing' })
   enableUrlPreview!: boolean;
 
-  @Field({ description: 'Workspace created date' })
-  createdAt!: Date;
-
   @Field(() => [InviteUserType], {
     description: 'Members of workspace',
   })
diff --git a/packages/backend/server/src/models/common/index.ts b/packages/backend/server/src/models/common/index.ts
index 40656295c3ba2..445e67262d361 100644
--- a/packages/backend/server/src/models/common/index.ts
+++ b/packages/backend/server/src/models/common/index.ts
@@ -1,3 +1,2 @@
 export * from './feature';
 export * from './page';
-export * from './permission';
diff --git a/packages/backend/server/src/models/common/permission.ts b/packages/backend/server/src/models/common/permission.ts
deleted file mode 100644
index a86fcb6c2bb60..0000000000000
--- a/packages/backend/server/src/models/common/permission.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export enum Permission {
-  Read = 0,
-  Write = 1,
-  Admin = 10,
-  Owner = 99,
-}
diff --git a/packages/backend/server/src/models/page.ts b/packages/backend/server/src/models/page.ts
index 3fa540435e49e..83f5140ade934 100644
--- a/packages/backend/server/src/models/page.ts
+++ b/packages/backend/server/src/models/page.ts
@@ -5,9 +5,9 @@ import {
   type WorkspacePageUserPermission as PageUserPermission,
 } from '@prisma/client';
 
+import { WorkspaceRole } from '../core/permission';
 import { BaseModel } from './base';
-import { Permission, PublicPageMode } from './common';
-
+import { PublicPageMode } from './common';
 export type { Page };
 export type UpdatePageInput = {
   mode?: PublicPageMode;
@@ -93,7 +93,7 @@ export class PageModel extends BaseModel {
     workspaceId: string,
     pageId: string,
     userId: string,
-    permission: Permission = Permission.Read
+    permission: WorkspaceRole = WorkspaceRole.Collaborator
   ): Promise<PageUserPermission> {
     let data = await this.db.workspacePageUserPermission.findUnique({
       where: {
@@ -134,15 +134,15 @@ export class PageModel extends BaseModel {
       }
 
       // If the new permission is owner, we need to revoke old owner
-      if (permission === Permission.Owner) {
+      if (permission === WorkspaceRole.Owner) {
         await this.db.workspacePageUserPermission.updateMany({
           where: {
             workspaceId,
             pageId,
-            type: Permission.Owner,
+            type: WorkspaceRole.Owner,
             userId: { not: userId },
           },
-          data: { type: Permission.Admin },
+          data: { type: WorkspaceRole.Admin },
         });
         this.logger.log(
           `Change owner of workspace ${workspaceId} page ${pageId} to user ${userId}`
@@ -163,7 +163,7 @@ export class PageModel extends BaseModel {
     workspaceId: string,
     pageId: string,
     userId: string,
-    permission: Permission = Permission.Read
+    permission: WorkspaceRole = WorkspaceRole.Collaborator
   ) {
     const count = await this.db.workspacePageUserPermission.count({
       where: {
@@ -190,7 +190,7 @@ export class PageModel extends BaseModel {
         userId,
         type: {
           // We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading
-          not: Permission.Owner,
+          not: WorkspaceRole.Owner,
         },
       },
     });
diff --git a/packages/backend/server/src/models/workspace.ts b/packages/backend/server/src/models/workspace.ts
index ccd7da3da4ccf..44995802439e9 100644
--- a/packages/backend/server/src/models/workspace.ts
+++ b/packages/backend/server/src/models/workspace.ts
@@ -8,8 +8,8 @@ import {
 import { groupBy } from 'lodash-es';
 
 import { EventBus } from '../base';
+import { WorkspaceRole } from '../core/permission';
 import { BaseModel } from './base';
-import { Permission } from './common';
 
 declare global {
   interface Events {
@@ -90,7 +90,7 @@ export class WorkspaceModel extends BaseModel {
         public: false,
         permissions: {
           create: {
-            type: Permission.Owner,
+            type: WorkspaceRole.Owner,
             userId: userId,
             accepted: true,
             status: WorkspaceMemberStatus.Accepted,
@@ -141,7 +141,7 @@ export class WorkspaceModel extends BaseModel {
     const rows = await this.db.workspaceUserPermission.findMany({
       where: {
         userId,
-        type: Permission.Owner,
+        type: WorkspaceRole.Owner,
         OR: this.acceptedCondition,
       },
       select: {
@@ -177,7 +177,7 @@ export class WorkspaceModel extends BaseModel {
   async grantMember(
     workspaceId: string,
     userId: string,
-    permission: Permission = Permission.Read,
+    permission: WorkspaceRole = WorkspaceRole.Collaborator,
     status: WorkspaceMemberStatus = WorkspaceMemberStatus.Pending
   ): Promise<WorkspaceUserPermission> {
     const data = await this.db.workspaceUserPermission.findUnique({
@@ -191,17 +191,19 @@ export class WorkspaceModel extends BaseModel {
 
     if (!data) {
       // Create a new permission
-      // TODO(fengmk2): should we check the permission here? Like owner can't be pending?
       const created = await this.db.workspaceUserPermission.create({
         data: {
           workspaceId,
           userId,
           type: permission,
-          status,
+          status:
+            permission === WorkspaceRole.Owner
+              ? WorkspaceMemberStatus.Accepted
+              : status,
         },
       });
       this.logger.log(
-        `Granted workspace ${workspaceId} member ${userId} with permission ${permission}`
+        `Granted workspace ${workspaceId} member ${userId} with permission ${WorkspaceRole[permission]}`
       );
       await this.notifyMembersUpdated(workspaceId);
       return created;
@@ -216,14 +218,14 @@ export class WorkspaceModel extends BaseModel {
         data: { type: permission },
       });
       // If the new permission is owner, we need to revoke old owner
-      if (permission === Permission.Owner) {
+      if (permission === WorkspaceRole.Owner) {
         await this.db.workspaceUserPermission.updateMany({
           where: {
             workspaceId,
-            type: Permission.Owner,
+            type: WorkspaceRole.Owner,
             userId: { not: userId },
           },
-          data: { type: Permission.Admin },
+          data: { type: WorkspaceRole.Admin },
         });
         this.logger.log(
           `Change owner of workspace ${workspaceId} to ${userId}`
@@ -318,7 +320,7 @@ export class WorkspaceModel extends BaseModel {
   async isMember(
     workspaceId: string,
     userId: string,
-    permission: Permission = Permission.Read
+    permission: WorkspaceRole = WorkspaceRole.Collaborator
   ) {
     const count = await this.db.workspaceUserPermission.count({
       where: {
@@ -340,7 +342,7 @@ export class WorkspaceModel extends BaseModel {
     return await this.db.workspaceUserPermission.findFirst({
       where: {
         workspaceId,
-        type: Permission.Owner,
+        type: WorkspaceRole.Owner,
         OR: this.acceptedCondition,
       },
       include: {
@@ -356,7 +358,7 @@ export class WorkspaceModel extends BaseModel {
     return await this.db.workspaceUserPermission.findMany({
       where: {
         workspaceId,
-        type: Permission.Admin,
+        type: WorkspaceRole.Admin,
         OR: this.acceptedCondition,
       },
       include: {
@@ -394,7 +396,7 @@ export class WorkspaceModel extends BaseModel {
 
     // We shouldn't revoke owner permission
     // should auto deleted by workspace/user delete cascading
-    if (!member || member.type === Permission.Owner) {
+    if (!member || member.type === WorkspaceRole.Owner) {
       return false;
     }
 
diff --git a/packages/backend/server/src/plugins/copilot/resolver.ts b/packages/backend/server/src/plugins/copilot/resolver.ts
index 2b88a721c093d..5300d84e3a1f7 100644
--- a/packages/backend/server/src/plugins/copilot/resolver.ts
+++ b/packages/backend/server/src/plugins/copilot/resolver.ts
@@ -335,6 +335,7 @@ export class CopilotResolver {
       await this.permissions.checkCloudPagePermission(
         workspaceId,
         docId,
+        'Doc_Read',
         user.id
       );
     } else {
@@ -368,6 +369,7 @@ export class CopilotResolver {
     await this.permissions.checkCloudPagePermission(
       options.workspaceId,
       options.docId,
+      'Doc_Read',
       user.id
     );
     const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${options.workspaceId}`;
@@ -401,6 +403,7 @@ export class CopilotResolver {
     await this.permissions.checkCloudPagePermission(
       workspaceId,
       docId,
+      'Doc_Update',
       user.id
     );
     const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${workspaceId}`;
@@ -428,6 +431,7 @@ export class CopilotResolver {
     await this.permissions.checkCloudPagePermission(
       options.workspaceId,
       options.docId,
+      'Doc_Copy',
       user.id
     );
     const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${options.workspaceId}`;
@@ -456,6 +460,7 @@ export class CopilotResolver {
     await this.permissions.checkCloudPagePermission(
       options.workspaceId,
       options.docId,
+      'Doc_Delete',
       user.id
     );
     if (!options.sessionIds.length) {
diff --git a/packages/backend/server/src/plugins/license/resolver.ts b/packages/backend/server/src/plugins/license/resolver.ts
index 463c759e9cd09..450430ef698ea 100644
--- a/packages/backend/server/src/plugins/license/resolver.ts
+++ b/packages/backend/server/src/plugins/license/resolver.ts
@@ -11,7 +11,7 @@ import {
 
 import { ActionForbidden, Config } from '../../base';
 import { CurrentUser } from '../../core/auth';
-import { Permission, PermissionService } from '../../core/permission';
+import { PermissionService, WorkspaceRole } from '../../core/permission';
 import { WorkspaceType } from '../../core/workspaces';
 import { SubscriptionRecurring } from '../payment/types';
 import { LicenseService } from './service';
@@ -61,7 +61,7 @@ export class LicenseResolver {
     await this.permission.checkWorkspaceIs(
       workspace.id,
       user.id,
-      Permission.Owner
+      WorkspaceRole.Owner
     );
 
     return this.service.getLicense(workspace.id);
@@ -80,7 +80,7 @@ export class LicenseResolver {
     await this.permission.checkWorkspaceIs(
       workspaceId,
       user.id,
-      Permission.Owner
+      WorkspaceRole.Owner
     );
 
     return this.service.activateTeamLicense(workspaceId, license);
@@ -98,7 +98,7 @@ export class LicenseResolver {
     await this.permission.checkWorkspaceIs(
       workspaceId,
       user.id,
-      Permission.Owner
+      WorkspaceRole.Owner
     );
 
     return this.service.deactivateTeamLicense(workspaceId);
@@ -116,7 +116,7 @@ export class LicenseResolver {
     await this.permission.checkWorkspaceIs(
       workspaceId,
       user.id,
-      Permission.Owner
+      WorkspaceRole.Owner
     );
 
     const { url } = await this.service.createCustomerPortal(workspaceId);
diff --git a/packages/backend/server/src/plugins/payment/resolver.ts b/packages/backend/server/src/plugins/payment/resolver.ts
index 45c7344e027c1..2153ceaf06eaa 100644
--- a/packages/backend/server/src/plugins/payment/resolver.ts
+++ b/packages/backend/server/src/plugins/payment/resolver.ts
@@ -27,7 +27,7 @@ import {
   WorkspaceIdRequiredToUpdateTeamSubscription,
 } from '../../base';
 import { CurrentUser, Public } from '../../core/auth';
-import { Permission, PermissionService } from '../../core/permission';
+import { PermissionService, WorkspaceRole } from '../../core/permission';
 import { UserType } from '../../core/user';
 import { WorkspaceType } from '../../core/workspaces';
 import { Invoice, Subscription, WorkspaceSubscriptionManager } from './manager';
@@ -541,7 +541,11 @@ export class WorkspaceSubscriptionResolver {
     @CurrentUser() me: CurrentUser,
     @Parent() workspace: WorkspaceType
   ) {
-    await this.permission.checkWorkspace(workspace.id, me.id, Permission.Owner);
+    await this.permission.checkWorkspace(
+      workspace.id,
+      me.id,
+      WorkspaceRole.Owner
+    );
     return this.db.invoice.count({
       where: {
         targetId: workspace.id,
@@ -557,7 +561,11 @@ export class WorkspaceSubscriptionResolver {
     take: number,
     @Args('skip', { type: () => Int, nullable: true }) skip?: number
   ) {
-    await this.permission.checkWorkspace(workspace.id, me.id, Permission.Owner);
+    await this.permission.checkWorkspace(
+      workspace.id,
+      me.id,
+      WorkspaceRole.Owner
+    );
 
     return this.db.invoice.findMany({
       where: {
diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql
index 69c0e60d4cee4..a8878d0524051 100644
--- a/packages/backend/server/src/schema.gql
+++ b/packages/backend/server/src/schema.gql
@@ -204,12 +204,28 @@ type DocNotFoundDataType {
   spaceId: String!
 }
 
+"""User permission in doc"""
+enum DocRole {
+  Editor
+  External
+  Manager
+  Owner
+  Reader
+}
+
+type DocType {
+  id: String!
+  permissions: RolePermissions!
+  public: Boolean!
+  role: DocRole!
+}
+
 type EditorType {
   avatarUrl: String
   name: String!
 }
 
-union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidLicenseUpdateParamsDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WorkspaceMembersExceedLimitToDowngradeDataType | WrongSignInCredentialsDataType
+union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidLicenseUpdateParamsDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WorkspaceMembersExceedLimitToDowngradeDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType
 
 enum ErrorNames {
   ACCESS_DENIED
@@ -240,8 +256,11 @@ enum ErrorNames {
   EMAIL_ALREADY_USED
   EMAIL_TOKEN_NOT_FOUND
   EMAIL_VERIFICATION_REQUIRED
+  EXPECT_TO_GRANT_DOC_USER_ROLES
   EXPECT_TO_PUBLISH_PAGE
+  EXPECT_TO_REVOKE_DOC_USER_ROLES
   EXPECT_TO_REVOKE_PUBLIC_PAGE
+  EXPECT_TO_UPDATE_DOC_USER_ROLE
   FAILED_TO_CHECKOUT
   FAILED_TO_SAVE_UPDATES
   FAILED_TO_UPSERT_SNAPSHOT
@@ -279,6 +298,7 @@ enum ErrorNames {
   SPACE_ACCESS_DENIED
   SPACE_NOT_FOUND
   SPACE_OWNER_NOT_FOUND
+  SPACE_SHOULD_HAVE_ONLY_ONE_OWNER
   SUBSCRIPTION_ALREADY_EXISTS
   SUBSCRIPTION_EXPIRED
   SUBSCRIPTION_HAS_BEEN_CANCELED
@@ -296,10 +316,26 @@ enum ErrorNames {
   WORKSPACE_ID_REQUIRED_TO_UPDATE_TEAM_SUBSCRIPTION
   WORKSPACE_LICENSE_ALREADY_EXISTS
   WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE
+  WORKSPACE_PERMISSION_NOT_FOUND
   WRONG_SIGN_IN_CREDENTIALS
   WRONG_SIGN_IN_METHOD
 }
 
+type ExpectToGrantDocUserRolesDataType {
+  docId: String!
+  spaceId: String!
+}
+
+type ExpectToRevokeDocUserRolesDataType {
+  docId: String!
+  spaceId: String!
+}
+
+type ExpectToUpdateDocUserRoleDataType {
+  docId: String!
+  spaceId: String!
+}
+
 """The type of workspace feature"""
 enum FeatureType {
   AIEarlyAccess
@@ -321,6 +357,29 @@ input ForkChatSessionInput {
   workspaceId: String!
 }
 
+input GrantDocUserRolesInput {
+  docId: String!
+  role: DocRole!
+  userIds: [String!]!
+  workspaceId: String!
+}
+
+type GrantedDocUserEdge {
+  cursor: String!
+  user: GrantedDocUserType!
+}
+
+type GrantedDocUserType {
+  role: DocRole!
+  user: UserType!
+}
+
+type GrantedDocUsersConnection {
+  edges: [GrantedDocUserEdge!]!
+  pageInfo: PageInfo!
+  totalCount: Int!
+}
+
 type HumanReadableQuotaType {
   blobLimit: String!
   copilotActionLimit: String
@@ -418,7 +477,10 @@ type InviteUserType {
   name: String
 
   """User permission in workspace"""
-  permission: Permission!
+  permission: Permission! @deprecated(reason: "Use role instead")
+
+  """User role in workspace"""
+  role: Permission!
 
   """Member invite status in workspace"""
   status: WorkspaceMemberStatus!
@@ -548,6 +610,7 @@ type Mutation {
   """Create a chat session"""
   forkCopilotSession(options: ForkChatSessionInput!): String!
   generateLicenseKey(sessionId: String!): String!
+  grantDocUserRoles(input: GrantDocUserRolesInput!): Boolean!
   grantMember(permission: Permission!, userId: String!, workspaceId: String!): String!
   invite(email: String!, permission: Permission @deprecated(reason: "never used"), sendInviteMail: Boolean, workspaceId: String!): String!
   inviteBatch(emails: [String!]!, sendInviteMail: Boolean, workspaceId: String!): [InviteResult!]!
@@ -561,6 +624,7 @@ type Mutation {
   removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Int!
   resumeSubscription(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, workspaceId: String): SubscriptionType!
   revoke(userId: String!, workspaceId: String!): Boolean!
+  revokeDocUserRoles(docId: String!, userIds: [String!]!): Boolean!
   revokeInviteLink(workspaceId: String!): Boolean!
   revokePage(pageId: String!, workspaceId: String!): Boolean! @deprecated(reason: "use revokePublicPage")
   revokePublicPage(pageId: String!, workspaceId: String!): WorkspacePage!
@@ -578,6 +642,7 @@ type Mutation {
 
   """Update a chat session"""
   updateCopilotSession(options: UpdateChatSessionInput!): String!
+  updateDocUserRole(docId: String!, role: DocRole!, userId: String!): Boolean!
   updateProfile(input: UpdateUserInput!): UserType!
 
   """update server runtime configurable setting"""
@@ -611,6 +676,23 @@ enum OAuthProviderType {
   OIDC
 }
 
+input PageGrantedUsersInput {
+  """Cursor"""
+  after: String
+
+  """Cursor"""
+  before: String
+  first: Int!
+  offset: Int!
+}
+
+type PageInfo {
+  endCursor: String
+  hasNextPage: Boolean!
+  hasPreviousPage: Boolean!
+  startCursor: String
+}
+
 type PasswordLimitsType {
   maxLength: Int!
   minLength: Int!
@@ -619,9 +701,9 @@ type PasswordLimitsType {
 """User permission in workspace"""
 enum Permission {
   Admin
+  Collaborator
+  External
   Owner
-  Read
-  Write
 }
 
 """The mode which the public page default in"""
@@ -651,7 +733,7 @@ type Query {
 
   """List all copilot prompts"""
   listCopilotPrompts: [CopilotPromptType!]!
-  listWorkspaceFeatures(feature: FeatureType!): [WorkspaceType!]!
+  listWorkspaceFeatures(feature: FeatureType!): [WorkspaceFeatureType!]!
   prices: [SubscriptionPrice!]!
 
   """server config"""
@@ -679,6 +761,9 @@ type Query {
   """Get workspace by id"""
   workspace(id: String!): WorkspaceType!
 
+  """Get workspace role permissions"""
+  workspaceRolePermissions(id: String!): WorkspaceRolePermissions!
+
   """Get all accessible workspaces for current user"""
   workspaces: [WorkspaceType!]!
 }
@@ -713,6 +798,35 @@ type RemoveAvatar {
   success: Boolean!
 }
 
+type RolePermissions {
+  Doc_Copy: Boolean!
+  Doc_Delete: Boolean!
+  Doc_Duplicate: Boolean!
+  Doc_Properties_Read: Boolean!
+  Doc_Properties_Update: Boolean!
+  Doc_Publish: Boolean!
+  Doc_Read: Boolean!
+  Doc_Restore: Boolean!
+  Doc_TransferOwner: Boolean!
+  Doc_Trash: Boolean!
+  Doc_Update: Boolean!
+  Doc_Users_Manage: Boolean!
+  Doc_Users_Read: Boolean!
+  Workspace_CreateDoc: Boolean!
+  Workspace_Delete: Boolean!
+  Workspace_Organize_Read: Boolean!
+  Workspace_Properties_Create: Boolean!
+  Workspace_Properties_Delete: Boolean!
+  Workspace_Properties_Read: Boolean!
+  Workspace_Properties_Update: Boolean!
+  Workspace_Settings_Read: Boolean!
+  Workspace_Settings_Update: Boolean!
+  Workspace_Sync: Boolean!
+  Workspace_TransferOwner: Boolean!
+  Workspace_Users_Manage: Boolean!
+  Workspace_Users_Read: Boolean!
+}
+
 type RuntimeConfigNotFoundDataType {
   key: String!
 }
@@ -814,6 +928,10 @@ type SpaceOwnerNotFoundDataType {
   spaceId: String!
 }
 
+type SpaceShouldHaveOnlyOneOwnerDataType {
+  spaceId: String!
+}
+
 type SubscriptionAlreadyExistsDataType {
   plan: String!
 }
@@ -988,6 +1106,15 @@ type WorkspaceBlobSizes {
   size: SafeInt!
 }
 
+type WorkspaceFeatureType {
+  """Workspace created date"""
+  createdAt: DateTime!
+  id: ID!
+
+  """is Public workspace"""
+  public: Boolean!
+}
+
 """Workspace invite link expire time"""
 enum WorkspaceInviteLinkExpireTime {
   OneDay
@@ -1023,6 +1150,31 @@ type WorkspacePageMeta {
   updatedBy: EditorType
 }
 
+type WorkspacePermissionNotFoundDataType {
+  spaceId: String!
+}
+
+type WorkspacePermissions {
+  Workspace_CreateDoc: Boolean!
+  Workspace_Delete: Boolean!
+  Workspace_Organize_Read: Boolean!
+  Workspace_Properties_Create: Boolean!
+  Workspace_Properties_Delete: Boolean!
+  Workspace_Properties_Read: Boolean!
+  Workspace_Properties_Update: Boolean!
+  Workspace_Settings_Read: Boolean!
+  Workspace_Settings_Update: Boolean!
+  Workspace_Sync: Boolean!
+  Workspace_TransferOwner: Boolean!
+  Workspace_Users_Manage: Boolean!
+  Workspace_Users_Read: Boolean!
+}
+
+type WorkspaceRolePermissions {
+  permissions: WorkspacePermissions!
+  role: Permission!
+}
+
 type WorkspaceType {
   """Available features of workspace"""
   availableFeatures: [FeatureType!]!
@@ -1069,11 +1221,14 @@ type WorkspaceType {
   """Owner of workspace"""
   owner: UserType!
 
+  """Page granted users list"""
+  pageGrantedUsersList(pageGrantedUsersInput: PageGrantedUsersInput!, pageId: String!): GrantedDocUsersConnection!
+
   """Cloud page metadata of workspace"""
   pageMeta(pageId: String!): WorkspacePageMeta!
 
-  """Permission of current signed in user in workspace"""
-  permission: Permission!
+  """Check if current user has permission to access the page"""
+  pagePermission(pageId: String!): DocType!
 
   """is Public workspace"""
   public: Boolean!
@@ -1087,6 +1242,9 @@ type WorkspaceType {
   """quota of workspace"""
   quota: QuotaQueryType!
 
+  """Role of current signed in user in workspace"""
+  role: Permission!
+
   """Shared pages of workspace"""
   sharedPages: [String!]! @deprecated(reason: "use WorkspaceType.publicPages")