Hey everyone,
Over the last week, we have been building ensforge: an open-source TypeScript SDK for reading, writing, and building applications on ENS.
It started as an internal SDK. We were implementing the same protocol routing, resolver permissions, batching, and transaction workflows in multiple places. ENSv2 made that duplication harder to maintain, so we moved the shared behavior into one library and then open-sourced it.
GitHub: thenamespace/ensforge
Docs: ensforge.com
TL;DR
ensforge provides:
- One action per intent:
getOwner,setText, orrenewName, rather than separate V1 and V2 APIs. - Automatic routing based on the nameβs protocol, migration state, resolver, and authorization.
- A normal Promise API and a typed Effect API backed by the same implementation.
- Composable read batching that separates Multicall-compatible reads from CCIP Read and other execution paths.
- Wallet-aware writes with simulation, call batching when supported, sequential fallback, and resumable multi-step flows.
- Onchain and indexed reads through the same Core, SDK, and React layers.
- React hooks powered by Effect Atom.
- Versioned contracts, deployed addresses, and tree-shakable ABI fragments.
The packages are currently published as 0.3.1 under Apache 2.0.
The problems we were running into
An ENS integration often starts with an address or avatar read. A complete application quickly runs into:
- Protocol routing: Is the name on V1 or V2? Is it migrated or reserved? Which registry, registrar, resolver, wrapper, or renewer should be called?
- Different ownership models: βOwnerβ may mean a registrant, registry owner, or wrapped owner depending on the name.
- Resolver authorization: A write may require resolver discovery, interface checks, ownership checks, roles, or delegated permissions.
- Different read paths: Some reads can use Multicall, some require CCIP Read, and some need a discovery call first.
- Multi-step writes: Registration and migration need several transactions and should resume safely after an interruption.
- Wallet differences: Some wallets support batched calls while others need sequential execution.
We did not want each Namespace application to implement these rules independently.
What ensforge does differently
One action per user intent
const owner = await getOwner(config, { name: "envoy1084.eth" });
const text = await getText(config, { name: "envoy1084.eth", key: "url" });
await renewName(config, { name: "envoy1084.eth", duration });
The action normalizes the name, discovers its state, and chooses the supported route. getNameState still exposes the complete discriminated V1 or V2 state when an application needs the details.
Promise and Effect from one implementation
Every action is implemented as an Effect and exposed through a Promise interface:
const owner = await getOwner(config, { name: "envoy1084.eth" });
const ownerEffect = getOwner.effect(config, { name: "envoy1084.eth" });
Promise users get a familiar TypeScript SDK. Effect users keep typed failures, retries, concurrency, interruption, tracing, and composition. Both interfaces run the same implementation.
Composable batching
Read actions can describe requests without executing them immediately:
const name = "envoy1084.eth";
const profile = await readBatch(config, {
owner: getOwner.request({ name }),
resolver: getResolver.request({ name }),
avatar: getAvatar.request({ name }),
});
ensforge groups compatible reads, uses Multicall where safe, and leaves CCIP Read or discovery-dependent requests on their correct paths.
Wallet-aware write plans
Write actions can:
- Resolve the correct contract and authorization path.
- Simulate before sending.
- Use wallet call batching when supported.
- Fall back to safe sequential execution when appropriate.
- Return resumable progress for registration, migration, and other multi-call workflows.
How is this different from ENSJS?
ENSJS is the established ENS JavaScript library and remains an important reference for this project. It already provides typed viem-based actions, batching, tree shaking, subgraph reads, hooks, and purpose-built record writes.
ensforge is not intended to be an official replacement or a drop-in rewrite. It explores a different application architecture:
| Area | ENSJS | ensforge |
|---|---|---|
| Execution model | Promise/client actions | Effect implementations with Promise and Effect interfaces |
| Protocol API | ENS client methods, including ENSv2-ready resolution | Intent-level actions that expose interpreted V1/V2 and migration state |
| Read batching | Batchable actions and aggregate reads | Typed .request composition with execution-path planning |
| Writes | Purpose-built ENS write helpers | Authorization discovery, simulation, wallet capabilities, write plans, and resumable progress |
| React | ENS hooks | Effect Atom hooks using the same SDK action and error model |
| Indexed data | Subgraph methods on the ENS client | Normalized V1/V2 indexer actions across Core, SDK, and React |
Package structure
@ensforge/core: standalone actions, utilities, batching, writes, and indexer actions.@ensforge/sdk: one configured client with methods grouped by ENS capability.@ensforge/react: Effect Atom queries, mutations, Suspense, prefetching, and invalidation.@ensforge/contracts: V1/V2 contracts, deployment metadata, resolver profiles, complete ABIs, and focused fragments.
ensforge uses viem for Ethereum interaction and can consume either viem clients or an existing Wagmi configuration. It does not introduce another transport or wallet abstraction.
Current status
ensforge is still early software.
- Current focus: ENSv1 Mainnet and the deployed ENSv2 Sepolia beta.
- Tests: unit coverage, a deterministic Anvil deployment, seeded V1/V2 integration cases, and live Mainnet/Sepolia smoke suites.
- Deployment artifacts are the source of truth for supported contracts and ABIs.
- The docs include interactive Core, SDK, and React examples on Mainnet and Sepolia.
- ENSv2 deployments and the Effect 4 integration may continue to evolve.
Try it
npm install @ensforge/sdk effect@rc viem
# or
pnpm add @ensforge/sdk effect@rc viem
# or
yarn add @ensforge/sdk effect@rc viem
# or
bun add @ensforge/sdk effect@rc viem
import { Ensforge } from "@ensforge/sdk";
const sdk = new Ensforge({ network: "mainnet", publicClient });
const state = await sdk.name.getNameState({ name: "ens.eth" });
const avatar = await sdk.records.getAvatar({ name: "ens.eth" });
Feedback
I would especially like feedback on:
- Does one intent-level API across ENSv1 and ENSv2 match how applications want to integrate ENS?
- Are any important protocol details lost by the routing layer?
- Is the Promise/Effect dual API useful?
- Which read or write workflows are still missing?
Source: github.com/thenamespace/ensforge
Docs and interactive examples: ensforge.com