---
title: "Admin Operations"
description: "Admin Operations provides a centralized administrative dashboard for platform maintainers to manage user access, enforce security policies, investigate abuse, and moderate workspaces and partner pr..."
last_updated: "2026-10-05T05:07:35.166923+00:00"
canonical_url: "https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/developer-tools/admin-operations"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [apps/web/app/ee/admin.dub.co/dashboard/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/page.tsx)
- [apps/web/app/ee/api/admin/impersonate/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/impersonate/route.ts)
- [apps/web/app/api/domains/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/route.ts)
- [apps/web/app/ee/api/admin/ban/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/ban/route.ts)
- [apps/web/app/api/workspaces/idOrSlug/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/route.ts)
- [apps/web/app/ee/admin.dub.co/dashboard/components/impersonate-user.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/components/impersonate-user.tsx)
- [apps/web/app/api/callback/plain/workspace/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/callback/plain/workspace/route.ts)
- [apps/web/app/ee/admin.dub.co/dashboard/links/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/links/page.tsx)
- [apps/web/app/api/ai/support-chat/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/ai/support-chat/route.ts)
- [apps/web/app/ee/admin.dub.co/dashboard/domains/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/domains/page.tsx)
- [apps/web/app/app.dub.co/dashboard/slug/ee/settings/domains/page-client.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(dashboard)/%5Bslug%5D/(ee)/settings/domains/page-client.tsx)
- [apps/web/app/ee/api/admin/links/ban/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/links/ban/route.ts)
- [apps/web/app/ee/api/email-domains/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/email-domains/route.ts)
- [apps/web/app/ee/admin.dub.co/dashboard/links/components/disable-restore-workspace.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/links/components/disable-restore-workspace.tsx)
- [apps/web/app/ee/api/admin/workspaces/disable/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/workspaces/disable/route.ts)
- [apps/web/lib/auth/admin-access-guard.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/admin-access-guard.ts)
- [apps/web/app/api/workspaces/idOrSlug/saml/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/saml/route.ts)
- [apps/web/app/app.dub.co/onboarding/workspaces/page.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/app/app.dub.co/(onboarding)/workspaces/page.tsx)
- [apps/web/app/ee/api/admin/ban/delete-workspace-admin.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/ban/delete-workspace-admin.ts)
- [apps/web/app/ee/api/email-domains/domain/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/email-domains/%5Bdomain%5D/route.ts)
- [apps/web/lib/auth/admin-impersonation.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/admin-impersonation.ts)
- [apps/web/ui/domains/domain-card.tsx](https://github.com/blade47/dub/blob/HEAD/apps/web/ui/domains/domain-card.tsx)
- [apps/web/app/ee/api/admin/workspaces/restore/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/workspaces/restore/route.ts)
- [apps/web/app/ee/api/admin/programs/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/programs/route.ts)
- [apps/web/scripts/restore-banned-workspace.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/restore-banned-workspace.ts)
- [apps/web/app/ee/api/admin/slack-support-invite/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/slack-support-invite/route.ts)
- [apps/web/lib/middleware/admin.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/admin.ts)
- [apps/web/app/api/workspaces/idOrSlug/scim/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/workspaces/%5BidOrSlug%5D/scim/route.ts)
- [apps/web/app/ee/api/admin/links/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/links/route.ts)
- [apps/web/app/api/domains/default/route.ts](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/domains/default/route.ts)
</details>

## Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/admin.ts#L8-L38), [apps/web/app/ee/admin.dub.co/dashboard/page.tsx:7-59](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/page.tsx#L7-L59)

## Admin Dashboard Architecture and Access Control

### Overview

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.

Sources: [apps/web/lib/middleware/admin.ts:8-38](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/admin.ts#L8-L38)

### Middleware Execution Flow

The `AdminMiddleware` function processes incoming requests to the admin domain through the following execution path:

1. `parse(req)` — Extracts the requested request path (`path`).
2. `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`.
3. `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.
4. `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 `/`.
5. `NextResponse.rewrite(...)` — For authorized sessions, rewrites the request to internal admin routes prefixed with `/admin.dub.co`.

Sources: [apps/web/lib/middleware/admin.ts:8-37](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/middleware/admin.ts#L8-L37)

### Access Control Rules and Restricted Paths

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.

| Constant Name | Restricted Values / Paths | Purpose |
| :--- | :--- | :--- |
| `ADMIN_ACCESS_BLOCKLIST` | `user_1M3YFDDQ97AGTKWQ9NCSRQTYB` | Identifies specific admin user IDs blocked from accessing restricted administrative tabs. |
| `ADMIN_RESTRICTED_PATHS` | `/links`, `/revenue`, `/domains` | UI route paths restricted for blocklisted administrators. |
| `ADMIN_RESTRICTED_API_PATHS` | `/api/admin/links`, `/api/admin/workspaces`, `/api/admin/revenue`, `/api/admin/domains` | Backend API routes restricted for blocklisted administrators. |

Sources: [apps/web/lib/auth/admin-access-guard.ts:3-14](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/admin-access-guard.ts#L3-L14)

> [!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`.
> 
> Sources: [apps/web/lib/auth/admin-access-guard.ts:16-17](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/admin-access-guard.ts#L16-L17)

## Account Impersonation and Diagnostic Context

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/impersonate/route.ts#L113-L221), [apps/web/lib/auth/admin-impersonation.ts:1-20](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/admin-impersonation.ts#L1-L20)

### Query Parsing and Identifier Types

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.

| Identifier Type | Matching Criteria & Validation Rule | Parsed Output Structure |
| :--- | :--- | :--- |
| `email` | Contains `@` and passes `emailSchema.safeParse(query)` | `{ type: "email", email: parsed.data }` |
| `stripeCustomerId` | Matches `STRIPE_CUSTOMER_ID_REGEX` (`/cus_[a-zA-Z0-9]+_/`) and starts with `cus_` or includes `stripe.com` | `{ type: "stripeCustomerId", stripeCustomerId }` |
| `slug` | Hostname matches app domain with a valid slug pathname, or query passes `validSlugRegex.test(slug)` | `{ type: "slug", slug: slug.toLowerCase() }` |
| `domain` | Normalized via `normalizeDomainInput` and passes `validDomainRegex.test(domain)` | `{ type: "domain", domain }` |

Sources: [apps/web/app/ee/api/admin/impersonate/route.ts:105-180](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/impersonate/route.ts#L105-L180)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/components/impersonate-user.tsx#L104-L118)

### Impersonation Token Tracking Workflow

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.

1. `markAdminImpersonation(email)` — Converts the target email to lowercase and adds it to the `pendingAdminImpersonations` set.
2. `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.

Sources: [apps/web/lib/auth/admin-impersonation.ts:1-20](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/admin-impersonation.ts#L1-L20)

> [!NOTE]
> `pendingAdminImpersonations` is stored entirely in memory. It tracks sign-ins via admin impersonation links specifically inside `CustomPrismaAdapter.useVerificationToken` prior to record removal.
> 
> Sources: [apps/web/lib/auth/admin-impersonation.ts:1-3](https://github.com/blade47/dub/blob/HEAD/apps/web/lib/auth/admin-impersonation.ts#L1-L3)

## Fraud Enforcement and Workspace Bans

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/ban/route.ts#L11-L87), [apps/web/app/ee/api/admin/ban/delete-workspace-admin.ts:19-121](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/ban/delete-workspace-admin.ts#L19-L121)

### Execution Call-Chain

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.

1. `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`).
2. `extractEmailDomain(email)` — Extracts the domain from the user's email address for domain-level blocking.
3. `updateConfig()` — Concurrently pushes the banned email to the `emails` Edge Config key and, if `blockEmailDomain` is true, the domain to `emailDomainTerms`.
4. `waitUntil()` — Defers asynchronous post-response cleanup, executing `deleteWorkspaceAdmin()` for each associated project via `Promise.allSettled`.
5. `prisma.user.delete()` — Removes the user record from the database after all workspaces finish deleting.
6. `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)`).

Sources: [apps/web/app/ee/api/admin/ban/route.ts:12-80](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/ban/route.ts#L12-L80)

> [!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.
> 
> Sources: [apps/web/app/ee/api/admin/ban/route.ts:45-57](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/ban/route.ts#L45-L57)

### Workspace Ban and Link Migration Procedure

The `deleteWorkspaceAdmin` routine handles individual workspace destruction in batches, migrating links and purging associated infrastructure assets.

1. **Batch Link Retrieval**: Queries up to 100 links associated with the workspace (`projectId`) whose domain matches any entry in `DUB_DOMAINS_ARRAY`. If no links remain, the loop breaks.
2. **Cache Eviction and Legal Transfer**: Concurrently calls `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`.
3. **Asset & Subscription Cleanup**: Concurrently executes three cleanup actions via `Promise.allSettled`:
   - Deletes custom workspace logos stored in R2 (`workspace.logo.startsWith(R2_URL + "/logos/" + workspace.id)`).
   - Cancels Stripe subscriptions immediately (`cancelSubscription()`), retrieves the Stripe customer with expanded default payment methods, and registers card fingerprints and customer emails via `addToStripeFraudValueLists()`.
   - Deletes affiliate programs via `deleteProgramAdmin(workspace.defaultProgramId)` if present.
4. **QStash Deletion Queue**: Publishes a deletion payload to `${APP_DOMAIN_WITH_NGROK}/api/cron/workspaces/delete` via `qstash.publishJSON()` using the workspace ID.

Sources: [apps/web/app/ee/api/admin/ban/delete-workspace-admin.ts:19-120](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/ban/delete-workspace-admin.ts#L19-L120)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/ban/delete-workspace-admin.ts#L25-L65)

### Workspace Cleanup Operations Reference

| Operation Target | Condition / Check | Action Performed | Source Reference |
| :--- | :--- | :--- | :--- |
| Default Domain Links | `domain: { in: DUB_DOMAINS_ARRAY }`, `take: 100` | Evicts cache via `linkCache.expireMany()` and reassigns `projectId` / `userId` to legal constants. | [apps/web/app/ee/api/admin/ban/delete-workspace-admin.ts:25-57](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/ban/delete-workspace-admin.ts#L25-L57) |
| Custom Workspace Logo | `workspace.logo.startsWith(R2_URL + "/logos/" + workspace.id)` | Deletes object from R2 storage using `storage.delete()`. | [apps/web/app/ee/api/admin/ban/delete-workspace-admin.ts:69-71](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/ban/delete-workspace-admin.ts#L69-L71) |
| Stripe Subscription | `workspace.stripeId` present | Cancels subscription immediately, retrieves customer payment method, and adds details to Stripe fraud value lists. | [apps/web/app/ee/api/admin/ban/delete-workspace-admin.ts:73-100](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/ban/delete-workspace-admin.ts#L73-L100) |
| Default Program | `workspace.defaultProgramId` present | Invokes `deleteProgramAdmin()` for marketplace program cleanup. | [apps/web/app/ee/api/admin/ban/delete-workspace-admin.ts:102-103](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/ban/delete-workspace-admin.ts#L102-L103) |
| Workspace Record | Unconditional after cleanup | Publishes deletion task to QStash cron endpoint `/api/cron/workspaces/delete`. | [apps/web/app/ee/api/admin/ban/delete-workspace-admin.ts:108-114](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/ban/delete-workspace-admin.ts#L108-L114) |

Sources: [apps/web/app/ee/api/admin/ban/delete-workspace-admin.ts:25-114](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/ban/delete-workspace-admin.ts#L25-L114)

## Link and Domain Moderation Controls

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/links/page.tsx#L1-L23), [apps/web/app/ee/admin.dub.co/dashboard/domains/page.tsx:1-37](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/domains/page.tsx#L1-L37)

### Link Search and Filtering Parameters

The administrative endpoint for querying links (`GET /api/admin/links`) enforces strict role verification via `withAdmin` and parses parameters for searching and sorting.

| Parameter | Type / Allowed Values | Default | Purpose |
| :--- | :--- | :--- | :--- |
| `domain` | `string` | — | Filters links by custom domain. |
| `search` | `string` | — | Matches against `shortLink` (if prefixed with `https://`) or contains `shortLink` / `url`. |
| `sort` | `createdAt`, `clicks`, `lastClicked` | `createdAt` | Specifies the descending sort order for results. |
| `page` | `string` (parsed as integer) | — | Determines pagination offset (`skip: (parseInt(page) - 1) * 100`). |

Sources: [apps/web/app/ee/api/admin/links/route.ts:8-78](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/links/route.ts#L8-L78)

> [!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.
> 
> Sources: [apps/web/app/ee/api/admin/links/route.ts:21-40](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/links/route.ts#L21-L40)

### Link Ban Call-Chain Execution

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:

1. **Schema Validation**: Parses `searchParams` with `domainKeySchema` to extract `domain` and `key`.
2. **Database Lookup**: Queries `prisma.link.findUnique()` using the compound unique key `{ domain_key: { domain, key } }`. If missing, responds with HTTP 404 (`"Link not found"`).
3. **URL Domain Extraction**: Computes `urlDomain` by passing `link.url` to `getDomainWithoutWWW()`.
4. **Concurrent Execution**: Executes three operations in parallel via `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.

Sources: [apps/web/app/ee/api/admin/links/ban/route.ts:14-46](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/links/ban/route.ts#L14-L46)

### Domain Administration Operations

The administrative domain management dashboard exposes three primary operations for handling `.link` domains and Vercel integrations.

| Operation | Component | Action Performed |
| :--- | :--- | :--- |
| Register premium .link domain | `RegisterPremiumDomain` | Creates a domain-renewal invoice and charges the workspace default payment method. Provisions the domain in Dub; Dynadot registration must be performed manually. |
| Renew domain (.link) | `RenewDomain` | Creates a domain-renewal invoice and charges the default payment method. Stripe webhooks extend Dub expiry and re-enable Dynadot auto-renew. |
| Refresh domain | `RefreshDomain` | Removes and re-adds the domain from Vercel. |

Sources: [apps/web/app/ee/admin.dub.co/dashboard/domains/page.tsx:1-34](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/domains/page.tsx#L1-L34)

## Workspace Disabling and Incident Recovery

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/links/components/disable-restore-workspace.tsx#L8-L42), [apps/web/app/ee/api/admin/workspaces/disable/route.ts:8-104](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/workspaces/disable/route.ts#L8-L104), [apps/web/app/ee/api/admin/workspaces/restore/route.ts:7-93](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/workspaces/restore/route.ts#L7-L93)

### Workspace Disabling Execution Flow

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:

1. **Project Lookup & Owner Extraction**: Queries `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"`).
2. **Link Deactivation**: Invokes `disableWorkspaceLinks(project.id)` to disable all routing links associated with the workspace identifier.
3. **Role Demotion**: Updates project member roles in batch, converting all `"owner"` roles to `"billing"` and all `"member"` roles to `"viewer"` via `prisma.projectUsers.updateMany()`.
4. **Timestamp Update**: Sets the `disabledAt` field on the project record to the current date and time via `prisma.project.update()`.
5. **Batch Email Dispatch**: Maps extracted owner emails to a batch email queue using `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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/admin.dub.co/(dashboard)/links/components/disable-restore-workspace.tsx#L12-L42), [apps/web/app/ee/api/admin/workspaces/disable/route.ts:10-97](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/workspaces/disable/route.ts#L10-L97)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/workspaces/disable/route.ts#L43-L64)

### Workspace Restoration and Iterative Link Recovery

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.

| Operation Phase | Target Model / Method | Action Performed |
| :--- | :--- | :--- |
| Batch Link Restoration | `prisma.link.findMany()` & `updateMany()` | Fetches batches of 100 links where `disabledAt` is not null, sets `disabledAt` to `null`, and calls `linkCache.expireMany(links)`. Loops until no disabled links remain. |
| Owner Role Reversion | `prisma.projectUsers.updateMany()` | Reverts all users with role `"billing"` back to the `"owner"` role. |
| Member Role Reversion | `prisma.projectUsers.updateMany()` | Reverts all users with role `"viewer"` back to the `"member"` role. |
| Project Flag Clearance | `prisma.project.update()` | Sets the project `disabledAt` timestamp to `null`. |

Sources: [apps/web/app/ee/api/admin/workspaces/restore/route.ts:21-86](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/workspaces/restore/route.ts#L21-L86)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/workspaces/restore/route.ts#L21-L34)

### Scripted Workspace Recovery

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.

```typescript
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 },
    });
  }
}
```

Sources: [apps/web/scripts/restore-banned-workspace.ts:1-133](https://github.com/blade47/dub/blob/HEAD/apps/web/scripts/restore-banned-workspace.ts#L1-L133)

## Support Triage and Partner Administration

### Overview

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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/api/callback/plain/workspace/route.ts#L20-L46), [apps/web/app/ee/api/admin/programs/route.ts:69-101](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/programs/route.ts#L69-L101), [apps/web/app/ee/api/admin/slack-support-invite/route.ts:10-49](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/slack-support-invite/route.ts#L10-L49)

### Slack Support Invitations and Channel Bridges

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`.

```typescript
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 });
});
```

Sources: [apps/web/app/ee/api/admin/slack-support-invite/route.ts:10-49](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/slack-support-invite/route.ts#L10-L49)

> [!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](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/slack-support-invite/route.ts#L38-L46)

### Marketplace Program Moderation

The administrative API at `/api/admin/programs` governs external marketplace program listings using Anthropic Claude models and FireCrawl web scraping.

| HTTP Method | Required Role | Function Performed | Response Schema / Output |
| :--- | :--- | :--- | :--- |
| `GET` | Admin (default) | Queries all programs with `addedToMarketplaceAt` not null, ordered by `marketplaceRanking` ascending and `addedToMarketplaceAt` ascending. | JSON array of active marketplace programs with flattened categories. |
| `POST` | Owner (`requiredRoles: ["owner"]`) | Scrapes program URL markdown content, executes AI-based categorization (`claude-sonnet-4-6`), lists program, and revalidates external marketplace pages. | Updated program ID and slug. |
| `PATCH` | Owner (`requiredRoles: ["owner"]`) | Deduplicates and updates marketplace rankings (`marketplaceRanking`) via a Prisma transaction, then triggers external page revalidation. | Object containing `ok: true` and count of updated items. |

Sources: [apps/web/app/ee/api/admin/programs/route.ts:69-238](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/programs/route.ts#L69-L238)

```typescript
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;
}
```

Sources: [apps/web/app/ee/api/admin/programs/route.ts:39-67](https://github.com/blade47/dub/blob/HEAD/apps/web/app/(ee)/api/admin/programs/route.ts#L39-L67)

## Related

- [Fraud Detection and Hold Rules](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/affiliate-platform/fraud-detection-and-hold-rules)
- [Link Resolution and Redirection](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/technical/link-management/link-resolution-and-redirection)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/934e554a-e6a1-476f-bb2f-23e62d86c3fd/llms.txt) for all pages in this wiki.
