Skip to main content

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.
  • Sui CLI installed (for onchain resolution examples)
  • Node.js 18+ with @mysten/sui installed: 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.sui instead of 0xfe9c7a... 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.sui resolves to 0x2. 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, 0x2 resolves to example.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.sui might point to 0x2, making 0x2 the target address for example.sui. Multiple names can point to the same target address.
  • Default address: The SuiNS name that the owner of a wallet address has designated to represent that address. For example, if you own example.sui and its target address is your wallet, you can set example.sui as the default name for your wallet. The owner must sign a set-default transaction to establish this connection.

When you display names in your UI, use the default address (reverse lookup) rather than the target address. The default address is guaranteed onchain because the wallet owner explicitly signed a transaction to set it.

Default address reset behavior

The default name for a wallet address resets automatically any time the target address of that name changes. This prevents stale mappings where a name displays for an address it no longer points to.

For example, if alice.sui points to address 0xA and 0xA has set alice.sui as its default name:

  1. The NFT holder changes the target address of alice.sui from 0xA to 0xB.
  2. The default name for 0xA resets to empty. Reverse lookup for 0xA now returns null.
  3. 0xB does not automatically inherit the default. The owner of 0xB must sign a new set-default transaction to display alice.sui.

Account for this reset in your app logic. If a user's default name suddenly returns null, the target address of their name might have changed.

NFT mutability and ownership

A SuiNS NFT (SuinsRegistration) acts as a capability object, not an identity token. The NFT grants the holder permission to:

  • Change the target address the name points to
  • Create subnames (if applicable)
  • Renew the registration
  • Transfer the NFT to another address

Because the NFT is transferable, the holder of the NFT might not be the address that the name points to. The NFT can change hands at any time without affecting the current target address until the new holder explicitly updates it.

caution

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:

[dependencies]
suins = { git = "https://github.com/MystenLabs/suins-contracts/", subdir = "packages/suins", rev = "releases/mainnet/core/v3" }
caution

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 calling send_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, you have 2 options:

  • @mysten/sui convenience methods: The SuiClient provides resolveNameServiceAddress and resolveNameServiceNames methods for basic lookups.
  • @mysten/suins SDK extension: The SuiNS SDK adds higher-level methods like getNameRecord to any SuiClient through the $extend pattern. Use this when you need name record details beyond the target address (expiration, metadata, avatar).

Lookup with @mysten/sui

For GraphQL RPC, 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.

Lookup with the SuiNS SDK extension

The @mysten/suins package provides the suins() extension function. Attach it to any SuiClient through $extend to add SuiNS methods. The extension uses ClientWithCoreApi and PackageInfo internally, and automatically loads the correct constants for Mainnet and Testnet:

The getNameRecord method returns the full name record including expiration timestamp and metadata fields. Use this when you need to check expiry or retrieve associated data like avatars and content hashes.

Install the SuiNS SDK with:

$ npm i @mysten/suins

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:

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.sui on 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.

End-to-end onchain verification

When you resolve a SuiNS name to send assets or authorize actions, verify 4 conditions before using the result. Skipping any check can result in assets sent to the wrong address, expired names, or unresolvable targets.

Follow this sequence for every onchain resolution:

  1. Verify the name exists. Call lookup on the SuiNS registry. If the result is None, the name is not registered or has been released after expiry. Abort the transaction.
  2. Check expiration. Call has_expired with a Clock reference. Expired names remain in the registry but should not be trusted. Abort if expired.
  3. Confirm the target address is set. Call target_address on the name record. A name can exist without pointing to any address until the holder sets one. Abort if None.
  4. Match the expected address (optional). If you expect the name to resolve to a specific address (for example, a counterparty in a trade), compare the resolved target address to the expected value. This prevents a time-of-check to time-of-use (TOCTOU) issue where the NFT holder changes the target address between your lookup and the actual transfer.

The Move example in the onchain resolution section demonstrates steps 1 through 3. Step 4 depends on your app's requirements.

Target address snapshots

The target address returned by lookup is a snapshot of the current registry state at the time of the transaction. Between the time you read the target address and the time the transaction executes, the NFT holder could change the target. On Sui, this is safe within a single programmable transaction block because the registry read and the transfer happen atomically. If you separate the lookup and the action across multiple transactions, the target address might change between them.

Offchain verification

For offchain verification (for example, in a backend service), combine the SuiNS SDK getNameRecord method with expiry checks. The verification follows the same 3-step sequence as the onchain approach:

  1. Call getNameRecord and check for a null return (name does not exist).
  2. Compare expirationTimestampMs against the current time to confirm the name has not expired.
  3. Check that targetAddress is set and non-empty before using it.

Reverse lookup verification

When you display a name for an address in your UI, use reverse lookup (resolveNameServiceNames) rather than scanning all names that point to that address. The reverse lookup returns only the name that the address owner has explicitly set as their default through a signed transaction. This guarantees that the address owner consented to being identified by that name (SuiNS developer reference).

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:

CapabilityNode subnamesLeaf subnames
Has NFTYesNo, parent NFT acts as capability
Can create childrenYes, if parent allowsNo
ExpirationYes, parent-determined or extendableNo, tied to parent
Target addressNFT holder can set; can be emptyActive parent holder can set; cannot be empty
Reverse registryYesYes
Transfer ownershipYes, through NFTNo
RevokeNo (except post-expiration)Yes, parent holder can revoke

Choosing a subname type

Choose the subname type based on how you want to manage ownership and lifecycle:

  • Use leaf subnames when your app creates and manages subnames on behalf of users. Leaf subnames are lighter (no NFT overhead), the parent holder controls them fully, and you can revoke them at any time. This is the right choice for app-assigned identities.
  • Use node subnames when you want the subname holder to have independent control. Node subnames have their own NFT, so the holder can update the target address, create child subnames, and transfer ownership without the parent's involvement.

Creating subnames

You can create subnames through 3 methods:

Step 1:

Choose your creation method based on your use case:

  • SuiNS portal (suins.io): Create subnames manually through the web interface. Navigate to your domain, select the subnames tab, and create a new subname. You need to own the parent domain NFT in your connected wallet.
  • Enoki subname API: Create subnames programmatically through a REST API. Best for apps that assign subnames to users automatically. See the Enoki subname documentation for API details.
  • Onchain through Move: Create subnames directly in your smart contract using the SuiNS subnames package. Use this when your subname creation logic is part of an onchain workflow.
Step 2:

Configure the subname with the following parameters:

  • Name: The label for the subname (for example, alice for alice.myapp.sui). Labels follow the same character rules as SuiNS names: lowercase alphanumeric characters and hyphens, 3 to 63 characters.
  • Target address: The address the subname points to. Required for leaf subnames. Optional for node subnames (can be set later by the NFT holder).
  • Expiration (node subnames only): The timestamp when the node subname expires. Must not exceed the parent domain's expiration.
Step 3:

Set up resolution so the subname can be looked up. The subname is resolvable immediately after creation through onchain lookup and offchain resolveNameServiceAddress. If the target address owner wants the subname as their default name, they must sign a separate set-default transaction. Both node and leaf subnames support reverse lookup registration.

Subname configuration and parent rules

The parent domain holder configures rules that govern child subname behavior:

  • Allow child creation: The parent can enable or disable subname creation under their domain. If disabled, no new subnames can be created.
  • Allow expiration extension: For node subnames, the parent can permit the subname holder to extend the expiration up to the parent's own expiration date.
  • Revocation: The parent holder can revoke leaf subnames at any time. Node subnames cannot be revoked by the parent until they expire.
info

If the parent domain expires, all subnames under it stop resolving, regardless of whether they are node or leaf subnames. Renew the parent domain at suins.io before the 30-day grace period ends to avoid losing subname resolution.

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

ErrorCauseResolution
Name not found (null or ENameNotFound)Name not registered, or expired and releasedCheck the name at suins.io; renew if expired
Target address not set (ENameNotPointingToAddress)Name exists but holder has not set a target addressHolder must call set-target-address in the SuiNS portal
Name expired (ENameExpired)Storage epoch passed; name still in registry but resolves as expiredHolder must renew at suins.io
Wrong networkMainnet name queried on Testnet client or reverseMatch SuiClient URL to the network where the name is registered
Enoki subname creation fails (domain not LIVE)Domain linked but not published in Enoki PortalPublish the domain in the Enoki Portal before calling the API
Subname not resolving after creationEnoki subname is asynchronous; status is PENDINGPoll GET /v1/subnames until status is ACTIVE
Domain expired, subnames stop resolvingSuiNS domain past expiry or grace periodRenew 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.