# Phase 2: Cloudflare DNS Client **Status:** ⬜ Not Started **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** backend ## Objective Create an `internal/dns` package with a `Provider` interface and a Cloudflare implementation using the Cloudflare API v4 (direct HTTP, no SDK). ## Tasks - [ ] Task 1: Define `Provider` interface in `internal/dns/provider.go` - `EnsureRecord(ctx, fqdn, ip) error` — create or update A record - `DeleteRecord(ctx, fqdn) error` — delete A record if exists - `ListRecords(ctx) ([]Record, error)` — list all A records in the zone - `Record` struct: ID, FQDN, Type, Content (IP), Proxied, TTL - [ ] Task 2: Create `internal/dns/cloudflare.go` — Cloudflare implementation - HTTP client with `Authorization: Bearer ` header - Base URL: `https://api.cloudflare.com/client/v4` - `EnsureRecord`: GET records by name, create if missing, update if IP differs - `DeleteRecord`: GET record by name, DELETE if found - `ListRecords`: GET all A records in zone - `ListZones`: GET zones for the token (for zone picker) - `TestConnection`: verify token works (GET /user/tokens/verify) - [ ] Task 3: Create `internal/dns/dns.go` — factory function - `NewProvider(providerName, config) (Provider, error)` - Config struct with token, zoneID - Returns `nil, nil` when providerName is empty (wildcard mode) - [ ] Task 4: Wire DNS test/zones endpoints in `internal/api/settings.go` - `POST /api/settings/dns/test` — create temp Cloudflare client, call TestConnection - `GET /api/settings/dns/zones` — create temp client, call ListZones ## Files to Modify/Create - `internal/dns/provider.go` — interface + Record type - `internal/dns/cloudflare.go` — Cloudflare implementation - `internal/dns/dns.go` — factory function - `internal/api/settings.go` — wire test/zones endpoints to real client ## Acceptance Criteria - Provider interface defined with EnsureRecord, DeleteRecord, ListRecords - Cloudflare client makes correct API calls with proper auth headers - EnsureRecord is idempotent (create if missing, update if changed, no-op if same) - DeleteRecord is idempotent (no error if record doesn't exist) - ListZones returns zone ID + name pairs - TestConnection returns success/failure ## Notes - Cloudflare API v4 docs: zones endpoint, dns_records endpoint - Use `context.Context` for timeout control on all HTTP calls - A records only (type "A"), TTL=1 (auto), proxied=false (DNS only, not CF proxy) ## Review Checklist - [ ] All tasks completed - [ ] Code follows project conventions - [ ] No unintended side effects - [ ] Build passes - [ ] Tests pass (new + existing) ## Handoff to Next Phase