Skip to main content

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

Every subcommand supports agent mode: --agent (structured TOON output) with --budget, --fields, and --full.

Subcommands

Create a domain

Scaffold a new apex domain in the current project:
Adopt an existing Route 53 hosted zone by reference instead of creating a new one:
Scaffold a delegated child under an already-registered parent:
Fan out one delegated child per workload account after the apex scaffold:
--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:
If the deploy fails while ACM is still waiting, for example when the deploy times out, its error repeats the outstanding records. CloudFormation keeps waiting after the command exits. Create the records, then re-run 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:
When the domain declares certificates and its stack holds them, the certificate validation step reads them from ACM. 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):
Delete consented residue records:
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:
When Fjall manages the parent zone, the child’s NS records are written into it through the parent’s delegation role and the command waits for propagation. When the parent is externally owned, the command prints the exact NS records to create at your registrar instead.

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 from a BIND zone file:
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)

Run fjall domain import with no domain argument in a terminal to open the guided flow:
  1. How do you want to import a domain? picks between a discovered Route 53 zone and a BIND zone file. Choosing BIND prompts for Path to the BIND zone file:.
  2. Fjall discovers zones, then Select a hosted zone to import: lists them. Already-configured zones are hidden.
  3. A Reading records ... and classifying ownership... step runs the classifier.
  4. 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.
  5. Import review: <zone> reports Keeping X of N record(s); Y excluded., then asks Registrar for the imported domain: (route53 or external-records).
Selecting a registrar is the commit. The scaffold write starts immediately, shows 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:
  1. Zone name.
  2. Registrar mode (route53, external-delegated, or external-records). Choosing external-delegated adds Delegated subdomain (e.g. 'app').
  3. Add a record? loops over Record type, Record name (use @ for apex), Record value, and TTL (seconds, blank for default).
  4. Add a certificate? collects Certificate primary domain and SANs (comma-separated, blank for none).
  5. Add a delegation? collects Subdomain to delegate, Target account ID, and Automate delegation?.
  6. 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:
From fjall 38 an ejected 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.