diff --git a/app/components/PackageDependents.vue b/app/components/PackageDependents.vue
new file mode 100644
index 00000000..b521442b
--- /dev/null
+++ b/app/components/PackageDependents.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+ Dependents
+ (top {{ total.toLocaleString() }})
+
+
+
+
+
+
+
+ -
+
+ {{ dependent.name }}
+
+
+
+ {{ formatCompactNumber(dependent.downloads, { decimals: 1 }) }}
+
+
+
+
+
+
+
+
diff --git a/app/composables/useNpmRegistry.ts b/app/composables/useNpmRegistry.ts
index 22c01617..bb2123f3 100644
--- a/app/composables/useNpmRegistry.ts
+++ b/app/composables/useNpmRegistry.ts
@@ -608,6 +608,51 @@ export function getOutdatedTooltip(info: OutdatedDependencyInfo): string {
return `Patch update available (latest: ${info.latest})`
}
+// ============================================================================
+// Package Dependents
+// ============================================================================
+
+export interface DependentPackage {
+ name: string
+ downloads: number
+ description?: string
+ version?: string
+}
+
+export interface DependentsResponse {
+ dependents: DependentPackage[]
+ total: number
+}
+
+const emptyDependentsResponse: DependentsResponse = {
+ dependents: [],
+ total: 0,
+}
+
+/**
+ * Fetch packages that depend on a given package (dependents).
+ * Uses the e18e CouchDB mirror to get accurate dependency data.
+ * Results are sorted by download count (most downloaded first)
+ * to help with security triage when vulnerabilities are discovered.
+ */
+export function usePackageDependents(packageName: MaybeRefOrGetter) {
+ return useLazyAsyncData(
+ () => `dependents:${toValue(packageName)}`,
+ async () => {
+ const name = toValue(packageName)
+ if (!name) return emptyDependentsResponse
+
+ return await $fetch(
+ `/api/registry/dependents/${encodeURIComponent(name)}`,
+ )
+ },
+ {
+ server: false,
+ default: () => emptyDependentsResponse,
+ },
+ )
+}
+
/**
* Get CSS class for a dependency version based on outdated status
*/
diff --git a/app/pages/[...package].vue b/app/pages/[...package].vue
index dbe71d17..7769ad44 100644
--- a/app/pages/[...package].vue
+++ b/app/pages/[...package].vue
@@ -892,6 +892,9 @@ defineOgImageComponent('Package', {
:peer-dependencies-meta="displayVersion?.peerDependenciesMeta"
:optional-dependencies="displayVersion?.optionalDependencies"
/>
+
+
+
diff --git a/server/api/registry/dependents/[...pkg].get.ts b/server/api/registry/dependents/[...pkg].get.ts
new file mode 100644
index 00000000..aa44e2c5
--- /dev/null
+++ b/server/api/registry/dependents/[...pkg].get.ts
@@ -0,0 +1,112 @@
+import * as v from 'valibot'
+import { PackageRouteParamsSchema } from '#shared/schemas/package'
+import { CACHE_MAX_AGE_ONE_HOUR } from '#shared/utils/constants'
+
+const E18E_LIVE_REGISTRY_URL = 'https://npm.devminer.xyz/live_registry'
+const E18E_REGISTRY_URL = 'https://npm.devminer.xyz/registry'
+
+interface DependentsViewResponse {
+ total_rows: number
+ offset: number
+ rows: {
+ id: string
+ key: string
+ value: { name: string; version: string }
+ }[]
+}
+
+interface DownloadsViewResponse {
+ total_rows: number
+ offset: number
+ rows: {
+ id: string
+ key: string
+ value: number
+ }[]
+}
+
+export interface DependentPackage {
+ name: string
+ downloads: number
+ version?: string
+}
+
+export interface DependentsResponse {
+ dependents: DependentPackage[]
+ total: number
+}
+
+/**
+ * GET /api/registry/dependents/:name
+ *
+ * Fetch packages that depend on the given package using the e18e CouchDB mirror.
+ * Uses CouchDB views for efficient lookups, then fetches download stats separately.
+ * Results are sorted by download count (most downloaded first) for security triage.
+ */
+export default defineCachedEventHandler(
+ async (event): Promise => {
+ const pkgParamSegments = getRouterParam(event, 'pkg')?.split('/') ?? []
+ const { rawPackageName } = parsePackageParams(pkgParamSegments)
+
+ try {
+ const { packageName } = v.parse(PackageRouteParamsSchema, {
+ packageName: rawPackageName,
+ })
+
+ const dependentsResponse = await $fetch(
+ `${E18E_LIVE_REGISTRY_URL}/_design/dependents/_view/dependents2?key=${encodeURIComponent(JSON.stringify(packageName))}&limit=250`,
+ )
+
+ if (dependentsResponse.rows.length === 0) {
+ return { dependents: [], total: 0 }
+ }
+
+ const packageNames = dependentsResponse.rows.map(row => row.value.name)
+
+ const downloadsResponse = await $fetch(
+ `${E18E_REGISTRY_URL}/_design/downloads/_view/downloads`,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: { keys: packageNames },
+ },
+ )
+
+ const downloadsMap = new Map()
+ for (const row of downloadsResponse.rows) {
+ downloadsMap.set(row.key, row.value)
+ }
+
+ const versionMap = new Map()
+ for (const row of dependentsResponse.rows) {
+ versionMap.set(row.value.name, row.value.version)
+ }
+
+ const dependents: DependentPackage[] = packageNames
+ .map(name => ({
+ name,
+ downloads: downloadsMap.get(name) ?? 0,
+ version: versionMap.get(name),
+ }))
+ .sort((a, b) => b.downloads - a.downloads)
+
+ return {
+ dependents,
+ total: dependents.length,
+ }
+ } catch {
+ return {
+ dependents: [],
+ total: 0,
+ }
+ }
+ },
+ {
+ maxAge: CACHE_MAX_AGE_ONE_HOUR,
+ swr: true,
+ getKey: event => {
+ const pkg = getRouterParam(event, 'pkg') ?? ''
+ return `dependents:v2:${pkg.replace(/\/+$/, '').trim()}`
+ },
+ },
+)