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:
- 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, andport. - Standard RFC 1035 Zone Engine: Generates and parses standard RFC 1035 BIND/CoreDNS compatible zone files (
.zone/.db) with serial incrementing and comment preservation. - Cloud-Native CoreDNS Integration: Powered by an official CoreDNS container configured with the
autoplugin, which detects zone file updates in./docker/coredns/zones/{domain}.dband reloads them dynamically without service interruptions. - Multi-Resolver Propagation Testing: Tests live DNS resolution in real-time across major public DNS providers (Cloudflare
1.1.1.1, Google8.8.8.8, and Quad99.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 Type | Value Format | RFC Rules & Validation |
|---|---|---|
| A | 192.0.2.1 | Must be a valid IPv4 address (no leading zeroes). |
| AAAA | 2001:db8::1 | Must be a valid IPv6 address. |
| CNAME | target.domain.com | Must be a valid domain/hostname. RFC 1912: Cannot be apex (@) and cannot coexist with other records of the same name. |
| MX | mail.domain.com | Mail exchange host. priority (0β65535) is required. |
| TXT | Arbitrary string | Max length 2048 characters. Escapes quotes automatically for SPF, DKIM, DMARC. |
| NS | ns1.vexlyx.com | Nameserver hostname delegation. Cannot be an IP address. |
| SRV | sipserver.domain.com | priority (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 β
; 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.com5. 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 ofDnsRecordResponse.
2. Create DNS Record β
- Endpoint:
POST /api/domains/:id/dns - Body:
CreateDnsRecordInput - Response:
201 Createdβ CreatedDnsRecordResponse.
3. Setup Recommended Defaults β
- Endpoint:
POST /api/domains/:id/dns/defaults - Action: Provisions
@A record,wwwCNAME, andns1/ns2NS 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β UpdatedDnsRecordResponse.
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/plainRFC 1035 zone content withContent-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βDnsPropagationResponsecontaining resolver status (MATCH,MISMATCH,NOT_FOUND), latency, and detected records.
6. How to Test β
Run the automated test suite:
python tests/test_dns_management.pyVerify TypeScript & Linting:
pnpm typecheck
pnpm lint7. How to Extend β
- DNSSEC Support: CoreDNS supports DNSSEC signing via the
dnssecplugin. 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
DnsRecordTypeSchemawith 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/ProjectSelecttriggers used fixed pixel widths (w-[140px]/w-[150px]/w-[160px]) that didn't shrink on narrow viewports. Changed tow-full sm:w-35/sm:w-37.5/sm:w-40(canonical Tailwind spacing) so they stack full-width belowsmand take fixed widths side-by-side above it. - Domain card action row (
domains/page.tsx): for non-ACTIVEdomains, a 4th "Verify" button was appended to a singleflex justify-betweenrow 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 agrid-cols-3label-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): displayedrecord.namefollowed by an appended.{domain.hostname}suffix, assumingnameis always zone-relative. The auto-created verification TXT record storesnameas the full hostname already, so the suffix duplicated it and, combined with aflex items-centerwrapper, visually overlapped once the name wrapped across lines in the narrow column. Fixed by skipping the suffix whenrecord.namealready ends with the domain's hostname, and replacing the flex row with plain wrapping text (break-all) plusalign-topon 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 onoverflow-x-autoto stay contained, but the row still extended past the card in some layouts. Replaced withflex-wrapthroughout (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 thelgbreakpoint, where there's enough width for all of it. - SRV form grid (
domains/[id]/dns/page.tsx): the Priority/Weight/Portgrid-cols-3in 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/TXT | F3.3 β CoreDNS zone hosting (this doc) | |
|---|---|---|
| What it does | User 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. |
| Prerequisite | None 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. NoDnsRecordrows or CoreDNS zone file are created,syncZoneFileis a no-op, and every/api/domains/:id/dns*endpoint returns 409DNS_NOT_MANAGED. The dashboard hides the DNS button on/domainsand the SSL page, and/domains/[id]/dnsshows the opt-in flow instead of the editor.MANAGED: Vexlyx CoreDNS is authoritative. Reached viaPATCH /api/domains/:id/dns-mode{ "mode": "MANAGED" }, which requires anACTIVE(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 inDNS_NAMESERVERS). Enabling seeds the default records and writes the zone file.- Switching back removes the zone file but keeps
DnsRecordrows, so re-enabling is lossless. It is blocked (409DNS_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_modemigration:MANAGEDif they had any record beyond the_vexlyx-challengeTXT, elseCONNECTED. - Testing: with
NODE_ENV=testorVEXLYX_MOCK_DNS=truethe delegation check reports delegated; verify a domain withPOST /:id/verify?mock=truefirst.
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:
- In
/etc/vexlyx/vexlyx.envsetVEXLYX_DNS_BIND=<host private IP>(not0.0.0.0: systemd-resolved holds127.0.0.53:53),VEXLYX_COREFILE=./docker/coredns/Corefile.public(authoritative-only, noforward) andDNS_NAMESERVERS=ns1.<yourdomain>,ns2.<yourdomain>. - Create A records for those nameserver names pointing at the server's public IP, open UDP+TCP 53 in the cloud firewall and
ufw, thendocker compose ... up -d coredns api. - Migrating a live domain: verify ownership, use Prepare zone before switching (
PATCH /dns-modewithskipDelegationCheck: true), recreate the domain's existing records in the zone, confirm withdig @<server-ip> <domain>, and only then change the registrar's nameservers. Until delegation passes, the DNS page shows a "Not live yet" banner.