> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nullpass.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom Domain

> Manage custom domain for DROP service

## Endpoints

```
GET /api/user/custom-domain
POST /api/user/custom-domain
DELETE /api/user/custom-domain
```

## Overview

Manages custom domain configuration for DROP service. Requires Enterprise tier. Custom domains are stored in service metadata.

## GET /api/user/custom-domain

Retrieves custom domain configuration for the authenticated user's DROP service.

### Response

<ResponseField name="customDomain" type="string">
  Custom domain (null if not set)
</ResponseField>

<ResponseField name="customDomainVerified" type="boolean">
  Whether domain is verified
</ResponseField>

<ResponseField name="hasCustomDomain" type="boolean">
  Whether custom domain is configured
</ResponseField>

## POST /api/user/custom-domain

Sets a custom domain for DROP service. Requires Enterprise tier.

### Request

<ParamField body="domain" type="string" required>
  Domain name (1-255 characters). Must be valid domain format.
</ParamField>

### Response

<ResponseField name="success" type="boolean">
  Always `true` on success
</ResponseField>

<ResponseField name="domain" type="string">
  The domain that was set
</ResponseField>

<ResponseField name="message" type="string">
  "Domain connected successfully. Please configure your DNS settings."
</ResponseField>

## DELETE /api/user/custom-domain

Removes custom domain configuration.

### Response

<ResponseField name="success" type="boolean">
  Always `true` on success
</ResponseField>

<ResponseField name="message" type="string">
  "Custom domain disconnected successfully"
</ResponseField>

## Implementation Details

### Code Reference

```13:100:nullpass_clean/src/app/api/user/custom-domain/route.ts theme={null}
export async function POST(request: NextRequest) {
  const corsResponse = handleCors(request)
  if (corsResponse) return corsResponse

  const blocked = await protectRoute(request)
  if (blocked) return blocked

  const auth = await requireAuth(request)
  if ('error' in auth) return auth.error

  try {
    const body = await request.json()
    const { domain } = domainSchema.parse(body)

    const dropService = await prisma.userServiceEntitlement.findUnique({
      where: {
        userId_service: {
          userId: auth.userId,
          service: 'DROP',
        },
      },
    })

    if (!dropService || dropService.tier !== 'enterprise') {
      return errorResponse('Enterprise plan required for custom domains', 403, request.headers.get('origin'))
    }

    const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])*$/
    if (!domainRegex.test(domain.trim())) {
      return errorResponse('Invalid domain format', 400, request.headers.get('origin'))
    }

    const allEntitlements = await prisma.userServiceEntitlement.findMany({
      where: {
        service: 'DROP',
        userId: {
          not: auth.userId,
        },
      },
      select: {
        metadata: true,
      },
    })

    const existingDomain = allEntitlements.find((entitlement: { metadata: any }) => {
      const metadata = (entitlement.metadata as any) || {}
      return metadata.customDomain === domain.trim()
    })

    if (existingDomain) {
      return errorResponse('Domain is already in use', 409, request.headers.get('origin'))
    }

    const currentMetadata = (dropService.metadata as any) || {}
    await prisma.userServiceEntitlement.update({
      where: {
        userId_service: {
          userId: auth.userId,
          service: 'DROP',
        },
      },
      data: {
        metadata: {
          ...currentMetadata,
          customDomain: domain.trim(),
          customDomainVerified: false,
        },
      },
    })

    await createAuditLog(auth.userId, 'USER_UPDATE', {
      field: 'customDomain',
      value: domain.trim(),
    })

    return jsonResponse({
      success: true,
      domain: domain.trim(),
      message: 'Domain connected successfully. Please configure your DNS settings.',
    }, 200, request.headers.get('origin'))
  } catch (error: any) {
    if (error.name === 'ZodError') {
      return errorResponse(error.errors[0].message, 400, request.headers.get('origin'))
    }
    console.error('Custom domain error:', error)
    return errorResponse('Internal server error', 500, request.headers.get('origin'))
  }
}
```

## Status Codes

<ResponseField name="200" type="OK">
  Success
</ResponseField>

<ResponseField name="400" type="Bad Request">
  Invalid domain format
</ResponseField>

<ResponseField name="403" type="Forbidden">
  Enterprise plan required
</ResponseField>

<ResponseField name="409" type="Conflict">
  Domain is already in use by another user
</ResponseField>

<ResponseField name="401" type="Unauthorized">
  Missing or invalid authentication token
</ResponseField>

## Requirements

* **Tier**: Enterprise plan required
* **Service**: DROP service only
* **Domain Format**: Valid domain name format (regex validated)
* **Uniqueness**: Domain must be unique across all users

## Example Requests

### Get Custom Domain

```bash theme={null}
curl -X GET https://auth.nullpass.xyz/api/user/custom-domain \
  -H "Authorization: Bearer YOUR_TOKEN"
```

### Set Custom Domain

```bash theme={null}
curl -X POST https://auth.nullpass.xyz/api/user/custom-domain \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "example.com"
  }'
```

### Remove Custom Domain

```bash theme={null}
curl -X DELETE https://auth.nullpass.xyz/api/user/custom-domain \
  -H "Authorization: Bearer YOUR_TOKEN"
```

## Example Response (GET)

```json theme={null}
{
  "customDomain": "example.com",
  "customDomainVerified": false,
  "hasCustomDomain": true
}
```

## Domain Storage

Custom domains are stored in the `metadata` field of the DROP service entitlement:

* `customDomain`: Domain string
* `customDomainVerified`: Verification status (boolean)

## Audit Events

* **USER\_UPDATE**: Custom domain set or removed (includes `field: 'customDomain'`)
