Skip to main content
Subdomains allow ENS name owners to create hierarchical naming structures under their domains. For example, if you own example.eth, you can create app.example.eth, blog.example.eth, etc.

Overview

Subdomains in ENS:
  • Are fully independent names with their own owners and resolvers
  • Can be created by the parent domain owner
  • Support unlimited nesting (e.g., sub.sub.example.eth)
  • Can have different owners than the parent domain
  • Can be wrapped, transferred, and managed independently

Prerequisites

To create subdomains, you must:
  • Own the parent ENS name
  • Have the parent name’s resolver set (optional but recommended)
  • Have ETH for gas fees

Creating Subdomains

Using ENS Registry

The most basic way to create a subdomain is through the ENS registry:
1

Calculate the Parent Node

import { namehash, labelhash } from 'viem'

// Parent domain
const parentName = 'example.eth'
const parentNode = namehash(parentName)

// Subdomain label
const label = 'app'
const labelHash = labelhash(label)
2

Create the Subdomain

Use setSubnodeOwner to create a subdomain:
const ensRegistry = await viem.getContractAt(
  'ENSRegistry',
  ENS_REGISTRY_ADDRESS
)

// Create app.example.eth owned by subdomainOwner
const tx = await ensRegistry.write.setSubnodeOwner([
  parentNode,           // Parent node (example.eth)
  labelHash,            // Label hash (app)
  subdomainOwnerAddress // Who will own the subdomain
])

await tx.wait()
console.log('Subdomain created: app.example.eth')
Only the owner of the parent node can create subdomains.
3

Set the Resolver

Set a resolver for the subdomain so it can store records:
const subdomainNode = namehash('app.example.eth')

const tx = await ensRegistry.write.setResolver([
  subdomainNode,
  publicResolverAddress
])

await tx.wait()
4

Configure Subdomain Records

Set address and other records in the resolver:
const resolver = await viem.getContractAt(
  'PublicResolver',
  publicResolverAddress
)

// Set ETH address
await resolver.write.setAddr([
  subdomainNode,
  subdomainAddress
])

// Set text records
await resolver.write.setText([
  subdomainNode,
  'url',
  'https://app.example.com'
])

console.log('Subdomain configured!')

Create Subdomain with Record in One Transaction

You can create a subdomain and set its resolver and owner in a single transaction:
import { namehash, labelhash } from 'viem'

const parentNode = namehash('example.eth')
const label = 'app'
const labelHash = labelhash(label)

const ensRegistry = await viem.getContractAt(
  'ENSRegistry',
  ENS_REGISTRY_ADDRESS
)

// Create subdomain with owner, resolver, and TTL in one call
const tx = await ensRegistry.write.setSubnodeRecord([
  parentNode,              // Parent node
  labelHash,               // Label hash
  subdomainOwnerAddress,   // Owner
  publicResolverAddress,   // Resolver
  0                        // TTL (0 = use default)
])

await tx.wait()
console.log('Subdomain created with resolver set!')
Using setSubnodeRecord is more gas-efficient than calling setSubnodeOwner and setResolver separately.

Subdomain Ownership Patterns

Pattern 1: Independent Ownership

Give complete control to the subdomain owner:
// Create subdomain owned by different address
await ensRegistry.write.setSubnodeOwner([
  parentNode,
  labelHash,
  independentOwnerAddress
])

// The subdomain owner now has full control
// Parent owner cannot modify unless ownership is transferred back
Once you transfer subdomain ownership, you cannot modify it unless the new owner transfers it back or you reclaim it using the parent domain’s authority.

Pattern 2: Retained Control

Keep subdomains owned by the parent owner:
// Create subdomain owned by parent owner
await ensRegistry.write.setSubnodeOwner([
  parentNode,
  labelHash,
  parentOwnerAddress // Same as parent owner
])

// Set resolver to point to different address
const resolver = await viem.getContractAt(
  'PublicResolver',
  publicResolverAddress
)

await resolver.write.setAddr([
  subdomainNode,
  targetAddress // Different from owner
])

Pattern 3: Temporary Delegation

Grant temporary control that can be revoked:
// Option 1: Use approval system
await ensRegistry.write.setApprovalForAll([
  delegateAddress,
  true
])
// Delegate can now manage all your names

// Option 2: Keep ownership, only set resolver records
// Owner keeps subdomain, delegate gets resolver approval
await resolver.write.setApprovalForAll([
  delegateAddress,
  true
])
Source: ~/workspace/source/contracts/resolvers/PublicResolver.sol:77-85

Bulk Subdomain Creation

Create multiple subdomains efficiently:
async function createSubdomains(parentName, labels, owner) {
  const parentNode = namehash(parentName)
  const ensRegistry = await viem.getContractAt(
    'ENSRegistry',
    ENS_REGISTRY_ADDRESS
  )

  // Create all subdomains
  const txs = await Promise.all(
    labels.map(label => 
      ensRegistry.write.setSubnodeRecord([
        parentNode,
        labelhash(label),
        owner,
        publicResolverAddress,
        0
      ])
    )
  )

  // Wait for all transactions
  await Promise.all(txs.map(tx => tx.wait()))
  
  console.log(`Created ${labels.length} subdomains`)
  return txs
}

// Usage
await createSubdomains(
  'example.eth',
  ['app', 'api', 'blog', 'docs', 'mail'],
  ownerAddress
)
Consider batching operations or using multicall patterns to save on gas costs when creating many subdomains.

Configuring Subdomain Records

Set Multiple Records

Use multicall to set multiple records in one transaction:
import { encodeFunctionData } from 'viem'

const subdomainNode = namehash('app.example.eth')
const resolver = await viem.getContractAt(
  'PublicResolver',
  publicResolverAddress
)

// Prepare multiple calls
const calls = [
  encodeFunctionData({
    abi: resolver.abi,
    functionName: 'setAddr',
    args: [subdomainNode, appAddress]
  }),
  encodeFunctionData({
    abi: resolver.abi,
    functionName: 'setText',
    args: [subdomainNode, 'url', 'https://app.example.com']
  }),
  encodeFunctionData({
    abi: resolver.abi,
    functionName: 'setText',
    args: [subdomainNode, 'description', 'Example application']
  }),
  encodeFunctionData({
    abi: resolver.abi,
    functionName: 'setAddr',
    args: [subdomainNode, 137, polygonAddress] // Polygon address
  })
]

// Execute all calls in one transaction
const tx = await resolver.write.multicallWithNodeCheck([
  subdomainNode,
  calls
])

await tx.wait()
console.log('All records set!')
Using multicall significantly reduces gas costs and ensures atomic updates when setting multiple records.

Subdomain Resolution

Resolving subdomains works exactly like resolving regular names:
import { namehash, zeroAddress } from 'viem'

async function resolveSubdomain(name) {
  const node = namehash(name)
  
  const ensRegistry = await viem.getContractAt(
    'ENSRegistry',
    ENS_REGISTRY_ADDRESS
  )
  
  // Get resolver
  const resolverAddress = await ensRegistry.read.resolver([node])
  
  if (resolverAddress === zeroAddress) {
    return { error: 'No resolver set' }
  }
  
  const resolver = await viem.getContractAt(
    'PublicResolver',
    resolverAddress
  )
  
  // Get address
  const address = await resolver.read.addr([node])
  
  return {
    name,
    node,
    resolver: resolverAddress,
    address
  }
}

// Usage
const result = await resolveSubdomain('app.example.eth')
console.log(result)

Reclaiming Subdomains

If you own a parent domain, you can always reclaim control of subdomains:
const parentNode = namehash('example.eth')
const labelHash = labelhash('app')

// Reclaim subdomain ownership
const tx = await ensRegistry.write.setSubnodeOwner([
  parentNode,
  labelHash,
  newOwnerAddress // Can be yourself to reclaim
])

await tx.wait()
Reclaiming a subdomain will override the current owner. This can break existing applications if the subdomain was being used independently. Only reclaim subdomains when necessary.

Wildcard Resolution

ENS supports wildcard resolution for subdomains that don’t exist:
// If you have a custom resolver implementing ENSIP-10 wildcard resolution
// Queries to non-existent subdomains can be handled by the parent's resolver

// Example: *.example.eth could all resolve to the same address
// This requires a custom resolver implementation
Wildcard resolution requires a custom resolver that implements ENSIP-10. The standard PublicResolver doesn’t support wildcards.

Subdomain Authorization

Control who can modify subdomain records:
const resolver = await viem.getContractAt(
  'PublicResolver',
  publicResolverAddress
)

// Approve someone for a specific subdomain
const subdomainNode = namehash('app.example.eth')

await resolver.write.approve([
  subdomainNode,
  delegateAddress,
  true // approved
])

// Check if approved
const isApproved = await resolver.read.isApprovedFor([
  ownerAddress,
  subdomainNode,
  delegateAddress
])
Source: ~/workspace/source/contracts/resolvers/PublicResolver.sol:96-110

Use Cases

Application Subdomains

// Main site: example.eth -> https://example.com
// App: app.example.eth -> 0x1234... (smart contract)
// Blog: blog.example.eth -> IPFS hash
// API: api.example.eth -> https://api.example.com

User Subdomains

// Give each user their own subdomain
// alice.users.example.eth
// bob.users.example.eth

async function createUserSubdomain(username, userAddress) {
  const parentNode = namehash('users.example.eth')
  const labelHash = labelhash(username)
  
  await ensRegistry.write.setSubnodeRecord([
    parentNode,
    labelHash,
    userAddress,
    publicResolverAddress,
    0
  ])
  
  const userNode = namehash(`${username}.users.example.eth`)
  const resolver = await viem.getContractAt(
    'PublicResolver',
    publicResolverAddress
  )
  
  await resolver.write.setAddr([userNode, userAddress])
  
  console.log(`Created ${username}.users.example.eth`)
}

Service Endpoints

// Regional endpoints
// us.api.example.eth
// eu.api.example.eth
// asia.api.example.eth

// Version endpoints  
// v1.api.example.eth
// v2.api.example.eth

Best Practices

Create a consistent subdomain structure for your application. For example, use app. for applications, www. for websites, and api. for APIs.
Be careful when transferring subdomain ownership. Once transferred, you cannot modify the subdomain unless you reclaim it (which may break existing uses) or the new owner transfers it back.
Set TTL values appropriately. A TTL of 0 means records should not be cached. Higher values improve performance but reduce flexibility.

Subdomain Management Contract

For complex subdomain management, consider creating a custom controller:
contract SubdomainManager {
    ENS public ens;
    bytes32 public parentNode;
    address public owner;
    
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }
    
    function createSubdomain(
        bytes32 label,
        address subdomainOwner,
        address resolver
    ) external onlyOwner {
        ens.setSubnodeRecord(
            parentNode,
            label,
            subdomainOwner,
            resolver,
            0
        );
    }
    
    function reclaimSubdomain(
        bytes32 label,
        address newOwner
    ) external onlyOwner {
        ens.setSubnodeOwner(parentNode, label, newOwner);
    }
}

Subdomain Expiration

Subdomains don’t have independent expiration dates:
  • Subdomains exist as long as the parent name is registered
  • If the parent name expires, all subdomains become unavailable
  • When the parent is renewed, subdomains become available again
  • Subdomain records are preserved even if the parent expires
If your parent domain expires and someone else registers it, they gain control of all subdomains. Always renew parent domains before expiration.

Next Steps

Build docs developers (and LLMs) love