---
id: w1/m11/t001
title: REST custom-domains endpoints over spec.host/hosts, verified against Render's OpenAPI
worker: worker1
status: done
estimate: 30m
depends_on: []
---

## Objective

`GET/POST /v1/services/{id}/custom-domains` and `DELETE /v1/services/{id}/custom-domains/{domain}` as a thin layer over `App.spec.host`/`spec.hosts[]` — **verify the request/response shapes against Render's OpenAPI (`render-public-api-1.json`) before coding** (list envelope, domain object fields, status codes), same discipline as the services/env-vars surfaces.

## Context

- `spec.host` is the primary domain (from `domains[0]` in bex.yml), `spec.hosts[]` the extras — `lego/types/v1alpha1/app_types.go`; operator reconciles Traefik + cert-manager per docs/ADR005-custom-domain.md.
- New verbs live in the apps feature (`lego/backend/internal/apps/`) or a sibling fragment file — one service implementation, adapters thin.
- Authorize gate first, like every verb (`s.Authorize`).

## Steps

1. Extract the Render shapes from the OpenAPI spec into the task notes (fields bex can honor vs omit — follow the "safe superset" rule).
2. Service methods: ListDomains / AddDomain (spec patch, dedupe, validate hostname) / DeleteDomain (also handles removing the primary → promote next or clear).
3. REST fragment + table row updates in docs/ADR006-bex-api.md.

## Files

- `lego/backend/internal/apps/service.go` (+ `domains.go` if it reads better)
- `lego/backend/internal/apps/rest.go`
- `docs/ADR006-bex-api.md`

## Acceptance criteria

- [ ] POST adds to `spec.hosts[]` (or sets `spec.host` when empty); DELETE removes; GET lists both primary and extras in Render's envelope.
- [ ] Shapes match the OpenAPI spec for every field bex serves.

## Out of scope

- GraphQL/MCP (t002); verification status (t003); DNS instructions/ownership checks (Render's `verificationStatus` flow beyond cert state).
