Overview
fjall domain manages custom domains for your applications. It scaffolds domains, deploys their AWS Route 53 hosted zones, classifies every live record against your declarations, verifies nameserver delegation, and imports or exports DNS records.
A domain is declared in fjall/domains/<zone>/infrastructure.ts with the typed Domain construct. See Domain for the construct API.
Usage
--agent (structured TOON output) with --budget, --fields, and --full.
Subcommands
Create a domain
Scaffold a new apex domain in the current project:--parent defaults to the longest-suffix registered parent found in fjall-config.json. --account and --region default to the parent entry’s pins for a delegated child, otherwise the first successful deploy records them. --per-account is idempotent, and a solo-account organisation fans out nothing.
fjall create domain --name example.com is the create-family spelling of the same scaffold. It takes -n, --name <name> instead of the positional argument and carries the same option set.
Both create and import register the domain in fjall-config.json automatically, so the organisation cascade can deploy it. A domain nested under an already-registered domain is registered as delegated with that domain as its parent. Everything else is apex.
Deploy a domain
Deploy the domain’s hosted zone and DNS infrastructure to AWS:--plan exits 2 when changes are pending approval. Spell each --remediate value exactly as the ticket prints it, for example --remediate 'www.example.com/A'=recreate. A weighted, latency, or failover variant carries its SetIdentifier in a three-part key: --remediate 'www.example.com/A@blue'=recreate.
The deploy echoes the resolved identity before any synth or AWS read, on every route including --plan: Deploying 'example.com' with AWS account 123456789012 (target 'production'). Resolution fails closed. An unresolvable --target stops before anything is read or mutated, with a message listing the available targets. An unresolvable AWS identity names the cure (fjall connect) rather than falling back to an unnamed account. --region without --target applies the region override to the ambient credential chain.
When you deploy an adopted zone (one that already had live records) for the first time, any record that both the live zone and your infrastructure.ts claim must be released through the record-takeover ceremony. The deploy stops with a ticket naming each colliding record, and you consent per row with --remediate '<name>/<TYPE>'=recreate. Consent withheld means exit code 4, with nothing released and nothing deployed. See Destructive Changes.
--allow-replace does not exist on domain surfaces. It is rejected
rather than silently dropped. Consent to takeover ticket rows with
--remediate '<name>/<TYPE>'=recreate, appending @<setIdentifier> exactly
when the live record set has one.fjall domain deploy is the only domain-deploy spelling. fjall deploy takes application names only and rejects fjall deploy domain <name> with a teaching error before anything is validated or deployed.
Delegated child domains deploy in two phases automatically. The zone and its NS delegation go first. The certificates follow once the delegation has propagated and the CAA preflight passes. This prevents ACM DNS validation hanging against an undelegated zone.
An external-records domain with certificates needs you to act before its deploy can finish. CloudFormation creates each certificate and then waits until its DNS validation record resolves publicly, and only you can create that record at the external DNS provider. ACM chooses the record when the certificate is created, so the deploy reads it from ACM while CloudFormation waits and announces each record as a warning:
fjall domain deploy once the domain stack has settled.
The read needs cloudformation:DescribeStacks, cloudformation:ListStackResources, cloudformation:DescribeStackEvents and acm:DescribeCertificate. A denied read is announced once, naming those four actions, and the deploy then stops looking: no record is announced for the rest of that run, while CloudFormation keeps waiting. Grant the four actions and re-run, or read each certificate’s validation record from ACM yourself (see Domain).
Every successful deploy, apex and delegated alike, self-records the entry’s account and region keys in fjall-config.json: the 12-digit AWS account id and the region the domain stack deployed into. Record classification uses a delegated child’s account key to gather the child zone’s cross-account evidence, and the parent-side delegation resolution reads the apex entry’s.
Verify delegation
Check whether the domain’s nameservers point to Route 53:--target defaults to the domain entry’s home pin, falling back to the active target.
The command runs five progress steps (Checking DNS propagation, Checking delegation role, Checking zone drift, Checking certificate validation, Completing), then prints the verdict:
Delegation role: and Hosted zone: print only when those audits return a verdict.
After the DNS check, verify audits the parent-side delegation role. A managed zone whose fjall-config.json declares delegated children needs its stack to publish a delegation-role export, otherwise no child NS delegation can be written in-stack. The audit reports published, missing (redeploy the parent, which publishes the export), or unavailable (credentials could not be resolved, so run fjall connect). It is diagnostic only and never gates the DNS verdict.
An external-records domain skips the nameserver lookup, because its DNS stays at the external provider. The verdict prints a DNS line in place of the delegation lines, and the result carries registrar: "external-records" and delegated: false with no nameservers:
Certificate validation: reports the worst verdict across the certificates:
Immediately after a deploy requests a certificate, ACM may not have published its record yet. The verdict is still
pending, and verify warns ACM has not published the validation record for example.com yet; re-run 'fjall domain verify example.com' in a minute. rather than naming a record.
An unusable verdict warns with ACM’s status verbatim, and its reason when ACM gives one: ACM reports the certificate for example.com as VALIDATION_TIMED_OUT; it will not become valid without a new certificate request. A validation record that was never created is the usual cause of VALIDATION_TIMED_OUT.
ACM derives a validation record from the account and the name, so a wildcard and its base name share one record, and so do two certificates covering the same name. Each distinct record is reported once. Its domainNames (the (validates example.com) suffix in the warning) lists every name it validates, so you create that CNAME once.
The records still to create are also returned as validationRecords (name, type, value, domainNames) for --agent and --fields projections. The reads need cloudformation:DescribeStacks, cloudformation:ListStackResources, cloudformation:DescribeStackEvents and acm:DescribeCertificate.
Print registrar delegation instructions instead of the verdict block:
--show-delegation refuses an external-records domain, because Fjall creates no hosted zone for it and there are no nameservers to delegate to.
List live records
List every live record in the zone with its ownership classification:fjall domain records example.com behaves identically, because list is the default action.
Each record is classified as one of:
A
satellite record covers the apex alias an application stack manages, live ACM-validation records, and a child’s NS row written through Custom::CrossAccountZoneDelegation. A residue record covers validation records for certificates that no longer exist, and aliases to AWS targets proven gone.
Classification is read-only and fail-closed. An evidence gap makes a record unknown, never residue, and unknown records are never offered for remediation.
Evidence is drawn from two sources: live CloudFormation stack templates, including Custom::CrossAccountZoneDelegation delegation resources, and ACM certificate ownership. A validation record counts as satellite while its certificate is live.
Evidence crosses accounts. A delegated child zone living in another AWS account classifies its parent-side NS row as satellite when the child’s domain stack fully proves ownership. The stack must be live, its template must claim the NS record via a literal DelegatedZoneName, and its nameservers stack output must match the live NS values (order, case, and trailing-dot insensitive). Any missing piece fails closed to unknown with a warning naming the child domain and the cure, never a guess.
The classifier finds the child through the delegated domain’s account key in fjall-config.json, set automatically by a successful delegated fjall domain deploy. You can also add it by hand. An unreachable child account affects only that child’s row. The rest of the zone’s evidence, residue detection included, is unaffected.
Sync a zone
Report the classification of every live record (read-only):
Spell each
--remediate value exactly as the ticket prints it, for example --remediate '_abc.example.com/CNAME'=recreate.
--fix runs the same destruction ceremony as deploy. A ticket names each residue record, you consent per row, and withheld consent exits 4 with nothing deleted. Only residue-classified records ever appear on the ticket, so unknown records are never remediable.
Delegate a child domain
Write (or report) the NS delegation for a delegated child domain:Destroy a domain
Destroy the domain’s stack through the destruction ceremony:
Consent to an apex zone by name (
--remediate 'example.com'=recreate). Consent to a delegated child’s NS row with its zone-qualified key (--remediate 'staging.example.com/NS'=recreate, or =forget).
Destroying an apex zone requires consenting to the zone by name because a recreated zone gets a different nameserver set, so DNS stops resolving until the registrar is repointed. Destroying a delegated child deletes its NS record from the parent zone first. When the parent cannot delete it, because no delegation role is published, the ticket offers =forget instead.
Export DNS records
Export the domain’s DNS records to a BIND zone file:Import a domain
Import from an existing Route 53 hosted zone (the default when a domain is given):
Import curates an
infrastructure.ts from the live zone. Records owned by other live stacks (satellites) are deliberately left undeclared, with a header explaining each omission. The first fjall domain deploy of the adopted zone then passes through the record-takeover ceremony described above.
Guided import (no domain argument)
Runfjall domain import with no domain argument in a terminal to open the guided flow:
How do you want to import a domain?picks between a discovered Route 53 zone and a BIND zone file. Choosing BIND prompts forPath to the BIND zone file:.- Fjall discovers zones, then
Select a hosted zone to import:lists them. Already-configured zones are hidden. - A
Reading records ... and classifying ownership...step runs the classifier. Curate records for <zone>: N candidate(s)is a multi-select. Each row carries a[declared],[satellite],[residue], or[unknown]chip, and satellite and residue rows start unticked.Import review: <zone>reportsKeeping X of N record(s); Y excluded., then asksRegistrar for the imported domain:(route53 or external-records).
Importing domain: <zone>, and finishes with Imported domain '<zone>'..
Prompt-driven import (domain argument plus --interactive)
fjall domain import example.com --interactive uses a plain prompt sequence with no zone discovery and no curation screen:
Zone name.Registrar mode(route53, external-delegated, or external-records). Choosing external-delegated addsDelegated subdomain (e.g. 'app').Add a record?loops overRecord type,Record name (use @ for apex),Record value, andTTL (seconds, blank for default).Add a certificate?collectsCertificate primary domainandSANs (comma-separated, blank for none).Add a delegation?collectsSubdomain to delegate,Target account ID, andAutomate delegation?.- A single confirm,
Accept N records, M certificates, K delegations?, gates the write.
List domains
Show all domains configured in the current project:Eject a domain
Remove a domain from Fjall management while keeping its AWS resources in place:external-records domain emits the same record outputs the Fjall-managed stack does: the declared records only. Neither side describes a certificate-validation record, because ACM chooses those at deploy time (see Domain).
Exit codes
Next Steps
Domain construct
Declare zones, records, certificates, and delegation in infrastructure.ts.
Destructive Changes
How the destruction ceremony and named consents work.
Deploy
Deploy your application to AWS.
Connect AWS
Connect an AWS account to deploy into.