diff --git a/apps/web/src/app/(dashboard)/admin/layout.tsx b/apps/web/src/app/(dashboard)/admin/layout.tsx
new file mode 100644
index 000000000..340605bc7
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/admin/layout.tsx
@@ -0,0 +1,23 @@
+import { redirect } from 'next/navigation';
+
+import { isAdmin } from '@documenso/lib/';
+import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session';
+
+export type AdminLayoutProps = {
+ children: React.ReactNode;
+};
+
+export default async function AdminLayout({ children }: AdminLayoutProps) {
+ const user = await getRequiredServerComponentSession();
+ const isUserAdmin = isAdmin(user);
+
+ if (!user) {
+ redirect('/signin');
+ }
+
+ if (!isUserAdmin) {
+ redirect('/dashboard');
+ }
+
+ return {children};
+}
diff --git a/apps/web/src/app/(dashboard)/admin/page.tsx b/apps/web/src/app/(dashboard)/admin/page.tsx
new file mode 100644
index 000000000..e4a62f725
--- /dev/null
+++ b/apps/web/src/app/(dashboard)/admin/page.tsx
@@ -0,0 +1,107 @@
+import {
+ Archive,
+ File,
+ FileX2,
+ LucideIcon,
+ User as LucideUser,
+ Mail,
+ MailOpen,
+ PenTool,
+ Send,
+ UserPlus2,
+ UserSquare2,
+} from 'lucide-react';
+
+import { getDocsCount } from '@documenso/lib/server-only/admin/get-documents';
+import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients';
+import {
+ getUsersCount,
+ getUsersWithSubscriptionsCount,
+} from '@documenso/lib/server-only/admin/get-users';
+import {
+ ReadStatus as InternalReadStatus,
+ SendStatus as InternalSendStatus,
+ SigningStatus as InternalSigningStatus,
+} from '@documenso/prisma/client';
+
+import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card';
+
+type TCardData = {
+ icon: LucideIcon;
+ title: string;
+ status:
+ | 'TOTAL_RECIPIENTS'
+ | 'OPENED'
+ | 'NOT_OPENED'
+ | 'SIGNED'
+ | 'NOT_SIGNED'
+ | 'SENT'
+ | 'NOT_SENT';
+}[];
+
+const CARD_DATA: TCardData = [
+ {
+ icon: UserSquare2,
+ title: 'Total recipients in the database',
+ status: 'TOTAL_RECIPIENTS',
+ },
+ {
+ icon: MailOpen,
+ title: 'Total recipients with opened count',
+ status: InternalReadStatus.OPENED,
+ },
+ {
+ icon: Mail,
+ title: 'Total recipients with unopened count',
+ status: InternalReadStatus.NOT_OPENED,
+ },
+ {
+ icon: Send,
+ title: 'Total recipients with sent count',
+ status: InternalSendStatus.SENT,
+ },
+ {
+ icon: Archive,
+ title: 'Total recipients with unsent count',
+ status: InternalSendStatus.NOT_SENT,
+ },
+ {
+ icon: PenTool,
+ title: 'Total recipients with signed count',
+ status: InternalSigningStatus.SIGNED,
+ },
+ {
+ icon: FileX2,
+ title: 'Total recipients with unsigned count',
+ status: InternalSigningStatus.NOT_SIGNED,
+ },
+];
+
+export default async function Admin() {
+ const [usersCount, usersWithSubscriptionsCount, docsCount, recipientsStats] = await Promise.all([
+ getUsersCount(),
+ getUsersWithSubscriptionsCount(),
+ getDocsCount(),
+ getRecipientsStats(),
+ ]);
+
+ return (
+
+
Documenso instance metrics
+
+
+
+
+ {CARD_DATA.map((card) => (
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx
index 19a15564b..0bea64565 100644
--- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx
+++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx
@@ -38,7 +38,7 @@ export type ProfileDropdownProps = {
export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
const { theme, setTheme } = useTheme();
const { getFlag } = useFeatureFlags();
- const userIsAdmin = isAdmin(user);
+ const isUserAdmin = isAdmin(user);
const isBillingEnabled = getFlag('app_billing');
@@ -69,7 +69,7 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => {
- {userIsAdmin && (
+ {isUserAdmin && (
diff --git a/packages/lib/server-only/admin/get-documents.ts b/packages/lib/server-only/admin/get-documents.ts
new file mode 100644
index 000000000..9100a886c
--- /dev/null
+++ b/packages/lib/server-only/admin/get-documents.ts
@@ -0,0 +1,5 @@
+import { prisma } from '@documenso/prisma';
+
+export const getDocsCount = async () => {
+ return await prisma.document.count();
+};
diff --git a/packages/lib/server-only/admin/get-recipients.ts b/packages/lib/server-only/admin/get-recipients.ts
new file mode 100644
index 000000000..0be612e55
--- /dev/null
+++ b/packages/lib/server-only/admin/get-recipients.ts
@@ -0,0 +1,20 @@
+import { prisma } from '@documenso/prisma';
+import { ReadStatus, SendStatus, SigningStatus } from '@documenso/prisma/client';
+
+export const getRecipientsStats = async () => {
+ const results = await prisma.recipient.groupBy({
+ by: ['readStatus', 'signingStatus', 'sendStatus'],
+ _count: true,
+ });
+
+ return {
+ TOTAL_RECIPIENTS: results.length,
+ [ReadStatus.OPENED]: results.filter((r) => r.readStatus === 'OPENED')?.[0]?._count ?? 0,
+ [ReadStatus.NOT_OPENED]: results.filter((r) => r.readStatus === 'NOT_OPENED')?.[0]?._count ?? 0,
+ [SigningStatus.SIGNED]: results.filter((r) => r.signingStatus === 'SIGNED')?.[0]?._count ?? 0,
+ [SigningStatus.NOT_SIGNED]:
+ results.filter((r) => r.signingStatus === 'NOT_SIGNED')?.[0]?._count ?? 0,
+ [SendStatus.SENT]: results.filter((r) => r.sendStatus === 'SENT')?.[0]?._count ?? 0,
+ [SendStatus.NOT_SENT]: results.filter((r) => r.sendStatus === 'NOT_SENT')?.[0]?._count ?? 0,
+ };
+};
diff --git a/packages/lib/server-only/admin/get-users.ts b/packages/lib/server-only/admin/get-users.ts
new file mode 100644
index 000000000..09892171a
--- /dev/null
+++ b/packages/lib/server-only/admin/get-users.ts
@@ -0,0 +1,18 @@
+import { prisma } from '@documenso/prisma';
+import { SubscriptionStatus } from '@documenso/prisma/client';
+
+export const getUsersCount = async () => {
+ return await prisma.user.count();
+};
+
+export const getUsersWithSubscriptionsCount = async () => {
+ return await prisma.user.count({
+ where: {
+ Subscription: {
+ some: {
+ status: SubscriptionStatus.ACTIVE,
+ },
+ },
+ },
+ });
+};