Docs / Records

Record reference

Every claimed name is one JSON file at domains/<name>.json, validated by lib/schema.js's validateRecord. No key outside this shape is allowed.

Shape

{
  "name": "you",
  "owner": { "github": "you" },
  "claimedAt": "2026-01-01T00:00:00.000Z",
  "records": {}
}

Top-level fields

name — the subdomain, lowercase. 2 to 32 characters, [a-z0-9] with internal hyphens only (never leading or trailing), and no punycode (an xn-- prefix, or -- as the third and fourth character, is rejected). Must match the filename: domains/you.json must contain "name": "you".

owner — exactly one key, github, the GitHub login that owns the record. Set once at claim time and immutable afterward; a pull request that changes it is rejected.

claimedAt — an ISO 8601 timestamp, set once at claim time. Also immutable.

records — an object holding zero or more record types, detailed below. An empty records object is valid: it's the default right after claiming, and it means the name serves the built-in profile card.

subdomains — optional, detailed in its own section below.

Record types

TypeShapeCoexistence
CNAMEA single hostname string.Cannot appear with A, TXT, MX, or URL.
AA non-empty array of IPv4 addresses.May coexist with TXT and MX. Not with CNAME or URL.
TXTA non-empty array of strings, each up to 255 characters.May coexist with A and MX. Not with CNAME or URL.
MX1 to 5 entries: { priority: 0-65535, value: hostname }May coexist with A and TXT. Not with CNAME or URL.
URLA single absolute http:// or https:// string.Must be the only key in records. No DNS record is created; see below.

These rules come straight from lib/schema.js. If anything on this page ever disagrees with that file, the file is correct.

Why CNAME and URL are exclusive

CNAME's exclusivity isn't a rule this registry invented, it's a DNS protocol constraint. A CNAME aliases a name to another name entirely, and DNS doesn't allow a name with a CNAME to carry any other record type, since that would leave a resolver with two contradictory answers for what the name is.

If you need both a routing target and a verification string at the same name, use A with your host's IP addresses instead of CNAME, since A and TXT may coexist.

URL is exclusive for a different reason: it has no DNS representation at all. It is served by the app itself (app/sites/[name]/page.jsx issues a redirect), not by DNS, so mixing it with a DNS record type doesn't mean anything.

URL redirects

A URL record has no DNS representation. The wildcard *.runs-on.dev record already routes every claimed name to the app, so when a record's records object holds only URL, the site issues a 307 redirect to that URL instead of rendering the profile card. lib/dns.js's planDnsChanges plans no DNS change for it.

Because this makes a runs-on.dev name an open redirector for whatever URL is in the file, the target must be an absolute http:// or https:// URL. javascript:, data:, vbscript:, and protocol-relative (//evil.com) values are all rejected, checked both in CI and again at render time.

subdomains

An optional object, keyed by label, letting an owner set records at a subdomain of their claimed name, for example _atproto.you for a Bluesky handle or _discord.you for Discord verification.

domains/you.json

"subdomains": {
  "_atproto": { "TXT": ["did=did:plc:abc123"] }
}
  • At most 10 entries.
  • Each label matches the same grammar as name, plus one optional leading underscore, and no dots: a subdomain is exactly one label deep.
  • Each value holds A, TXT, CNAME, or MX under the same coexistence rules as the root records object. URL is not allowed on a subdomain, since the app only ever looks up the claimed name itself and could never serve a redirect record living underneath it.
  • The resulting full name (<label>.<name>.runs-on.dev) must stay within the 253-character DNS name limit.

How a record reaches DNS

Merging a pull request that changes domains/<name>.json triggers a workflow that runs scripts/sync-dns.mjs. It computes the desired DNS records from your file via planDnsChanges, deletes whatever was synced for that name before, and creates the new set through Vercel's domains API. Removing your record instead of editing it clears any DNS it had created, the same way.

You never touch DNS directly, and claiming itself needs no DNS write at all, since *.runs-on.dev is a wildcard record that already resolves every name.