Skip to content

Part 7 of 7 · Domain renewal watcher series ~7 min read

Engineering reference: the domain renewal watcher architecture

The first six posts are for the person deciding whether to build this. This one is for the person building it. Same system, no analogies: the services by name, the functions, the two tables, the RDAP path, and why there is no model in it.

Key takeaways

  • Single region, single account. Every resource is regional; nothing is global except the IAM roles.
  • 3 Lambda functions, each with its own execution role. No shared role, no wildcards on resources.
  • 2 DynamoDB tables, each keyed so the concurrency story is a condition expression rather than a lock.
  • One Bedrock model, called once, with a JSON schema it must fill or leave null.
  • Nothing always-on: no instance, no container, no provisioned capacity.

The system, by service name

The domain renewal watcher drawn with AWS service namesThree boxes across the top outside the AWS account. RDAP and certificate transparency logs, reached over outbound HTTPS. The Domain register, read through the Google Sheets API read-only. And SES outbound, carrying escalations and the quarterly digest. Inside the account, three groups. EventBridge carrying a weekly check and a monthly discovery run. Three Lambda functions named check, discover and escalate. And two DynamoDB tables named domains and snapshots. A note gives the region as us-east-1, one account, outbound HTTPS only, and states that no credential in it can renew anything.AWS ACCOUNTRDAP + CT logsoutbound HTTPSDomain registerSheets API, read-onlySES outboundescalation, digestEventBridgeweekly check,monthly discoveryLambda x3check, discover,escalateDynamoDB x2domains, snapshotsingroundsoutus-east-1. One account. Outbound HTTPS only; no credential can renew anything.
Fig 1. The same shape as Part 1 with the service names filled in. Nothing here is new; it is the same three groups, named.
  • Compute
  • Database
  • App integration
  • Networking
  • Management

Region and account

  • Region: us-east-1. Chosen because SES inbound receipt rules exist in only a subset of regions and this one has the widest Bedrock model availability. If your data has to stay elsewhere, check both constraints before moving: inbound SES is the binding one.
  • Account: one. This is a small system, and a separate account per environment costs more in wiring than it saves. A dev and a prod stack in the same account, with distinct resource prefixes, is the right size here.
  • Everything is regional. The only global resources are the IAM roles and policies. There is no CloudFront, no global table and no cross-region replication, because nothing here has a latency or durability requirement that would justify them.

Lambda inventory

FunctionTriggerDoesTimeout / memory
dr-checkEventBridge weeklyRDAP per domain; diffs expiry, status and nameservers120s / 512 MB
dr-discoverEventBridge monthlyCertificate logs, hosted zones, and registrar charges120s / 512 MB
dr-escalateEventBridge dailyThe ladder, the post-expiry clock, and the quarterly digest30s / 512 MB

Splitting this into separate functions is not about modularity. It is that only one of them needs Bedrock permissions and only one is reachable from the public internet, and neither of those is true if it is one handler behind a router.

IAM, scoped

RoleAllowedOn
dr-check-roledynamodb:PutItem, secretsmanager:GetSecretValueDomains and snapshots; the Sheets credential only
dr-discover-roleroute53:ListHostedZones, dynamodb:PutItemZones, read; the domains table
dr-escalate-roledynamodb:Query, ses:SendEmailDomains, read; one verified identity

No role has a Resource: “*” on anything that writes, and every GetSecretValue grant names a single secret arn. That is why there is more than one secret rather than one JSON blob with everything in it.

DynamoDB schemas

Table: domains

PK   domain            S   example.com
     state             S   watched | candidate | not_ours
     expiry            S   2027-03-14
     source            S   rdap | invoice   — how confident that date is
     registrar         S   name and IANA id
     registrar_account S   the login identifier, not a credential
     pays_with         S   card ending 4417
     internal_owner    S   a person on the staff list, checked
     depends           L   [mx, portal, redirect]
     nameservers       L   as last seen
     status            L   EPP status codes as last seen
     last_checked      S   2026-07-26T06:00:00Z
     fail_count        N   consecutive lookup failures

`last_checked` is what a separate heartbeat scans. A domain not checked in
three weeks alarms whatever the rest of the system believes.

Table: snapshots

PK   domain            S   example.com
SK   checked_at        S   2026-07-26
     rdap              S   the response, as received
     ttl               N   epoch, +2 years

Keeping the raw response means a nameserver change can be evidenced later
rather than asserted, and a parsing change can be tested against history.

Inbound and outbound

  • Outbound HTTPS only. There is no inbound path at all except the signed acknowledgement links in escalation messages.
  • The IANA bootstrap is fetched weekly and cached. Fetching it per lookup is both wasteful and impolite to a free public service.
  • RDAP requests identify themselves with a user agent naming the business and a contact address, which is the norm for automated registry queries.
  • No credential in this account can renew, transfer or modify a domain. The escalation tells a person to go and do it in the registrar’s own dashboard.

The model call

  • There is no model in this system. RDAP returns structured JSON and status codes are a fixed vocabulary.
  • The one place a model would fit is parsing free-text WHOIS for the registries without RDAP, and the honest choice is to not track those from WHOIS at all.
  • Those domains carry an invoice date marked as such, with a longer lead time, rather than a parsed date presented as authoritative.
  • Discovery is search and set difference, not classification. A candidate is any domain found that is not in the register.
  • This is worth noting because it is a system where adding a model would make everything less reliable and nothing faster.

Things worth knowing before you build it

  • Query the registry, not the registrar. They disagree exactly when it matters: a failed renewal payment, a transfer in flight, an acquired registrar with stale records.
  • Do not stop reporting at expiry. The redemption and pending-delete windows are where days matter, and it is where most watchers go quiet.
  • Escalate to your staff list. The registration contact is the least reliable address in the business and every registrar already emails it.
  • Cache the IANA bootstrap. Fetching it per domain per week is rude to a free service and gains nothing.
  • Record which card pays for each domain. An expired card is the most common cause of a renewal everybody assumed had happened.

That is the whole system. Seven posts, one diagram at a time, and nothing in it that needs a server.

All posts