Skip to main content

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

customDomain
string
Custom domain (null if not set)
customDomainVerified
boolean
Whether domain is verified
hasCustomDomain
boolean
Whether custom domain is configured

POST /api/user/custom-domain

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

Request

domain
string
required
Domain name (1-255 characters). Must be valid domain format.

Response

success
boolean
Always true on success
domain
string
The domain that was set
message
string
“Domain connected successfully. Please configure your DNS settings.”

DELETE /api/user/custom-domain

Removes custom domain configuration.

Response

success
boolean
Always true on success
message
string
“Custom domain disconnected successfully”

Implementation Details

Code Reference

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

200
OK
Success
400
Bad Request
Invalid domain format
403
Forbidden
Enterprise plan required
409
Conflict
Domain is already in use by another user
401
Unauthorized
Missing or invalid authentication token

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

curl -X GET https://auth.nullpass.xyz/api/user/custom-domain \
  -H "Authorization: Bearer YOUR_TOKEN"

Set Custom Domain

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

curl -X DELETE https://auth.nullpass.xyz/api/user/custom-domain \
  -H "Authorization: Bearer YOUR_TOKEN"

Example Response (GET)

{
  "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')