diff --git a/web/src/api/license/api.ts b/web/src/api/license/api.ts
new file mode 100644
index 0000000..d6199da
--- /dev/null
+++ b/web/src/api/license/api.ts
@@ -0,0 +1,52 @@
+//
+// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
+//
+// This file is part of the Bichon Email Archiving Project
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import axiosInstance from '@/api/axiosInstance'
+
+export interface LicenseStatusResponse {
+ status: string
+ email?: string | null
+ edition?: string | null
+ updates_until?: string | null
+ features?: string[] | null
+ days_remaining?: number | null
+ build_date?: string | null
+ valid_until?: string | null
+ machine_id: string
+ account_limit?: number | null
+ accounts_used: number
+}
+
+export interface UploadLicenseResponse {
+ success: boolean
+ email: string
+ edition: string
+ updates_until: string
+}
+
+export async function get_license_status(): Promise {
+ const { data } = await axiosInstance.get('api/v1/license/status')
+ return data
+}
+
+export async function upload_license(license: string): Promise {
+ const { data } = await axiosInstance.post('api/v1/license/upload', {
+ license,
+ })
+ return data
+}
diff --git a/web/src/components/layout/data/sidebar-data.ts b/web/src/components/layout/data/sidebar-data.ts
index 9aea766..8202113 100644
--- a/web/src/components/layout/data/sidebar-data.ts
+++ b/web/src/components/layout/data/sidebar-data.ts
@@ -22,7 +22,7 @@ import {
IconLayoutDashboard,
IconSettings
} from '@tabler/icons-react'
-import { IdCard, Inbox, Paperclip, Search, Upload, Users2, ScrollText } from 'lucide-react'
+import { BadgeCheck, IdCard, Inbox, Paperclip, Search, Upload, Users2, ScrollText } from 'lucide-react'
import { type SidebarData } from '../types'
import { useTranslation } from 'react-i18next'
import { useCurrentUser } from '@/hooks/use-current-user'
@@ -34,6 +34,7 @@ export function useSidebarData(): SidebarData {
const { require_any_permission } = useCurrentUser()
const { features } = useEdition()
const auditEnabled = features.includes('audit_log')
+ const licenseEnabled = features.includes('license')
return {
navGroups: [
@@ -107,6 +108,12 @@ export function useSidebarData(): SidebarData {
url: '/api-docs',
icon: IconHelp,
},
+ {
+ title: t('navigation.license'),
+ url: '/license',
+ icon: BadgeCheck,
+ visible: licenseEnabled && require_any_permission(['system:root', 'user:manage']),
+ },
{
title: t('navigation.auditLog'),
url: '/audit-log',
diff --git a/web/src/features/license/index.tsx b/web/src/features/license/index.tsx
new file mode 100644
index 0000000..2c9cec6
--- /dev/null
+++ b/web/src/features/license/index.tsx
@@ -0,0 +1,290 @@
+//
+// Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com)
+//
+// This file is part of the Bichon Email Archiving Project
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import { AxiosError } from 'axios'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Copy, FileUp, Loader2 } from 'lucide-react'
+import { Badge } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from '@/components/ui/card'
+import { Label } from '@/components/ui/label'
+import { Textarea } from '@/components/ui/textarea'
+import { FixedHeader } from '@/components/layout/fixed-header'
+import { Main } from '@/components/layout/main'
+import { useEdition } from '@/hooks/use-edition'
+import { useCurrentUser } from '@/hooks/use-current-user'
+import { useToast } from '@/hooks/use-toast'
+import {
+ get_license_status,
+ upload_license,
+ type LicenseStatusResponse,
+} from '@/api/license/api'
+
+function formatEpoch(ts?: string | null): string {
+ if (!ts) return '—'
+ const n = Number(ts)
+ if (!Number.isFinite(n)) return ts
+ return new Date(n * 1000).toLocaleDateString()
+}
+
+function InfoRow({ label, children }: { label: string; children: React.ReactNode }) {
+ return (
+