Getting Started
Core Architecture
Link Engine
Analytics & Attribution
Partners & Affiliates
Third-Party Integrations
Identity & Security
Automation & Messaging
Developer Tools
The following files were used as context for generating this wiki page:
Admin Operations provides a centralized administrative dashboard for platform maintainers to manage user access, enforce security policies, investigate abuse, and moderate workspaces and partner programs across the system. It secures administrative endpoints through role verification and route middleware while offering comprehensive tooling for account impersonation, automated workspace banning, link and domain moderation, and support escalation workflows.
Sources: apps/web/lib/middleware/admin.ts:8-38, apps/web/app/ee/admin.dub.co/dashboard/page.tsx:7-59
The admin dashboard architecture secures administrative subdomains through dedicated route middleware and fine-grained access guards. Request validation follows a precise sequence starting from the incoming NextRequest and verifying authentication tokens against project membership, ensuring that only authorized users within the designated admin workspace can access administrative views.
The AdminMiddleware function processes incoming requests to the admin domain through the following execution path:
parse(req) — Extracts the requested request path (path).getUserViaToken(req) — Resolves the user session via authentication tokens. If no user is found and the path is not /login, the request is redirected to /login.prismaEdge.projectUsers.findUnique(...) — Queries edge database storage using the compound key userId_projectId evaluated against user.id and DUB_WORKSPACE_ID. If the user is not found in the admin project, NextResponse.next() is returned to surface a 404 response.canAccessAdminPath(...) — Evaluates whether the user ID is present in the admin access blocklist and whether the pathname intersects restricted paths. If access is restricted or the user visits /login, the request redirects to /.NextResponse.rewrite(...) — For authorized sessions, rewrites the request to internal admin routes prefixed with /admin.dub.co.Administrative access controls enforce hardcoded restrictions using user ID blocklists and path matching utilities. The system differentiates between page-level routes and API endpoints, maintaining distinct restriction arrays for each context.
Warning
Path-matching logic uses pathname === p || pathname.startsWith(p + "/"). This means restricting /links automatically restricts all nested sub-routes such as /links/analytics or /links/settings for any user present in the ADMIN_ACCESS_BLOCKLIST.
Account impersonation allows system administrators to query user accounts and generate authenticated session URLs for troubleshooting and support. The workflow spans client-side query parsing, backend user lookup and workspace serialization, and verification token tracking via in-memory data structures.
Sources: apps/web/app/ee/api/admin/impersonate/route.ts:113-221, apps/web/lib/auth/admin-impersonation.ts:1-20
The parseImpersonateQuery function processes raw administrator input, normalizing formats such as mailto: prefixes, Stripe customer IDs, app hostnames, and custom domains into structured type definitions.
Tip
When pasting identifiers into the ImpersonateUser form clipboard handler, the client strips mailto: prefixes and extracts Stripe customer IDs matching cus_[a-zA-Z0-9]+ before submitting the query body to /api/admin/impersonate.
Sources: apps/web/app/ee/admin.dub.co/dashboard/components/impersonate-user.tsx:104-118
Token consumption and tracking rely on pendingAdminImpersonations, a module-scoped Set<string> managed by helper functions in admin-impersonation.ts. These functions integrate directly into verification token validation workflows before token deletion occurs.
markAdminImpersonation(email) — Converts the target email to lowercase and adds it to the pendingAdminImpersonations set.consumeAdminImpersonation(email) — Checks if the lowercase email exists in pendingAdminImpersonations. If present, deletes it from the set and returns true, signaling a valid admin session creation.Note
pendingAdminImpersonations is stored entirely in memory. It tracks sign-ins via admin impersonation links specifically inside CustomPrismaAdapter.useVerificationToken prior to record removal.
The fraud enforcement and workspace ban pipeline executes administrative bans on users and their associated workspaces. The workflow combines Edge Config updates, database record transfers, link cache evictions, billing cancellations, Stripe fraud list additions, and background cleanup managed via Vercel waitUntil and QStash background jobs.
Sources: apps/web/app/ee/api/admin/ban/route.ts:11-87, apps/web/app/ee/api/admin/ban/delete-workspace-admin.ts:19-121
When an administrator initiates a ban via POST /api/admin/ban, the request passes through the withAdmin authorization wrapper requiring the owner role before executing the main handler.
prisma.user.findUniqueOrThrow() — Queries the target user by email, fetching their unique identifier, image URL, and all owned workspaces (projects where role: "owner"), including workspace metadata (id, slug, logo, stripeId, defaultProgramId).extractEmailDomain(email) — Extracts the domain from the user's email address for domain-level blocking.updateConfig() — Concurrently pushes the banned email to the emails Edge Config key and, if blockEmailDomain is true, the domain to emailDomainTerms.waitUntil() — Defers asynchronous post-response cleanup, executing deleteWorkspaceAdmin() for each associated project via Promise.allSettled.prisma.user.delete() — Removes the user record from the database after all workspaces finish deleting.storage.delete() — Deletes the user profile image from cloud storage via storage.delete() if it resides on the configured R2 storage URL (isStored(user.image)).Warning
Edge Config updates execute before workspace deletion and user removal. This sequence ensures that automated feedback or notification emails are immediately blocked at the edge before downstream database deletions trigger.
The deleteWorkspaceAdmin routine handles individual workspace destruction in batches, migrating links and purging associated infrastructure assets.
projectId) whose domain matches any entry in DUB_DOMAINS_ARRAY. If no links remain, the loop breaks.linkCache.expireMany(defaultDomainLinks) to evict links from cache and prisma.link.updateMany() to reassign the link ownership to LEGAL_WORKSPACE_ID and LEGAL_USER_ID.Promise.allSettled:
workspace.logo.startsWith(R2_URL + "/logos/" + workspace.id)).cancelSubscription()), retrieves the Stripe customer with expanded default payment methods, and registers card fingerprints and customer emails via addToStripeFraudValueLists().deleteProgramAdmin(workspace.defaultProgramId) if present.${APP_DOMAIN_WITH_NGROK}/api/cron/workspaces/delete via qstash.publishJSON() using the workspace ID.Tip
Default domain links are processed in a while (true) loop taking batches of 100 links at a time. Reassigning them to LEGAL_WORKSPACE_ID prevents dangling references and preserves platform routing integrity while removing ownership from the banned user.
Sources: apps/web/app/ee/api/admin/ban/delete-workspace-admin.ts:25-65
Link and domain moderation tooling enables administrators to review system links, ban malicious URLs, and manage domain lifecycles. The moderation interface provides controls for ban operations, workspace ownership updates, premium domain provisioning, domain renewals, and Vercel domain synchronization.
Sources: apps/web/app/ee/admin.dub.co/dashboard/links/page.tsx:1-23, apps/web/app/ee/admin.dub.co/dashboard/domains/page.tsx:1-37
The administrative endpoint for querying links (GET /api/admin/links) enforces strict role verification via withAdmin and parses parameters for searching and sorting.
Note
Unfiltered queries restrict results to links whose domains belong to DUB_DOMAINS_ARRAY, created within the past 30 days (gte: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30)), and belonging to non-legal users or null user IDs.
When an administrator executes a ban via DELETE /api/admin/links/ban, the backend processes the request through a sequence of validation and state update steps:
searchParams with domainKeySchema to extract domain and key.prisma.link.findUnique() using the compound unique key { domain_key: { domain, key } }. If missing, responds with HTTP 404 ("Link not found").urlDomain by passing link.url to getDomainWithoutWWW().Promise.all:
prisma.link.update() reassigns userId to LEGAL_USER_ID and projectId to LEGAL_WORKSPACE_ID.linkCache.set() updates the link cache entry with the legal workspace ID.updateConfig() pushes urlDomain to Edge Config under the key "domains" if urlDomain is present.The administrative domain management dashboard exposes three primary operations for handling .link domains and Vercel integrations.
Workspace disabling and incident recovery mechanisms provide administrators with frontend controls and backend routes to suspend routing for entire workspaces, downgrade user roles, trigger automated email notifications, restore disabled resources, and execute programmatic recovery scripts.
Sources: apps/web/app/ee/admin.dub.co/dashboard/links/components/disable-restore-workspace.tsx:8-42, apps/web/app/ee/api/admin/workspaces/disable/route.ts:8-104, apps/web/app/ee/api/admin/workspaces/restore/route.ts:7-93
When an administrator submits a workspace slug for disabling via the DisableRestoreWorkspace client component, the request is sent to the /api/admin/workspaces/disable endpoint, which executes a multi-step database and notification sequence:
prisma.project.findUnique() by slug, including workspace users with the role "owner" who have non-null email addresses. If the project does not exist, returns HTTP 404 ("Workspace not found").disableWorkspaceLinks(project.id) to disable all routing links associated with the workspace identifier."owner" roles to "billing" and all "member" roles to "viewer" via prisma.projectUsers.updateMany().disabledAt field on the project record to the current date and time via prisma.project.update().queueBatchEmail(), dispatching the WorkspaceDisabled template containing workspace usage statistics, plan details, and name metadata.Sources: apps/web/app/ee/admin.dub.co/dashboard/links/components/disable-restore-workspace.tsx:12-42, apps/web/app/ee/api/admin/workspaces/disable/route.ts:10-97
Warning
Disabling a workspace automatically downgrades all workspace owners to the "billing" role and members to the "viewer" role, preventing administrative actions while preserving billing contact access.
Sources: apps/web/app/ee/api/admin/workspaces/disable/route.ts:43-64
Restoring a workspace through /api/admin/workspaces/restore reverses administrative suspensions by paginating through disabled links in batches of 100, clearing cache expirations, and restoring original user roles.
Note
The link restoration loop uses a while (true) construct that fetches links in pages of 100 until linksToRestore.length evaluates to zero, ensuring memory stability when restoring workspaces with large link volumes.
Sources: apps/web/app/ee/api/admin/workspaces/restore/route.ts:21-34
For emergency incident recovery outside the administrative dashboard, the restore-banned-workspace.ts maintenance script programmatically provisions or recovers workspaces and links via direct database operations.
import { generateRandomString } from "@/lib/api/utils/generate-random-string";
import { prisma } from "@/lib/prisma";
import { LEGAL_USER_ID, nanoid } from "@dub/utils";
import { linkCache } from "../lib/api/links/cache";
const WORKSPACE_ID = "ws_xxx";
const WORKSPACE_NAME = "xxx";
const WORKSPACE_SLUG = "xxx";
const USER_ID = "user_xxx";
const LINK_IDS: string[] = [];
async function recoverWorkspace() {
let workspace = await prisma.project.findUnique({
where: { id: WORKSPACE_ID },
});
if (!workspace) {
workspace = await prisma.project.create({
data: {
id: WORKSPACE_ID,
name: WORKSPACE_NAME,
slug: WORKSPACE_SLUG,
billingCycleStart: new Date().getDate(),
invoicePrefix: generateRandomString(8),
inviteCode: nanoid(24),
defaultDomains: { create: {} },
users: {
create: {
userId: LEGAL_USER_ID,
role: "owner",
notificationPreference: { create: {} },
},
},
},
});
} else {
await prisma.projectUsers.upsert({
where: {
userId_projectId: { userId: LEGAL_USER_ID, projectId: WORKSPACE_ID },
},
create: {
userId: LEGAL_USER_ID,
projectId: WORKSPACE_ID,
role: "owner",
notificationPreference: { create: {} },
},
update: { role: "owner" },
});
}
const links = await prisma.link.findMany({
where: { id: { in: LINK_IDS } },
select: { id: true, domain: true, key: true, projectId: true, userId: true },
});
if (links.length > 0) {
await Promise.allSettled([
linkCache.expireMany(links),
prisma.link.updateMany({
where: { id: { in: links.map((l) => l.id) } },
data: { projectId: WORKSPACE_ID, userId: USER_ID },
}),
]);
await prisma.project.update({
where: { id: WORKSPACE_ID },
data: { totalLinks: links.length },
});
}
}Administrative operations include managing Slack support channels, provisioning customer context panels, and moderating external marketplace programs.
Sources: apps/web/app/api/callback/plain/workspace/route.ts:20-46, apps/web/app/ee/api/admin/programs/route.ts:69-101, apps/web/app/ee/api/admin/slack-support-invite/route.ts:10-49
The administrative endpoint POST /api/admin/slack-support-invite processes manual Slack Connect invitations on behalf of a workspace. It accepts an email, an array of emails, a workspaceSlug, and an optional channelId.
export const POST = withAdmin(async ({ req }) => {
const {
email,
emails: emailsBody,
workspaceSlug,
channelId,
} = await req.json();
const emails =
Array.isArray(emailsBody) && emailsBody.length > 0
? emailsBody
: typeof email === "string" && email.trim()
? [email.trim()]
: [];
if (emails.length === 0 || !workspaceSlug) {
return NextResponse.json(
{ error: "email (or emails) and workspaceSlug are required" },
{ status: 400 },
);
}
const { inviteIds, nameTaken } = await inviteToSlackSupportChannel({
emails,
workspaceSlug,
channelId: channelId || undefined,
});
if (nameTaken) {
return NextResponse.json(
{
error: `Channel #${sharedSupportChannelName(workspaceSlug)} already exists. Provide its Slack channel ID (C…) to send the invite.`,
nameTaken: true,
},
{ status: 409 },
);
}
return NextResponse.json({ success: true, inviteIds });
});Warning
If a shared support channel name is already taken (nameTaken returns true), the endpoint responds with HTTP 409 Conflict and requires administrators to supply the explicit Slack channel ID starting with C to bypass name collision checks.
Sources: apps/web/app/ee/api/admin/slack-support-invite/route.ts:38-46
The administrative API at /api/admin/programs governs external marketplace program listings using Anthropic Claude models and FireCrawl web scraping.
async function categorizeProgram({
programName,
url,
title,
description,
}: {
programName: string;
url: string;
title: string;
description: string;
}) {
const prompt = `Analyze this website and categorize it into 1-3 most relevant categories.
Website information:
Name: ${programName}
Website URL: ${url}
Page Title: ${title}
Meta Description: ${description}`;
const { output } = await generateText({
model: anthropic("claude-sonnet-4-6"),
output: Output.object({
schema: categorizationSchema,
}),
prompt,
temperature: 0.4,
});
return categorizationSchema.parse(output).categories;
}