# Custom Domains — Locus Build

> Companion guide to [`SKILL.md`](./SKILL.md). Covers BYOD and domain purchase flows.
>
> **BYOD-only?** See [`byod-domains.md`](./byod-domains.md) for a focused standalone guide.

## When To Load

Load this file only for custom domain registration, DNS verification, or domain purchase flows.

## Table of Contents

- [Option A: Bring Your Own Domain (BYOD)](#option-a-bring-your-own-domain-byod)
- [Option B: Purchase a New Domain](#option-b-purchase-a-new-domain)
- [Domain Management Endpoints](#domain-management-endpoints)
- [Agent Workflow: Custom Domains](#agent-workflow-custom-domains)

Locus supports two ways to add custom domains: **bring your own domain (BYOD)** or **purchase a new domain** through the platform. Both result in a fully routable domain with SSL.

> **Domains are workspace-owned assets.** They persist across project lifecycle — deleting a project does not delete its domains. The `projectId` field is optional and used for organizational grouping only.

**Environment note:** Domain purchase features (check availability, suggestions, purchase) require specific IAM permissions (Route53 Domains, ACM, ALB) and may not be available in all environments. BYOD registration works in all environments. If domain purchase returns a permissions error, contact the platform administrator.

## Option A: Bring Your Own Domain (BYOD)

Use a domain you already own. You'll add DNS records to point it at Locus and verify ownership.

**Step 1: Register your domain and request SSL certificate**

```bash
DOMAIN=$(curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domain": "api.example.com", "projectId": "'"$PROJECT_ID"'"}' \
  "https://api.buildwithlocus.com/v1/domains")

DOMAIN_ID=$(echo $DOMAIN | jq -r '.id')
echo "Domain ID: $DOMAIN_ID"
echo "CNAME target (point your domain here):"
echo $DOMAIN | jq -r '.cnameTarget'
echo "SSL validation records:"
echo $DOMAIN | jq '.validationRecords'
```

The response includes:

- `cnameTarget` — the DNS name your domain must CNAME to
- `validationRecords` — CNAME entries for SSL certificate validation

**Step 2: Add DNS records**

Add two DNS records at your DNS provider:

1. **SSL validation CNAME** (from `validationRecords`) — proves domain ownership for SSL
2. **Domain CNAME** → the `cnameTarget` value — routes traffic to Locus

**Step 2b (alternative): Auto-create DNS records (if domain is managed by Locus)**

```bash
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  "https://api.buildwithlocus.com/v1/domains/$DOMAIN_ID/create-dns-records"
```

**Step 3: Wait for auto-verification**

After DNS records are added, Locus automatically checks DNS every 5 minutes for up to 2 hours. No manual polling required — check `GET /v1/domains/$DOMAIN_ID` and look for `validationStatus: 'validated'`.

You can optionally trigger an immediate check:

```bash
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  "https://api.buildwithlocus.com/v1/domains/$DOMAIN_ID/verify"
```

Response:

```json
{
  "domainId": "dom_xxx",
  "cnameVerified": true,
  "certificateValidated": true,
  "validationStatus": "validated",
  "message": "Domain verified! CNAME resolves correctly and certificate is issued."
}
```

Both `cnameVerified` and `certificateValidated` must be `true` before the domain can be attached. DNS propagation typically takes 1-15 minutes but can take up to 30 minutes.

**Step 4: Attach to a service**

```bash
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"serviceId": "'"$SERVICE_ID"'"}' \
  "https://api.buildwithlocus.com/v1/domains/$DOMAIN_ID/attach"
```

Traffic to your domain will now route to the attached service on port 8080.

## Option B: Purchase a New Domain

Search for available domains, purchase one, and Locus auto-configures DNS, SSL, and routing.

**Step 1: Check domain availability**

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://api.buildwithlocus.com/v1/domains/check-availability?domain=myapp.com"
```

Response:

```json
{
  "domain": "myapp.com",
  "available": true,
  "price": {
    "registrationPrice": 12.0,
    "renewalPrice": 12.0,
    "currency": "USD"
  }
}
```

**Step 1b (optional): Get domain suggestions**

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://api.buildwithlocus.com/v1/domains/suggestions?keywords=myapp&tlds=com,dev,io"
```

Response:

```json
{
  "suggestions": [
    {
      "domain": "myapp.dev",
      "available": true,
      "price": {
        "registrationPrice": 12,
        "renewalPrice": 14,
        "currency": "USD"
      }
    },
    {
      "domain": "myappsite.com",
      "available": true,
      "price": {
        "registrationPrice": 18,
        "renewalPrice": 20,
        "currency": "USD"
      }
    },
    {
      "domain": "getmyapp.io",
      "available": true,
      "price": {
        "registrationPrice": 42,
        "renewalPrice": 48,
        "currency": "USD"
      }
    }
  ]
}
```

**Step 2: Purchase the domain**

```bash
PURCHASE=$(curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "'"$PROJECT_ID"'",
    "domain": "myapp.com",
    "contact": {
      "firstName": "Jane",
      "lastName": "Smith",
      "email": "jane@example.com",
      "phone": "+1.5551234567",
      "addressLine1": "123 Main St",
      "city": "San Francisco",
      "state": "CA",
      "countryCode": "US",
      "zipCode": "94105"
    },
    "autoRenew": true,
    "privacyProtection": true
  }' \
  "https://api.buildwithlocus.com/v1/domains/purchase")

DOMAIN_ID=$(echo $PURCHASE | jq -r '.domainId')
echo "Domain ID: $DOMAIN_ID"
```

Returns `202 Accepted` — registration is asynchronous and takes 1-15 minutes.

> **Billing:** Domain purchases are charged against your workspace credit balance. The registration price (shown in the availability check) is deducted at purchase time. If your balance is insufficient, the API returns `402` with `creditBalance` and `required` fields. Exempt accounts are not charged.

**Step 3: Poll registration status**

```bash
while true; do
  RESULT=$(curl -s -H "Authorization: Bearer $TOKEN" \
    "https://api.buildwithlocus.com/v1/domains/$DOMAIN_ID/registration-status")
  STATUS=$(echo $RESULT | jq -r '.registrationStatus')
  echo "Registration: $STATUS"
  [ "$STATUS" = "registered" ] || [ "$STATUS" = "failed" ] && break
  sleep 10
done
```

| Registration status | Meaning                              |
| ------------------- | ------------------------------------ |
| `registering`       | Purchase in progress (1-15 minutes)  |
| `registered`        | Domain purchased and auto-configured |
| `failed`            | Registration failed — check message  |

When status reaches `registered`, Locus has automatically:

1. Created DNS A record pointing to Locus
2. Requested and validated an SSL certificate
3. Added the certificate to the load balancer

**Step 4: Attach to a service**

```bash
curl -s -X POST \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"serviceId": "'"$SERVICE_ID"'"}' \
  "https://api.buildwithlocus.com/v1/domains/$DOMAIN_ID/attach"
```

## Domain Management Endpoints

| Action                     | Endpoint                                        | Notes                                                                                       |
| -------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------- |
| **List all domains**       | `GET /v1/domains`                               | All domains in workspace. Purchased domains include `registrationPrice` with `renewalPrice` |
| **Register domain (BYOD)** | `POST /v1/domains`                              | `{domain, projectId?}` → cert + CNAME target                                                |
| **Verify domain**          | `POST /v1/domains/:domainId/verify`             | Checks CNAME + cert status                                                                  |
| **Check availability**     | `GET /v1/domains/check-availability?domain=`    | Returns availability + price                                                                |
| **Get suggestions**        | `GET /v1/domains/suggestions?keywords=&tlds=`   | Domain name suggestions                                                                     |
| **Purchase domain**        | `POST /v1/domains/purchase`                     | `{domain, contact, projectId?}` → 202                                                       |
| **Registration status**    | `GET /v1/domains/:domainId/registration-status` | Poll until `registered`                                                                     |
| **List by project**        | `GET /v1/domains/project/:projectId`            | Filters workspace domains by project                                                        |
| **Get domain**             | `GET /v1/domains/:domainId`                     |                                                                                             |
| **Check cert status**      | `GET /v1/domains/:domainId/status`              |                                                                                             |
| **Auto-create DNS**        | `POST /v1/domains/:domainId/create-dns-records` | Locus-managed DNS only                                                                      |
| **Attach domain**          | `POST /v1/domains/:domainId/attach`             | `{serviceId}`                                                                               |
| **Detach domain**          | `POST /v1/domains/:domainId/detach`             |                                                                                             |
| **Delete domain**          | `DELETE /v1/domains/:domainId`                  | Must detach first                                                                           |

## Rate Limits

| Limit                                        | Value | Error |
| -------------------------------------------- | ----- | ----- |
| Max pending/validating domains per workspace | 5     | `429` |
| Max total domains per workspace              | 20    | `400` |

Pending domains that fail auto-verification after 2 hours are marked `failed` and no longer count against the pending limit.

## Agent Workflow: Custom Domains

**For AI agents:** Domain workflows involve waiting on DNS propagation or registration — operations that take minutes, not seconds. Clear communication is critical because the human can't tell whether things are working or stuck.

### BYOD Flow

**Step 1: Register and present DNS records clearly**

After calling `POST /v1/domains`, present the DNS records in a copy-paste-friendly format:

```
I've registered api.example.com with Locus. You need to add two DNS records at your DNS provider:

1. SSL validation (for SSL certificate):
   Type: CNAME
   Name: _abc123.api.example.com
   Value: _xyz789.validate.buildwithlocus.com

2. Domain routing (points your domain to Locus):
   Type: CNAME
   Name: api.example.com
   Value: lb.buildwithlocus.com
```

**Step 2: Set DNS propagation expectations and explain auto-verification**

```
Once you've added the DNS records, Locus will automatically verify them every 5 minutes for up to 2 hours.
DNS propagation usually takes 1-5 minutes, but can take up to 30 minutes depending on your provider.
You can also let me know when you've added them and I'll trigger a manual check.
```

**Step 3: Check status or trigger manual verify**

After the human confirms DNS records are added, you can trigger an immediate check:

```
Checking DNS verification...
```

If verification fails (either `cnameVerified` or `certificateValidated` is false):

```
DNS records haven't propagated yet — this is normal. Auto-verification will keep checking every 5 minutes.
```

Or let auto-verification handle it — just check domain status later:

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://api.buildwithlocus.com/v1/domains/$DOMAIN_ID" | jq '.validationStatus'
```

**Step 4: Attach and confirm**

```
Domain verified and SSL certificate issued!
Attaching api.example.com to your service...
Done — your service is now live at https://api.example.com
```

### Purchase Flow

**Step 1: Always confirm before purchasing (costs real money)**

```
myapp.com is available for $12.00/year (auto-renews at $12.00/year).
Would you like me to proceed with the purchase? I'll need your contact details for domain registration.
```

Never purchase a domain without explicit human confirmation.

**Step 2: Set expectations after purchase**

```
Purchasing myapp.com... Domain registration takes 1-15 minutes.
I'll monitor the status and let you know when it's ready.
```

**Step 3: Poll and report**

```
Domain registered! Locus has automatically configured DNS, SSL, and routing.
Attaching myapp.com to your service...
Done — your service is now live at https://myapp.com
```

### Communication Best Practices

**DO:**

- Format DNS records for easy copy-paste (name, type, value on separate lines)
- Always confirm before purchasing a domain — it costs money
- Distinguish between "not yet propagated" and "something is wrong" (propagation = normal, wrong records = problem)
- Mention SSL is included — humans often worry about certificates

**DON'T:**

- Assume verification failure means something is broken — DNS propagation takes time
- Purchase a domain without explicit human approval
- Skip presenting the DNS records — the human needs them to update their provider
- Rush verification retries — wait at least 2 minutes between attempts
