> ## Documentation Index
> Fetch the complete documentation index at: https://docs.namespace.ninja/llms.txt
> Use this file to discover all available pages before exploring further.

# Universal usernames

> Connect ENS to your existing usernames.

export const FlowExplainer = () => {
  const BEAT_MS = 2400;
  const ROUTES = [{
    id: 'ens',
    label: '.eth name',
    name: 'happy.brand.eth',
    note: 'brand.eth registered in ENS'
  }, {
    id: 'dns',
    label: 'DNS name',
    name: 'happy.brand.id',
    note: 'brand.id imported into ENS with DNSSEC'
  }];
  const STORES = [{
    id: 'onchain',
    label: 'Onchain',
    node: 'chain',
    note: 'one RPC call to your registry contract',
    lookup: 'The gateway makes one RPC call and reads the record from your registry contract.',
    back: 'The contract returns the record. Nothing else is queried.',
    out: {
      d: 'M662,144 C700,140 712,73 750,73',
      nx: 706,
      ny: 100,
      chip: {
        text: 'eth_call · recordOf(node)',
        x: 590,
        y: 112
      }
    },
    ret: {
      d: 'M750,92 C712,92 700,160 664,164',
      nx: 718,
      ny: 134,
      chip: {
        text: 'addr(60) 0x1a9C…4b2f · avatar',
        x: 600,
        y: 112
      }
    }
  }, {
    id: 'offchain',
    label: 'Offchain',
    node: 'api',
    note: 'one HTTPS request to your API',
    lookup: 'The gateway makes one HTTPS request and reads the record from the system you already run.',
    back: 'Your API returns the record. Nothing else is queried.',
    out: {
      d: 'M662,172 C700,176 712,243 750,243',
      nx: 706,
      ny: 218,
      chip: {
        text: 'GET /records/happy',
        x: 590,
        y: 206
      }
    },
    ret: {
      d: 'M750,224 C712,224 700,180 664,176',
      nx: 718,
      ny: 196,
      chip: {
        text: 'addr(60) 0x1a9C…4b2f · avatar',
        x: 600,
        y: 206
      }
    }
  }];
  const [beat, setBeat] = useState(0);
  const [paused, setPaused] = useState(false);
  const [routeId, setRouteId] = useState('ens');
  const [storeId, setStoreId] = useState('onchain');
  const route = ROUTES.find(r => r.id === routeId);
  const store = STORES.find(s => s.id === storeId);
  const NAME = route.name;
  const NODES = [{
    id: 'wallet',
    tone: 'wallet',
    x: 24,
    y: 130,
    w: 116,
    h: 56,
    label: 'Wallet / App'
  }, {
    id: 'resolver',
    tone: 'eth',
    x: 250,
    y: 130,
    w: 160,
    h: 56,
    label: 'ENS Resolver',
    sub: 'CCIP-Read'
  }, {
    id: 'gateway',
    tone: 'gw',
    x: 526,
    y: 130,
    w: 136,
    h: 56,
    label: 'CCIP Gateway'
  }, {
    id: 'chain',
    tone: 'chain',
    x: 754,
    y: 46,
    w: 176,
    h: 54,
    label: 'Onchain records',
    sub: 'your registry contract'
  }, {
    id: 'api',
    tone: 'api',
    x: 754,
    y: 216,
    w: 176,
    h: 54,
    label: 'Offchain records',
    sub: 'your API or database'
  }];
  const EDGES = [{
    id: 'query',
    beat: 0,
    num: '1',
    nx: 193,
    ny: 140,
    d: 'M140,148 L246,148',
    chip: {
      text: `resolve("${NAME}")`,
      x: 193,
      y: 66
    }
  }, {
    id: 'ccip',
    beat: 1,
    num: '2',
    nx: 466,
    ny: 140,
    d: 'M410,148 L522,148',
    chip: {
      text: 'GET /lookup/{sender}/{data}.json',
      x: 466,
      y: 84
    }
  }, {
    id: 'lookup',
    beat: 2,
    num: '3',
    ...store.out
  }, {
    id: 'return',
    beat: 3,
    num: '4',
    ...store.ret
  }, {
    id: 'signed',
    beat: 4,
    num: '5',
    nx: 466,
    ny: 186,
    d: 'M522,170 L412,170',
    chip: {
      text: '200 OK · sig 0x1c8f…9d02',
      x: 466,
      y: 216
    }
  }, {
    id: 'records',
    beat: 5,
    num: '6',
    nx: 194,
    ny: 186,
    d: 'M246,170 L142,170',
    chip: {
      text: 'addr(60) 0x1a9C…4b2f',
      x: 194,
      y: 216
    }
  }];
  const CHIP_H = 19;
  const chipW = text => text.length * 5.75 + 16;
  const BEATS = [{
    active: ['wallet', 'resolver'],
    caption: `A wallet asks Ethereum to resolve ${NAME}.`
  }, {
    active: ['resolver', 'gateway'],
    caption: 'The resolver stores no answer. It reverts with a gateway URL — that is CCIP-Read.'
  }, {
    active: ['gateway', store.node],
    caption: store.lookup
  }, {
    active: [store.node, 'gateway'],
    caption: store.back
  }, {
    active: ['gateway', 'resolver'],
    caption: 'The gateway signs the response so nothing can be swapped in transit.'
  }, {
    active: ['resolver', 'wallet'],
    caption: 'The resolver verifies that signature on Ethereum, and the wallet gets its records.'
  }];
  useEffect(() => {
    if (paused) return;
    const id = setTimeout(() => setBeat(b => (b + 1) % BEATS.length), BEAT_MS);
    return () => clearTimeout(id);
  }, [paused, beat]);
  const go = delta => setBeat(b => (b + delta + BEATS.length) % BEATS.length);
  const step = BEATS[beat];
  const isOn = id => step.active.includes(id);
  return <div className="fx" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
      <div className="fx-controls">
        <div aria-label="Parent name" className="fx-toggle" role="group">
          <span className="fx-toggle-label">Parent name</span>
          <div className="fx-toggle-row">
            {ROUTES.map(r => <button aria-pressed={r.id === routeId} className="fx-btn" data-on={r.id === routeId} key={r.id} onClick={() => setRouteId(r.id)} type="button">
                {r.label}
              </button>)}
          </div>
          <span className="fx-toggle-note">{route.note}</span>
        </div>
        <div aria-label="Where the records live" className="fx-toggle" data-align="end" role="group">
          <span className="fx-toggle-label">Records</span>
          <div className="fx-toggle-row">
            {STORES.map(s => <button aria-pressed={s.id === storeId} className="fx-btn" data-on={s.id === storeId} key={s.id} onClick={() => setStoreId(s.id)} type="button">
                {s.label}
              </button>)}
          </div>
          <span className="fx-toggle-note">{store.note}</span>
        </div>
      </div>

      <div className="fx-scroll">
        <svg aria-label={`A wallet resolves ${NAME}. The ENS resolver reverts with a gateway URL, the gateway reads the record from ${storeId === 'onchain' ? 'a registry contract on chain' : 'your own API'}, signs the response, and the resolver returns the record to the wallet.`} className="fx-svg" role="img" viewBox="0 0 964 316">
          <defs>
            <marker id="fx-head" markerHeight="6" markerWidth="6" orient="auto" refX="5" refY="3">
              <path className="fx-arrow" d="M0,0 L6,3 L0,6 Z" />
            </marker>
            <marker id="fx-head-on" markerHeight="6" markerWidth="6" orient="auto" refX="5" refY="3">
              <path className="fx-arrow-on" d="M0,0 L6,3 L0,6 Z" />
            </marker>
          </defs>

          {}
          <g className="fx-group">
            <rect height="108" rx="10" width="148" x="8" y="104" />
            <text x="8" y="96">Client interface</text>
            <rect height="108" rx="10" width="188" x="236" y="104" />
            <text x="236" y="96">Naming layer</text>
            <rect height="284" rx="14" width="456" x="500" y="16" />
            <text x="500" y="8">Resolution infrastructure</text>
          </g>

          {EDGES.map(e => <path className="fx-edge" d={e.d} data-on={e.beat === beat} key={e.id} markerEnd={e.beat === beat ? 'url(#fx-head-on)' : 'url(#fx-head)'} />)}

          {EDGES.filter(e => e.num).map(e => <text className="fx-edge-num" data-on={e.beat === beat} key={`n-${e.id}`} x={e.nx} y={e.ny}>
              {e.num}
            </text>)}

          {EDGES.filter(e => e.beat === beat && e.chip).map(e => {
    const w = chipW(e.chip.text);
    return <g className="fx-chip" key={`${beat}-c-${e.id}`}>
                <rect height={CHIP_H} rx="6" width={w} x={e.chip.x - w / 2} y={e.chip.y - CHIP_H / 2} />
                <text x={e.chip.x} y={e.chip.y + 3.5}>{e.chip.text}</text>
              </g>;
  })}

          {EDGES.filter(e => e.beat === beat).map(e => <circle className="fx-packet" key={`${beat}-${e.id}`} r="4.5" style={{
    offsetPath: `path("${e.d}")`,
    animationDelay: `${e.delay ?? 0}ms`
  }} />)}

          {NODES.map(n => <g className="fx-node" data-muted={n.id === 'chain' || n.id === 'api' ? n.id !== store.node : undefined} data-on={isOn(n.id)} data-tone={n.tone} key={n.id}>
              <rect height={n.h} rx="12" width={n.w} x={n.x} y={n.y} />
              <text className="fx-node-label" x={n.x + n.w / 2} y={n.y + (n.sub ? n.h / 2 - 2 : n.h / 2 + 5)}>
                {n.label}
              </text>
              {n.sub ? <text className="fx-node-sub" x={n.x + n.w / 2} y={n.y + n.h / 2 + 14}>
                  {n.sub}
                </text> : null}
            </g>)}
        </svg>
      </div>

      <div className="fx-footer">
        <p className="fx-caption" key={`${routeId}-${storeId}-${beat}`}>
          <span className="fx-step">{beat + 1}</span>
          {step.caption}
        </p>
        <div className="fx-nav">
          <button aria-label="Previous step" className="fx-navbtn" onClick={() => go(-1)} type="button">
            ←
          </button>
          <span className="fx-count">
            {beat + 1} / {BEATS.length}
          </span>
          <button aria-label="Next step" className="fx-navbtn" onClick={() => go(1)} type="button">
            →
          </button>
        </div>
      </div>
    </div>;
};

## Two ways teams create usernames today

Almost every product that ships usernames picks one of two approaches. Both leave the user stuck at the same place.

**Plain usernames:** usually displayed like `@alice` or `alice`. Fast to build, and inside your app they work exactly as intended. But the name only exists in your database. Send `@alice` to someone in another wallet or app, and it simply won't work. Users are back to copying and pasting an address.

**A custom TLD:** something like `alice.wallet` or `alice.pay`, whatever reads well next to your brand name. This looks like the better option but it's not. Your TLD isn't in DNS and isn't in ENS, so no wallet outside your product has any way to look it up. Making it work means asking every wallet, explorer, and app to integrate your naming system one at a time. Also, collisions are the other half of the problem: multiple projects have built competing versions of the same TLD, so `alice.wallet` can resolve to two different people depending on which app you ask.

## A better way with ENS

Every username you've already issued gets a matching ENS name. One rule, applied across your whole user base:

```
Your username:   happy.brand
ENS name:        happy.brand.eth
        or       happy.brand.id, using a domain you already own
```

Your system stays the source of truth and no data moves. The username just gains a form that ENS-aware wallets already know how to read.

<Note>
  This does not turn `.brand` into a new ENS top-level domain. Each user gets an ENS name for the identity they already have.
</Note>

## ENS isn't limited to .eth or Ethereum

Two (wrong) assumptions people usually make.

**You don't need a `.eth` name.**

`happy.brand.eth` and `happy.brand.id` resolve identically. The first means registering `brand.eth`. The second means taking a domain you already own, turning on DNSSEC, and [importing it into ENS](https://docs.ens.domains/learn/dns/). Same result, so pick on branding.

**ENS isn't only for Ethereum.**

One name holds a separate address record for every chain, and they don't have to be EVM chains. Bitcoin, Solana, Base, Arbitrum, and Ethereum sit side by side on the same name, along with profile fields like display name and avatar. Your user has one name and it works everywhere they hold funds.

## Follow one lookup

The diagram is a single name being looked up, start to finish. Its two toggles are the decisions above: the parent name, and where records are read from. Neither changes the path.

It loops on its own. Hover to pause, or use the dots to step through it.

<FlowExplainer />

Nothing there asked you to move your users, usernames, or records. The wallet asked a question, your system answered, the wallet showed the result.

## Your records stay where they are

Records can live in the database you run today, on an L2, or on another chain. No second registry on Ethereum, nothing to keep in sync. Whatever answers the question inside your product answers it here too.

## Decide before you build

1. **The naming rule.** A parent name and a predictable pattern, such as `happy.brand` becoming `happy.brand.eth`. Nobody should have to learn a second identity.
2. **Where the answer comes from.** Your database, a contract in your existing registry, or a proof Ethereum can verify.
3. **Who runs it.** Someone holds the parent name, keeps the lookup path online, and fixes it when it breaks.

## What this does not do

* Your existing TLD does not become part of ENS. Users gain an ENS name; `.brand` stays yours.
* Support varies. Most major wallets resolve these names, but not every app does, so test the ones your users actually use.
* Anything served this way is public. Addresses, avatars, and profile fields are readable by anyone who asks.

## Next steps

<CardGroup cols={3}>
  <Card title="Learn about subnames" icon="tag" href="/overview/subnames">
    Compare offchain and onchain names before you pick how to issue them.
  </Card>

  <Card title="Issue gasless subnames" icon="cloud" href="/developer-guide/guide/create-offchain-subnames">
    Create names and manage their records with the Offchain Manager SDK.
  </Card>

  <Card title="Talk to us" icon="compass-drafting" href="/user-guide/custom-solutions">
    Bring your existing naming service and we will map it onto ENS with you.
  </Card>
</CardGroup>
