Skip to content

DNS Record Management (F3.3) ​

Feature: F3.3 β€” DNS Record Management
Status: 🟒 COMPLETED
Package: @vexlyx/api, @vexlyx/dashboard, @vexlyx/shared
Prisma Models: Domain, DnsRecord
Infrastructure: CoreDNS v1.11, RFC 1035 Zone Engine


1. Overview ​

DNS Record Management enables full authoritative DNS zone management for domains hosted on Vexlyx nameservers.

Drawing from modern cloud-native standards (Cloudflare, CoreDNS, Kubernetes) and hosting control panels (cPanel, Plesk), Vexlyx decouples DNS management into:

  1. Type-safe Database Persistence: Records are stored in PostgreSQL with strict per-type Zod validation (A, AAAA, CNAME, MX, TXT, NS, SRV) and dedicated columns for priority, weight, and port.
  2. Standard RFC 1035 Zone Engine: Generates and parses standard RFC 1035 BIND/CoreDNS compatible zone files (.zone / .db) with serial incrementing and comment preservation.
  3. Cloud-Native CoreDNS Integration: Powered by an official CoreDNS container configured with the auto plugin, which detects zone file updates in ./docker/coredns/zones/{domain}.db and reloads them dynamically without service interruptions.
  4. Multi-Resolver Propagation Testing: Tests live DNS resolution in real-time across major public DNS providers (Cloudflare 1.1.1.1, Google 8.8.8.8, and Quad9 9.9.9.9).

2. Architecture & Data Flow ​

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Next.js Dashboard UI                     β”‚
β”‚  - /domains (Global domain overview & "Manage DNS" link)    β”‚
β”‚  - /domains/[id]/dns (Dedicated DNS Zone Management page)   β”‚
β”‚  - Breadcrumbs, Metric cards, Filter pills, Full-width tableβ”‚
β”‚  - Zone Export / Import Drawers & Live Propagation Checker  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                               β”‚ HTTP / JSON
                               β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Fastify API Server                      β”‚
β”‚  - GET    /api/domains/:id/dns          (List records)      β”‚
β”‚  - POST   /api/domains/:id/dns          (Create record)     β”‚
β”‚  - POST   /api/domains/:id/dns/defaults (Setup template)    β”‚
β”‚  - PATCH  /api/domains/:id/dns/:recId   (Update record)     β”‚
β”‚  - DELETE /api/domains/:id/dns/:recId   (Delete record)     β”‚
β”‚  - GET    /api/domains/:id/dns/export   (RFC 1035 export)   β”‚
β”‚  - POST   /api/domains/:id/dns/import   (RFC 1035 import)   β”‚
β”‚  - POST   /api/domains/:id/dns/:recId/propagation (Test)    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚                              β”‚
                β–Ό                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     PostgreSQL (Prisma)       β”‚ β”‚  CoreDNS RFC 1035 Zones   β”‚
β”‚  - model Domain               β”‚ β”‚  docker/coredns/zones/    β”‚
β”‚  - model DnsRecord            β”‚ β”‚  {hostname}.db            β”‚
β”‚    * type (A, MX, etc.)       β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚    * name (@, www, etc.)      β”‚               β”‚ Auto-reloaded
β”‚    * value, ttl, priority     β”‚               β–Ό
β”‚    * weight, port             β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ CoreDNS Container (:53)   β”‚
                                  β”‚ Authoritative answers for β”‚
                                  β”‚ hosted zones via `auto`   β”‚
                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

3. Supported Record Types & Validation ​

Record TypeValue FormatRFC Rules & Validation
A192.0.2.1Must be a valid IPv4 address (no leading zeroes).
AAAA2001:db8::1Must be a valid IPv6 address.
CNAMEtarget.domain.comMust be a valid domain/hostname. RFC 1912: Cannot be apex (@) and cannot coexist with other records of the same name.
MXmail.domain.comMail exchange host. priority (0–65535) is required.
TXTArbitrary stringMax length 2048 characters. Escapes quotes automatically for SPF, DKIM, DMARC.
NSns1.vexlyx.comNameserver hostname delegation. Cannot be an IP address.
SRVsipserver.domain.compriority (0–65535), weight (0–65535), and port (1–65535) are required.

4. RFC 1035 Zone Generation & CoreDNS Integration ​

CoreDNS Configuration (docker/coredns/Corefile) ​

.:53 {
    auto {
        directory /etc/coredns/zones (.*)\.db
        reload 5s
    }
    forward . 1.1.1.1 8.8.8.8
    cache 30
    log
    errors
}

Generated Zone File Format Example ​

text
; Zone file for example.com
; Generated by Vexlyx Control Panel
$ORIGIN example.com.
$TTL 3600

; SOA Record
@   IN  SOA ns1.vexlyx.com. hostmaster.example.com. (
            2026090401 ; Serial (YYYYMMDDnn)
            7200       ; Refresh (2h)
            3600       ; Retry (1h)
            1209600    ; Expire (2w)
            3600 )     ; Minimum TTL (1h)

; Nameservers
@       IN  NS      ns1.vexlyx.com.
@       IN  NS      ns2.vexlyx.com.

; Resource Records
@                    3600     IN  A      192.0.2.1
www                  3600     IN  CNAME  @
mail                 3600     IN  A      192.0.2.2
@                    3600     IN  MX    10 mail.example.com
@                    3600     IN  TXT   "v=spf1 mx ~all"
_sip._tcp            3600     IN  SRV   10 50 5060 sip.example.com

5. API Reference ​

All endpoints are scoped under /api/domains/:id/dns and require authentication (401 Unauthorized if unauthenticated).

1. List DNS Records ​

  • Endpoint: GET /api/domains/:id/dns
  • Response: 200 OK β€” Array of DnsRecordResponse.

2. Create DNS Record ​

  • Endpoint: POST /api/domains/:id/dns
  • Body: CreateDnsRecordInput
  • Response: 201 Created β€” Created DnsRecordResponse.
  • Endpoint: POST /api/domains/:id/dns/defaults
  • Action: Provisions @ A record, www CNAME, and ns1/ns2 NS records if they do not already exist.
  • Response: 200 OK β€” Full array of zone records.

4. Update DNS Record ​

  • Endpoint: PATCH /api/domains/:id/dns/:recordId
  • Body: UpdateDnsRecordInput
  • Response: 200 OK β€” Updated DnsRecordResponse.

5. Delete DNS Record ​

  • Endpoint: DELETE /api/domains/:id/dns/:recordId
  • Response: 204 No Content.

6. Export Zone File ​

  • Endpoint: GET /api/domains/:id/dns/export
  • Response: 200 OK β€” text/plain RFC 1035 zone content with Content-Disposition: attachment.

7. Import Zone File ​

  • Endpoint: POST /api/domains/:id/dns/import
  • Body: { "zoneContent": "...", "strategy": "skip" | "replace" }
  • Response: 200 OK β€” { importedCount: number, records: DnsRecordResponse[], errors: string[] }.

8. Test DNS Propagation ​

  • Endpoint: POST /api/domains/:id/dns/:recordId/propagation
  • Response: 200 OK β€” DnsPropagationResponse containing resolver status (MATCH, MISMATCH, NOT_FOUND), latency, and detected records.

6. How to Test ​

Run the automated test suite:

bash
python tests/test_dns_management.py

Verify TypeScript & Linting:

bash
pnpm typecheck
pnpm lint

7. How to Extend ​

  • DNSSEC Support: CoreDNS supports DNSSEC signing via the dnssec plugin. Keys can be generated per zone and configured in the Corefile.
  • Secondary Nameserver Clustering: Syncing zone files to remote nameservers via RFC 1995 IXFR / RFC 5936 AXFR or Git-based file synchronization.
  • CAA & DS Records: Easily added to DnsRecordTypeSchema with corresponding RFC validation.

8. Responsive Design Notes (F3.5) ​

A UX/responsiveness pass on /domains and /domains/[id]/dns fixed several overflow issues found through manual testing at 360px/768px/1024px:

  • Domain filter bar (domains/page.tsx): the Type/Status/Project Select triggers used fixed pixel widths (w-[140px]/w-[150px]/w-[160px]) that didn't shrink on narrow viewports. Changed to w-full sm:w-35/sm:w-37.5/sm:w-40 (canonical Tailwind spacing) so they stack full-width below sm and take fixed widths side-by-side above it.
  • Domain card action row (domains/page.tsx): for non-ACTIVE domains, a 4th "Verify" button was appended to a single flex justify-between row already holding "Manage DNS", "Manage SSL", and a help icon. With no wrap, the row overflowed the card width and visually bled into the neighboring grid card. Split into two rows: DNS/SSL/help on top, a full-width "Verify" button below when present.
  • DNS Verification Ownership dialog (domains/page.tsx): the TXT record info box used a grid-cols-3 label-row/value-row layout (Record Type / Host / TTL headers above their values). Reflowing that grid to a single column on mobile would separate labels from values in a confusing order. Restructured into three stacked label:value rows instead, which reads correctly at every width.
  • DNS records table Name column (domains/[id]/dns/page.tsx): displayed record.name followed by an appended .{domain.hostname} suffix, assuming name is always zone-relative. The auto-created verification TXT record stores name as the full hostname already, so the suffix duplicated it and, combined with a flex items-center wrapper, visually overlapped once the name wrapped across lines in the narrow column. Fixed by skipping the suffix when record.name already ends with the domain's hostname, and replacing the flex row with plain wrapping text (break-all) plus align-top on the row so a wrapped Name cell doesn't throw off sibling cells' alignment.
  • DNS table toolbar (domains/[id]/dns/page.tsx): the record-type filter pills, search input, and "Recommended Defaults" button relied on overflow-x-auto to stay contained, but the row still extended past the card in some layouts. Replaced with flex-wrap throughout (pills, search box, button) so the row wraps onto additional lines instead of ever overflowing β€” the outer group only switches to a single side-by-side row at the lg breakpoint, where there's enough width for all of it.
  • SRV form grid (domains/[id]/dns/page.tsx): the Priority/Weight/Port grid-cols-3 in the Add/Edit Record dialog was left unchanged β€” three short number inputs fit comfortably at 360px without cramping.

9. Two DNS Modes (F5.22) ​

Vexlyx supports two independent, non-overlapping ways for a domain to end up pointed at a project. Confusing them was a real support complaint ("why I show the NS panel if here I just want to add a domain, not manage the domain's nameserver") β€” this section exists to keep the distinction clear for anyone touching either flow.

F3.1 β€” Self-serve A/TXTF3.3 β€” CoreDNS zone hosting (this doc)
What it doesUser adds one A record (and a TXT record for ownership verification) at their existing registrar/DNS provider.User delegates the domain's entire authoritative DNS β€” every record, not just this project's β€” to Vexlyx's own nameservers (ns1.vexlyx.com / ns2.vexlyx.com).
Where/domains β€” "Verify" flow + the DNS Verification Ownership instructions dialog./domains/[id]/dns β€” the full zone management page described above.
Required?Yes β€” this is how a domain routes to a project at all.No. Entirely optional; most users never need to visit /domains/[id]/dns.
PrerequisiteNone beyond DNS access at the existing registrar.Changing the domain's NS records at the registrar to point at Vexlyx β€” a much bigger, riskier step (it moves all DNS, including any existing MX/email records) than adding one A record.

Before F5.22, /domains showed a "Manage DNS" button styled identically to "Manage SSL" on every domain card, implying both were required next steps. It has been relabeled "Host DNS on Vexlyx" with a tooltip clarifying it's optional and what it actually delegates (apps/dashboard/src/app/(panel)/domains/page.tsx). The zone page itself now opens with an explainer callout making the same distinction before the metric cards (apps/dashboard/src/app/(panel)/domains/[id]/dns/page.tsx). No behavior in either flow changed β€” this was a labeling/clarity fix only.

F5.25 update: the mode is now persisted and enforced ​

F5.22 above was labeling only. As of F5.25 each domain has a dnsMode:

  • CONNECTED (default): DNS stays with the user's provider. No DnsRecord rows or CoreDNS zone file are created, syncZoneFile is a no-op, and every /api/domains/:id/dns* endpoint returns 409 DNS_NOT_MANAGED. The dashboard hides the DNS button on /domains and the SSL page, and /domains/[id]/dns shows the opt-in flow instead of the editor.
  • MANAGED: Vexlyx CoreDNS is authoritative. Reached via PATCH /api/domains/:id/dns-mode { "mode": "MANAGED" }, which requires an ACTIVE (verified) domain and a passing NS delegation check (POST /:id/dns-mode/check, queries 1.1.1.1/8.8.8.8/9.9.9.9 for the nameservers in DNS_NAMESERVERS). Enabling seeds the default records and writes the zone file.
  • Switching back removes the zone file but keeps DnsRecord rows, so re-enabling is lossless. It is blocked (409 DNS_MODE_MAIL_ACTIVE) while the domain has mailboxes, since their MX/SPF/DKIM records live in the zone.
  • Subdomains inherit the parent's mode on creation. Existing domains were backfilled by the add_domain_dns_mode migration: MANAGED if they had any record beyond the _vexlyx-challenge TXT, else CONNECTED.
  • Testing: with NODE_ENV=test or VEXLYX_MOCK_DNS=true the delegation check reports delegated; verify a domain with POST /:id/verify?mock=true first.

F5.27: serving DNS publicly and migrating a live domain ​

To be a real nameserver the server must answer on a public address without becoming an open resolver:

  1. In /etc/vexlyx/vexlyx.env set VEXLYX_DNS_BIND=<host private IP> (not 0.0.0.0: systemd-resolved holds 127.0.0.53:53), VEXLYX_COREFILE=./docker/coredns/Corefile.public (authoritative-only, no forward) and DNS_NAMESERVERS=ns1.<yourdomain>,ns2.<yourdomain>.
  2. Create A records for those nameserver names pointing at the server's public IP, open UDP+TCP 53 in the cloud firewall and ufw, then docker compose ... up -d coredns api.
  3. Migrating a live domain: verify ownership, use Prepare zone before switching (PATCH /dns-mode with skipDelegationCheck: true), recreate the domain's existing records in the zone, confirm with dig @<server-ip> <domain>, and only then change the registrar's nameservers. Until delegation passes, the DNS page shows a "Not live yet" banner.

Released under the MIT license.