---
id: w7/m6/t001
title: Cross-App uniqueness guard — reject a host already registered on another App (Render's "already in use")
worker: worker7
status: todo
estimate: 45m
depends_on: []
---

## Objective

Stop two Apps from claiming the same hostname. Today `AddDomain` only checks whether the host is a duplicate **on the same App**; a different App (any tenant) can register the same host, producing two Ingress rules and an undefined routing/cert-issuance outcome. Add a cross-App uniqueness check — mirroring Render's "This domain already exists on another site." — that rejects the add when the host is already registered on a different App.

## Context

- `lego/backend/internal/apps/domains.go:207-221` — `AddDomain`; the only current check is the same-App duplicate loop at 218-221.
- The control-plane store is the source of truth for which App owns which host — add a lookup "is this host registered on any other App?" A partial unique index on the host column is the durable enforcement (cf. the `tenant_members` partial-unique-index pattern in `lego/backend/internal/store/members.go:123`).
- Store-less (hand-applied App CR) mode has no cross-App index; in that mode the check degrades to best-effort (a CR list scan) or is skipped with the legacy caveat already documented for network policies — decide and note it.
- Return `core.ErrBadRequest` (or a conflict variant) so adapters render a 400-class validation error, not a 500.

## Steps

1. Add a store method + (partial) unique index enforcing host uniqueness across Apps; update the fake store.
2. In `AddDomain`, before writing, reject when the host is registered on a different App with a typed conflict error.
3. Handle the store-less path (best-effort CR scan or documented skip); keep the same-App idempotent re-add behavior intact.

## Files

- `lego/backend/internal/apps/domains.go` — `AddDomain`
- `lego/backend/internal/store/` — host-uniqueness lookup + migration/index + fake store

## Acceptance criteria

- [ ] Adding a host already registered on a different App is rejected with a typed 400-class/conflict error; adding a free host still succeeds; re-adding the same host to the same App stays idempotent.
- [ ] Store enforces uniqueness durably (index) where the store is present; the store-less behavior is decided and documented.

## Out of scope

- Reserved/platform-host rejection (t002); adapter/dashboard surfacing (t003).
