diff --git a/apps/web/src/views/members/index.tsx b/apps/web/src/views/members/index.tsx
index da1dc000..a31e62d7 100644
--- a/apps/web/src/views/members/index.tsx
+++ b/apps/web/src/views/members/index.tsx
@@ -54,6 +54,7 @@ export default function MembersPage() {
memberStatus,
isLastRow,
showSkeleton,
+ showPendingIcon,
}: {
memberPublicId?: string;
memberId?: string | null | undefined;
@@ -64,6 +65,7 @@ export default function MembersPage() {
memberStatus?: string;
isLastRow?: boolean;
showSkeleton?: boolean;
+ showPendingIcon?: boolean;
}) => {
return (
@@ -82,6 +84,7 @@ export default function MembersPage() {
name={memberName ?? ""}
email={memberEmail ?? ""}
imageUrl={memberImage ? getAvatarUrl(memberImage) : undefined}
+ icon={showPendingIcon ? "?" : undefined}
/>
)}
@@ -93,20 +96,26 @@ export default function MembersPage() {
"mr-2 truncate text-xs font-medium text-neutral-900 dark:text-dark-1000 sm:text-sm",
showSkeleton &&
"md mb-2 h-3 w-[125px] animate-pulse rounded-sm bg-light-200 dark:bg-dark-200",
+ showPendingIcon &&
+ "italic text-neutral-500 dark:text-dark-900",
)}
>
{memberName}
-
- {memberEmail}
-
+ {((workspace.role === "admin" ||
+ data?.showEmailsToMembers === true) ||
+ showSkeleton) && (
+
+ {memberEmail}
+
+ )}
@@ -246,19 +255,24 @@ export default function MembersPage() {
{!isLoading &&
- data?.members.map((member, index) => (
-
- ))}
+ data?.members.map((member, index) => {
+ const isPendingInvite = member.status === "invited";
+
+ return (
+
+ );
+ })}
{isLoading && (
<>
diff --git a/apps/web/src/views/settings/WorkspaceSettings.tsx b/apps/web/src/views/settings/WorkspaceSettings.tsx
index 1d206e07..8c20c155 100644
--- a/apps/web/src/views/settings/WorkspaceSettings.tsx
+++ b/apps/web/src/views/settings/WorkspaceSettings.tsx
@@ -17,6 +17,7 @@ import { useWorkspace } from "~/providers/workspace";
import { api } from "~/utils/api";
import { DeleteWorkspaceConfirmation } from "./components/DeleteWorkspaceConfirmation";
import UpdateWorkspaceDescriptionForm from "./components/UpdateWorkspaceDescriptionForm";
+import UpdateWorkspaceEmailVisibilityForm from "./components/UpdateWorkspaceEmailVisibilityForm";
import UpdateWorkspaceNameForm from "./components/UpdateWorkspaceNameForm";
import UpdateWorkspaceUrlForm from "./components/UpdateWorkspaceUrlForm";
import { UpgradeToProConfirmation } from "./components/UpgradeToProConfirmation";
@@ -79,6 +80,14 @@ export default function WorkspaceSettings() {
workspaceDescription={workspace.description ?? ""}
/>
+
+ {t`Email visibility`}
+
+
+
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" &&
!hasActiveSubscription(subscriptions, "pro") &&
!hasActiveSubscription(subscriptions, "team") && (
diff --git a/apps/web/src/views/settings/components/UpdateWorkspaceEmailVisibilityForm.tsx b/apps/web/src/views/settings/components/UpdateWorkspaceEmailVisibilityForm.tsx
new file mode 100644
index 00000000..c8c5ccae
--- /dev/null
+++ b/apps/web/src/views/settings/components/UpdateWorkspaceEmailVisibilityForm.tsx
@@ -0,0 +1,53 @@
+import { t } from "@lingui/core/macro";
+import { useEffect, useState } from "react";
+
+import Toggle from "~/components/Toggle";
+import { api } from "~/utils/api";
+
+export default function UpdateWorkspaceEmailVisibilityForm({
+ workspacePublicId,
+ showEmailsToMembers,
+}: {
+ workspacePublicId: string;
+ showEmailsToMembers: boolean;
+}) {
+ const utils = api.useUtils();
+ const [isChecked, setIsChecked] = useState(showEmailsToMembers);
+
+ useEffect(() => {
+ setIsChecked(showEmailsToMembers);
+ }, [showEmailsToMembers]);
+
+ const updateWorkspace = api.workspace.update.useMutation({
+ onSuccess: () => {
+ void utils.workspace.byId.invalidate({
+ workspacePublicId,
+ });
+ },
+ });
+
+ const handleToggle = () => {
+ const newValue = !isChecked;
+ setIsChecked(newValue);
+ updateWorkspace.mutate({
+ workspacePublicId,
+ showEmailsToMembers: newValue,
+ });
+ };
+
+ return (
+
+
+
+ {t`Allow workspace members to see each other's email addresses`}
+
+
+
+
+ );
+}
diff --git a/packages/api/src/routers/workspace.ts b/packages/api/src/routers/workspace.ts
index f59c21d2..26060032 100644
--- a/packages/api/src/routers/workspace.ts
+++ b/packages/api/src/routers/workspace.ts
@@ -77,6 +77,49 @@ export const workspaceRouter = createTRPCRouter({
await assertUserInWorkspace(ctx.db, userId, result.id);
+ // Check if user is an admin
+ const userMember = result.members.find(
+ (member) => member.user?.id === userId,
+ );
+ const isAdmin = userMember?.role === "admin";
+
+ // Show emails if user is admin OR workspace setting allows it
+ const shouldShowEmails = isAdmin || result.showEmailsToMembers === true;
+
+ // If emails should be hidden, filter them out
+ if (!shouldShowEmails) {
+ const sanitizedMembers = result.members.map((member) => {
+ // If user doesn't have a display name, use anonymous identifier
+ const displayName =
+ member.user?.name?.trim() ?? `anonymous_${member.publicId}`;
+
+ const { email: _memberEmail, ...memberWithoutEmail } = member;
+ const sanitizedUser = member.user
+ ? (() => {
+ const { email: _userEmail, ...userWithoutEmail } = member.user;
+ return {
+ ...userWithoutEmail,
+ name: displayName,
+ };
+ })()
+ : {
+ id: null,
+ name: displayName,
+ image: null,
+ };
+
+ return {
+ ...memberWithoutEmail,
+ user: sanitizedUser,
+ };
+ });
+
+ return {
+ ...result,
+ members: sanitizedMembers,
+ } as Awaited>;
+ }
+
return result;
}),
bySlug: publicProcedure
@@ -230,6 +273,7 @@ export const workspaceRouter = createTRPCRouter({
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/)
.optional(),
description: z.string().min(3).max(280).optional(),
+ showEmailsToMembers: z.boolean().optional(),
}),
)
.output(z.custom>>())
@@ -291,9 +335,16 @@ export const workspaceRouter = createTRPCRouter({
name: input.name,
slug: input.slug,
description: input.description,
+ showEmailsToMembers: input.showEmailsToMembers,
},
);
+ if (!result)
+ throw new TRPCError({
+ message: `Unable to delete workspace`,
+ code: "INTERNAL_SERVER_ERROR",
+ });
+
return result;
}),
delete: protectedProcedure
@@ -336,12 +387,6 @@ export const workspaceRouter = createTRPCRouter({
input.workspacePublicId,
);
- if (!result)
- throw new TRPCError({
- message: `Unable to delete workspace`,
- code: "INTERNAL_SERVER_ERROR",
- });
-
return result;
}),
checkSlugAvailability: publicProcedure
diff --git a/packages/db/migrations/20260119164257_AddShowEmailsToMembersToWorkspace.sql b/packages/db/migrations/20260119164257_AddShowEmailsToMembersToWorkspace.sql
new file mode 100644
index 00000000..ac2f56e5
--- /dev/null
+++ b/packages/db/migrations/20260119164257_AddShowEmailsToMembersToWorkspace.sql
@@ -0,0 +1 @@
+ALTER TABLE "workspace" ADD COLUMN "showEmailsToMembers" boolean NOT NULL DEFAULT true;
diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json
index 79b06df3..87007a4a 100644
--- a/packages/db/migrations/meta/_journal.json
+++ b/packages/db/migrations/meta/_journal.json
@@ -155,6 +155,13 @@
"when": 1767045713686,
"tag": "20251229220153_UpdateCardTitleFromVarcharToText",
"breakpoints": true
+ },
+ {
+ "idx": 22,
+ "version": "7",
+ "when": 1768858977000,
+ "tag": "20260119164257_AddShowEmailsToMembersToWorkspace",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/db/src/repository/workspace.repo.ts b/packages/db/src/repository/workspace.repo.ts
index 3afeef54..1ebd531e 100644
--- a/packages/db/src/repository/workspace.repo.ts
+++ b/packages/db/src/repository/workspace.repo.ts
@@ -82,6 +82,7 @@ export const update = async (
slug?: string;
plan?: "free" | "pro" | "enterprise";
description?: string;
+ showEmailsToMembers?: boolean;
},
) => {
const [result] = await db
@@ -91,6 +92,7 @@ export const update = async (
slug: workspaceInput.slug,
plan: workspaceInput.plan,
description: workspaceInput.description,
+ showEmailsToMembers: workspaceInput.showEmailsToMembers,
})
.where(eq(workspaces.publicId, workspacePublicId))
.returning({
@@ -100,6 +102,7 @@ export const update = async (
slug: workspaces.slug,
description: workspaces.description,
plan: workspaces.plan,
+ showEmailsToMembers: workspaces.showEmailsToMembers,
});
return result;
@@ -139,6 +142,7 @@ export const getByPublicIdWithMembers = (
columns: {
id: true,
publicId: true,
+ showEmailsToMembers: true,
},
with: {
members: {
diff --git a/packages/db/src/schema/workspaces.ts b/packages/db/src/schema/workspaces.ts
index ae86d141..4fccc8b1 100644
--- a/packages/db/src/schema/workspaces.ts
+++ b/packages/db/src/schema/workspaces.ts
@@ -43,6 +43,7 @@ export const workspaces = pgTable("workspace", {
description: text("description"),
slug: varchar("slug", { length: 255 }).notNull().unique(),
plan: workspacePlanEnum("plan").notNull().default("free"),
+ showEmailsToMembers: boolean("showEmailsToMembers").notNull().default(true),
createdBy: uuid("createdBy").references(() => users.id, {
onDelete: "set null",
}),