Domain Names with SuiNS
By the end of this page, you can:
- Resolve a SuiNS name to an address onchain in Move and offchain through the Sui RPC.
- Implement reverse lookup to display human-readable names in your UI.
- Create and manage subnames under a domain you control.
- Integrate the Enoki subname API to provision per-user identities programmatically.
- Prerequisites
- Sui CLI installed (for onchain resolution examples)
- Node.js 18+ with
@mysten/suiinstalled:pnpm add @mysten/sui - A deployed Sui Move package if implementing onchain resolution
- A SuiNS domain if creating subnames (register at suins.io)
- An Enoki API key if using the Enoki subname API
This guide uses Sui Messenger as a concrete example throughout. Sui Messenger is a demo encrypted messaging app built on Sui that assigns every user a SuiNS subname under the sui-stack.sui parent domain. These subnames serve as human-readable identities in the app's channel system, replacing raw wallet addresses with names like alice.sui-stack.sui. The Onchain Websites with Walrus Sites guide uses SuiNS to attach a readable URL to a deployed Walrus Site.
Introduction to SuiNS
Sui Name Service (SuiNS) is a decentralized naming service on the Sui blockchain. You use SuiNS to replace complex wallet addresses with human-readable names ending in .sui, and to resolve names to addresses at runtime, both onchain in Move and offchain through RPCs. For the full developer reference including the SuiNS SDK, active package constants, and transaction patterns, see the SuiNS developer documentation.
SuiNS has 3 main use cases in app development:
- Address display: Show
alice.suiinstead of0xfe9c7a...in your UI. - Name-based transfers: Send assets to a name rather than a raw address. The SuiNS registry resolves the name to the correct target address at transaction time.
- App-specific subnames: Create subnames under a domain you control (for example,
alice.myapp.sui) to use as per-user or per-resource identifiers within your app.
Sui Messenger uses the third pattern. The app registers a subname for each user under sui-stack.sui, giving every participant a unique identity that the channel UI displays in place of their wallet address.
Resolution architecture
Resolution types
SuiNS supports 2 types of resolution:
- Lookup: A name resolves to an address. For example,
example.suiresolves to0x2. Use this when you want to send assets or look up what address a name points to. - Reverse lookup: An address resolves to a name. For example,
0x2resolves toexample.sui. Use this when you want to display a human-readable name for a known address.
Both resolution types are available onchain in Move and offchain through the Sui RPC.
Address types
Lookups work with 2 types of addresses:
- Target address: The address that a SuiNS name points to. The NFT holder sets this. For example,
example.suimight point to0x2, making0x2the target address forexample.sui. - Default address: The SuiNS name that the owner of a wallet address has designated to represent that address. For example, if you own
example.suiand its target address is your wallet, you can setexample.suias the default name for your wallet. The owner must sign a set-default transaction to establish this connection. The default address resets any time the target address changes.
Do not use SuiNS NFT ownership as a resolution method. A SuiNS NFT acts as a capability to change the target address, but it does not identify any specific address. Use the target address for lookup resolution and the default address for reverse lookup resolution.
Onchain resolution
Use the SuiNS core package to resolve names from within a Move module. Add the dependency to your Move.toml:
- Mainnet
- Testnet
[dependencies]
suins = { git = "https://github.com/mystenlabs/suins-contracts/", subdir = "packages/suins", rev = "releases/mainnet/core/v3" }
[dependencies]
suins = { git = "https://github.com/mystenlabs/suins-contracts/", subdir = "packages/suins", rev = "releases/testnet/core/v2" }
Use the core package only for onchain integration. The utility packages are subject to replacement and might break your logic if they change without a corresponding update to your code.
The following Move module demonstrates how to transfer an object to a SuiNS name. It looks up the name in the SuiNS registry, checks that the name exists and has not expired, retrieves its target address, and transfers the object:
module demo::demo {
use std::string::String;
use sui::clock::Clock;
use suins::{
suins::SuiNS,
registry::Registry,
domain
};
const ENameNotFound: u64 = 0;
const ENameNotPointingToAddress: u64 = 1;
const ENameExpired: u64 = 2;
public fun send_to_name<T: key + store>(
suins: &SuiNS,
obj: T,
name: String,
clock: &Clock
) {
let mut optional = suins.registry<Registry>().lookup(domain::new(name));
assert!(optional.is_some(), ENameNotFound);
let name_record = optional.extract();
assert!(!name_record.has_expired(clock), ENameExpired);
assert!(name_record.target_address().is_some(), ENameNotPointingToAddress);
transfer::public_transfer(obj, name_record.target_address().extract())
}
}
The lookup call takes a Domain value constructed from the name string. has_expired takes a Clock reference. target_address returns an Option<address> that you extract after verifying it is set.
The 3 error constants map to real scenarios you encounter in production:
ENameNotFound: the name does not exist in the registry, or the domain has expired and been released. Check that the name exists at suins.io before callingsend_to_name.ENameExpired: the name exists in the registry but its storage epoch has passed. The holder must renew it before it resolves again.ENameNotPointingToAddress: the name record exists and has not expired, but the holder has not set a target address. A name can exist without pointing anywhere until the NFT holder calls the set-target-address transaction.
Pass the SuiNS shared object as an argument to any function that performs onchain resolution. The object IDs for Mainnet and Testnet are listed in the SuiNS active constants.
Offchain resolution
For offchain resolution in a TypeScript or JavaScript app, use the Sui RPC endpoints. No additional package is required beyond @mysten/sui. For a higher-level TypeScript client that wraps these calls, see the SuiNS SDK.
Lookup (name to address) uses the suix_resolveNameServiceAddress JSON-RPC method:
import { SuiClient } from '@mysten/sui/client';
const client = new SuiClient({ url: 'https://fullnode.mainnet.sui.io:443' });
const address = await client.resolveNameServiceAddress({
name: 'example.sui',
});
// Returns: '0x2' or null if the name does not exist
Reverse lookup (address to default name) uses the suix_resolveNameServiceNames JSON-RPC method, or the defaultSuinsName field in GraphQL:
const names = await client.resolveNameServiceNames({
address: '0x2',
});
// Returns: { data: ['example.sui'], hasNextPage: false, nextCursor: null }
For GraphQL, use the resolveSuinsAddress query for lookup and the defaultSuinsName field on the Address type for reverse lookup. See the Sui GraphQL reference for the full schema.
In Sui Messenger, the useUserSubname hook resolves the subname for the connected wallet by querying the Enoki subname API rather than the SuiNS registry directly. This is because subnames under sui-stack.sui are provisioned programmatically through Enoki rather than registered by users through the SuiNS portal:
frontend/src/hooks/useUserSubname.ts. You probably need to run `pnpm prebuild` and restart the site.The hook queries https://api.enoki.mystenlabs.com/v1/subnames with the wallet address and parent domain. It returns the first matching subname for that address under sui-stack.sui. The staleTime of 5 minutes avoids redundant requests while keeping data reasonably fresh.
Enoki subname API vs the SuiNS SDK
The Enoki subname API is the right choice when your app provisions subnames on behalf of users rather than letting users register their own names. Enoki holds your SuiNS domain in a managed contract and handles subname creation, deletion, and renewal through a REST API. You authenticate with an Enoki API key and optionally a zkLogin JWT to associate the subname with the user's address automatically.
Use the Enoki subname API when:
- You want every authenticated user to receive a subname automatically (for example,
alice.myapp.suion first login). - Your subnames are app-controlled, not user-registered.
- You use zkLogin for authentication.
Use the SuiNS SDK or RPC directly when:
- Users register and own their own names through the SuiNS portal.
- You need to query or resolve existing names rather than provision new ones.
- You self-host a SuiNS indexer for bulk domain queries.
The key limitation of the Enoki approach is that each user gets at most 1 subname per domain when using a public API key with zkLogin. You need a private API key to specify an arbitrary target address or create multiple subnames per user. See the Enoki subname documentation for full API details.
Indexing
For queries beyond a single name or address lookup (for example, all subnames under a parent domain, or all names pointing to a given address), run your own instance of the suins-indexer. See the custom indexer documentation for setup instructions.
Subnames
Subnames are nested names under a parent name. For example, alice.myapp.sui is a subname under myapp.sui. Creating subnames has no cost. The maximum nesting depth is 8 levels (10 levels including the second-level domain (SLD) and top-level domain (TLD)).
Parent rules control whether children can be created and whether subnames can extend their expiration to match the parent.
Subname types
SuiNS has 2 subname types:
- Node subnames: Have an associated NFT (
SubDomainRegistration). The NFT holder can update the target address, create child subnames (if the parent permits), and transfer ownership. Node subnames have their own expiration, which the parent can allow extending. - Leaf subnames: Have no associated NFT. The parent's NFT holder controls the leaf's configuration. Leaf subnames do not expire independently; their lifetime matches the parent. The parent holder can revoke a leaf subname at any time.
The following table summarizes the key differences:
| Capability | Node subnames | Leaf subnames |
|---|---|---|
| Has NFT | Yes | No, parent NFT acts as capability |
| Can create children | Yes, if parent allows | No |
| Expiration | Yes, parent-determined or extendable | No, tied to parent |
| Target address | NFT holder can set; can be empty | Active parent holder can set; cannot be empty |
| Reverse registry | Yes | Yes |
| Transfer ownership | Yes, through NFT | No |
| Revoke | No (except post-expiration) | Yes, parent holder can revoke |
In Sui Messenger, each user receives a leaf subname under sui-stack.sui. Leaf subnames are the right choice here because the app manages them programmatically through Enoki. Users do not own or transfer their subnames. The Enoki API provisions a leaf subname for each address that authenticates with the app.
Sui Messenger: multi-service client with SuiNS subnames
The MessagingClientProvider in Sui Messenger composes SuiStackMessagingClient with SealClient and WalrusStorageAdapter into a single extended client. The useUserSubname hook fetches the subname for the connected wallet separately and displays it in the channel UI alongside messages:
import { SealClient } from '@mysten/seal';
import { SuiStackMessagingClient, WalrusStorageAdapter } from '@mysten/messaging';
const extendedClient = new SuiClient({ url: 'https://fullnode.testnet.sui.io:443' })
.$extend(
SealClient.asClientExtension({
serverConfigs: SEAL_SERVERS.map((id) => ({ objectId: id, weight: 1 })),
}),
)
.$extend(
SuiStackMessagingClient.experimental_asClientExtension({
storage: (client) =>
new WalrusStorageAdapter(client, {
publisher: 'https://publisher.walrus-testnet.walrus.space',
aggregator: 'https://aggregator.testnet.walrus.mirai.cloud',
epochs: 10,
}),
sessionKey,
}),
);
This pattern (composing Seal, Walrus, and Messaging onto a single SuiClient through $extend) is the standard way to build a multi-service Sui Stack app. Each extension adds its methods to the client without affecting the others. useUserSubname then resolves the wallet's subname independently and the UI renders it next to messages and channel entries, replacing raw addresses throughout the app.
For name registration, see suins.io. For the full developer reference including the SuiNS SDK and transaction patterns, see docs.suins.io.
Failure modes
| Error | Cause | Resolution |
|---|---|---|
Name not found (null or ENameNotFound) | Name not registered, or expired and released | Check the name at suins.io; renew if expired |
Target address not set (ENameNotPointingToAddress) | Name exists but holder has not set a target address | Holder must call set-target-address in the SuiNS portal |
Name expired (ENameExpired) | Storage epoch passed; name still in registry but resolves as expired | Holder must renew at suins.io |
| Wrong network | Mainnet name queried on Testnet client or reverse | Match SuiClient URL to the network where the name is registered |
Enoki subname creation fails (domain not LIVE) | Domain linked but not published in Enoki Portal | Publish the domain in the Enoki Portal before calling the API |
| Subname not resolving after creation | Enoki subname is asynchronous; status is PENDING | Poll GET /v1/subnames until status is ACTIVE |
| Domain expired, subnames stop resolving | SuiNS domain past expiry or grace period | Renew domain at suins.io before the 30-day grace period ends |
Troubleshooting
Name not found. lookup returns null or the Move ENameNotFound aborts. Check that the name exists and is spelled correctly at suins.io. Expired names return null even if they previously had registrations.
Target address not set. target_address returns None even though the name exists. The holder has not set a target address. In your UI, treat None as unresolvable and prompt the user to set a target address in the SuiNS portal.
Wrong network. resolveNameServiceAddress returns null on Mainnet for a name registered on Testnet, or the reverse. Confirm that the SuiClient URL matches the network where the name is registered.
Enoki subname creation fails. The API returns an error if the domain is not in LIVE status. Publish the domain in the Enoki Portal before calling the creation endpoint. If using a public API key with zkLogin, each user can only have 1 subname per domain. A second creation attempt returns an error.
Subname not resolving after creation. Enoki subname creation is asynchronous. The subname enters PENDING status and takes a few seconds to become ACTIVE and resolve onchain. Poll GET /v1/subnames until status is ACTIVE before assuming failure.
Subname creation blocked after domain expiry. If your SuiNS domain expires, Enoki cannot create or delete subnames and existing subnames stop resolving. Renew the domain at suins.io before the 30-day grace period ends.