docs/adr/0084-automation-milestones-tenant-opt-in-fail-closed-dns-carry-cost.md
Rama master · réplica de lectura del repositorio original.
# ADR-0084: Automation milestones — tenant opt-in, fail-closed DNS writes, carry-cost verdicts
## Metadata
| Field | Value |
| -------------- | ----------------------------------------------------------- |
| **Status** | Accepted |
| **Date** | 2026-10-15 |
| **Authors** | Alessio Brillo |
| **Deciders** | Alessio Brillo |
| **Supersedes** | — |
| **Relates to** | ADR-0002, ADR-0004, ADR-0006, ADR-0023, ADR-0038, ADR-0082, ADR-0083 |
| **Project** | DOMINUS |
## Context
ADR-0083 established the journaled post-acquisition pipeline
(`portfolio → site → indexing → dns → parking → listing`) behind the
`AUTOMATION_ENABLED` kill switch and the `AUTOMATION_DRY_RUN` default, and
its amendment fixed the shape of the fail-closed DNS write gate before any
provider existed. This ADR records the decisions taken while implementing
the remaining milestones (Phase 0, M3, the honest part of M4, M5) and the
boundaries that were deliberately not crossed.
The product goal is a fully automated lifecycle after a purchase — site,
indexing, DNS delegation, ads monetization, marketplace listing, exit — so
a SaaS tenant can buy a domain and let it become self-sustaining. Three
constructions in that path spend real money or mutate externally visible
state, and two external dependencies (parking and marketplace APIs) cannot
be verified without vendor access.
## Decision Drivers
1. A SaaS tenant must own its own switch, without weakening the operator's
global brake (ADR-0002 conservatism, ADR-0038 tenant isolation).
2. The DNS step is the first step that mutates state a visitor's traffic
depends on; the amendment's fail-closed model must survive contact
with implementation.
3. The community edition must keep working with zero paid API
dependencies; vendor adapters may only land verified (ADR-0082).
4. The carry-cost verdict must never drop a domain on its own: it feeds
the drop engine as a Reprice signal, keeping the human gate.
## Decision
1. **Tenant opt-in (Phase 0).** A `tenant_automation_settings` table
(one row per tenant, default: opted out, dry-run) holds the tenant's
own switch. The effective gate is the conservative combination:
`enabled = env AND tenant`, `dryRun = env OR tenant`. The operator
brake can only ever tighten a tenant's wish, never loosen it. The
Automation dashboard page exposes settings, step journal and audit
trail; `dominus automate settings --enabled/--dry-run` does the same
from the CLI.
2. **Fail-closed DNS writes (M3).** The `dns` step writes only after an
independently corroborated conclusion about the current state: the
write provider's view of the zone plus a public-DNS read of the
delegation, and the write is confirmed publicly afterwards. Anything
else is a named skip (with the manual instructions recorded in the
journal) or a named, resumable failure (`write_unconfirmed`) — never a
best-effort "probably applied". `ManualDnsWriteProvider` is the honest
default; `CloudflareDnsWriteProvider` is the first automated adapter.
3. **Revenue ingestion is a read (M3).** `MonetizationProvider` exposes
one method; the first implementation is a CSV reader over an operator
export (zero paid API dependencies, same pattern as the comps/keyword
providers). Writes are idempotent at (domain, date, source), so a
repeated ingestion corrects figures instead of double-counting them.
No provider → the step skips with `monetization_provider_unavailable`.
4. **Tenant listing policy (M4, partial).** What works today without
verified vendor APIs: a fixed base price for automatic listings
(`tenant_listing_settings.pricePolicy`) and offer rules (auto-decline
below a floor, counter below a counter price; both off by default,
the decline floor always checked first). The Afternic/Dan/Sedo
ListingProvider adapters stay out until their APIs are verified
(ADR-0082); the manual provider keeps tracking listings locally.
5. **Carry-cost verdicts (M5).** A scheduler job (`REVENUE_SYNC`,
read-only) ingests revenue on a cron; `computeCarryCostVerdict`
annualises revenue over the observed window (never extrapolating from
less than 30 days) against the renewal cost and reports
`sustainable | marginal | loss | insufficient_data`. A `loss` refines a
`Keep` from the drop engine into `Reprice` — visible in the portfolio
and at `GET /api/v1/sustainability` — and never drops a domain by
itself.
## Considered Options
### Option A: Tenant opt-in as a single global switch (env only)
**Advantages:** nothing new to build.
**Disadvantages:** in a multi-tenant SaaS every tenant would inherit one
operator's choice, and the tenant could not turn automation off without
an operator. Rejected.
### Option B: Write-then-compensate DNS writes (retry/rollback on failure)
**Advantages:** familiar transactional shape.
**Disadvantages:** compensation assumes the compensating action succeeds —
precisely what is uncertain during the incident that produced the bad
read (ADR-0083 amendment). Rejected.
### Option C: Invent the Sedo/Afternic API contracts now
**Advantages:** the roadmap lines could be ticked.
**Disadvantages:** exactly the ADR-0082 failure mode — adapters that were
never exercised against the vendor, deleted one release later. Rejected;
they return with verified access.
## Consequences
**Positive:** the lifecycle is automated as far as honestly possible; the
dangerous steps (DNS, money) are gated twice (kill switch + dry-run) and
audited; the tenant sees and controls its own switch; every unverified
dependency degrades to a named skip instead of a silent failure.
**Negative:** new tables (0063–0065) and a new job type; operators must
understand that both switches must allow a run; the vendor adapters remain
a blocked line until API access exists.
**Compliance Check:**
- [x] ADR-0002 conservatism: no step acts on uncertain state; unset
configuration always skips.
- [x] ADR-0004 provider abstraction: every external dependency is behind
an interface (DnsWriteProvider, MonetizationProvider,
PublicDelegationReader).
- [x] ADR-0006 trademark gate: untouched, still the publish gate for
listings and offers.
- [x] ADR-0023 async execution: REVENUE_SYNC runs on the existing queue
with handler registration asserted by the parity test.
- [x] ADR-0038 tenant isolation: every new table is repository-scoped.
- [x] ADR-0082 honesty: nothing is declared before it works; unverified
adapters stay out.
- [x] English identifiers, comments and documentation throughout.