Skip to content

[WIP] Split ENSApi config data model#1975

Draft
tk-o wants to merge 4 commits intomainfrom
refactor/1806-split-ensapi-config-data-model
Draft

[WIP] Split ENSApi config data model#1975
tk-o wants to merge 4 commits intomainfrom
refactor/1806-split-ensapi-config-data-model

Conversation

@tk-o
Copy link
Copy Markdown
Member

@tk-o tk-o commented Apr 21, 2026

Lite PR

Tip: Review docs on the ENSNode PR process

Summary

  • ENSApi Config is based purely on env vars
  • All dynamic config is based on the EnsNodeStackInfo object that is loaded from cache for every request. If the EnsNodeStackInfo object is not available, no relevant HTTP routes (the ones that rely on the EnsNodeStackInfo object) return 503.

Why

  • We wanted to simplify data model for handling the ENSApi Config.

Testing

  • How this was tested.
  • If you didn't test it, say why.

Notes for Reviewer (Optional)


Pre-Review Checklist (Blocking)

  • This PR does not introduce significant changes and is low-risk to review quickly.
  • Relevant changesets are included (or are not required)

Copilot AI review requested due to automatic review settings April 21, 2026 18:49
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 21, 2026

⚠️ No Changeset found

Latest commit: 1fecc67

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
admin.ensnode.io Ready Ready Preview, Comment Apr 21, 2026 6:49pm
ensnode.io Ready Ready Preview, Comment Apr 21, 2026 6:49pm
ensrainbow.io Ready Ready Preview, Comment Apr 21, 2026 6:49pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

This PR implements a split configuration design for ENSApi, separating environment-based config (static import) from ENSDb-derived config (async, middleware-shared). ensIndexerPublicConfig is removed from static EnsApiPublicConfig, fetched asynchronously from ENSDb, threaded through buildEnsNodeStackInfo, and propagated to handlers via request-scoped middleware context.

Changes

Cohort / File(s) Summary
Configuration refactoring
apps/ensapi/src/config/config.schema.ts, apps/ensapi/src/config/redact.ts, apps/ensapi/src/cache/stack-info.cache.ts
Removed ENSIndexer config fetching from schema; made buildEnsApiPublicConfig accept ensIndexerPublicConfig as explicit parameter; added DB fetch in stack-info loader; updated redaction to exclude namespace/indexer fields.
SDK type/serialization updates
packages/ensnode-sdk/src/ensapi/config/*, packages/ensnode-sdk/src/stack-info/*
Removed ensIndexerPublicConfig from EnsApiPublicConfig type and serialized forms; updated buildEnsNodeStackInfo to accept explicit ensIndexerPublicConfig and ensRainbowPublicConfig parameters; simplified serialize/deserialize logic.
Middleware introduction
apps/ensapi/src/handlers/api/router.ts, apps/ensapi/src/handlers/subgraph/subgraph-api.ts, apps/ensapi/src/middleware/stack-info.middleware.ts
Added stackInfoMiddleware application; introduced ensureEnsNodeStackInfoAvailable guard for type-safe stack-info assertion in handlers.
Handler/middleware updates
apps/ensapi/src/handlers/api/*.ts, apps/ensapi/src/middleware/*.ts
Updated handlers/middlewares to call ensureEnsNodeStackInfoAvailable and use c.var.stackInfo.ensIndexer instead of module-level config for plugin/namespace access.
Resolution library updates
apps/ensapi/src/lib/resolution/forward-resolution.ts, apps/ensapi/src/lib/resolution/reverse-resolution.ts, apps/ensapi/src/lib/resolution/multichain-primary-name-resolution.ts, apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts, apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts
Removed module-level config dependency; added namespace and plugins as explicit parameters threaded from stack info through resolution and acceleration helpers.
GraphQL/Omnigraph updates
apps/ensapi/src/omnigraph-api/yoga.ts, apps/ensapi/src/omnigraph-api/context.ts, apps/ensapi/src/omnigraph-api/builder.ts, apps/ensapi/src/omnigraph-api/schema/*.ts, apps/ensapi/src/omnigraph-api/lib/find-domains/*.ts
Changed from module-level yoga export to namespace-scoped createYogaForNamespace factory; updated GraphQL context to namespace-aware createYogaContextForNamespace; threaded namespace through domain/canonical-path resolvers.
Public client refactoring
apps/ensapi/src/lib/public-client.ts
Replaced getPublicClient(chainId) with buildPublicClientForRootChain(namespace), deriving root chain ID from namespace instead of static RPC configs.
Mock/test updates
apps/ensadmin/src/app/mock/..., apps/ensapi/src/config/config.schema.test.ts, apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.test.ts, packages/ensnode-sdk/src/ensapi/config/conversions.test.ts, packages/ensnode-sdk/src/ensnode/client.test.ts
Removed ensIndexerPublicConfig from mock serialized configs; removed test-local config mock where namespace was hardcoded; updated expected test outputs.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Router as API Router<br/>(middleware)
    participant StackMiddleware as Stack Info<br/>Middleware
    participant ENSDb as ENSDb Client
    participant Handler as Handler
    participant Config as Config

    Client->>Router: HTTP Request
    Router->>StackMiddleware: ensureEnsNodeStackInfoAvailable()
    alt Stack Info not cached
        StackMiddleware->>ENSDb: getEnsNodeStackInfo()
        ENSDb-->>StackMiddleware: {ensApi, ensDb, ensIndexer, ensRainbow}
        StackMiddleware->>StackMiddleware: Cache in c.var.stackInfo
    end
    StackMiddleware->>Handler: c.var.stackInfo available
    Handler->>Handler: Extract namespace & plugins<br/>from c.var.stackInfo.ensIndexer
    Handler->>Config: resolveForward(namespace, plugins, ...)
    Config->>Config: Compute using provided namespace
    Config-->>Handler: Result
    Handler-->>Client: Response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • ENSDb data model #1660: Introduces ENSDb-backed fetching of ensIndexerPublicConfig and client interfaces that this PR depends on for stack-info construction.
  • Integrate ENSDb SDK into ENSApi #1809: Adds foundational ENSDb integration and buildEnsNodeStackInfo wiring that this PR extends with explicit config parameter passing.
  • Feat/canonical name heuristic #1576: Introduces per-request GraphQL context and canonical-path dataloaders that this PR adapts to be namespace-aware via stack-info context.

Poem

🐰 A rabbit hops through configs old and new,
One static, one async—now split in two!
No more module-level globals holding fast,
Request-scoped context's here at last!
Stack info flows through middleware with grace, 🎀

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title '[WIP] Split ENSApi config data model' directly describes the main change: separating the ENSApi configuration into environment-based and dynamic stack-info-based parts.
Description check ✅ Passed The description follows the template with Summary, Why, Testing (incomplete), Notes, and Pre-Review Checklist sections, though Testing section lacks implementation details.
Linked Issues check ✅ Passed The PR successfully implements issue #1806 requirements: splits ENSApi config into env-based static config and ENSDb-derived dynamic config accessed via middleware context, ensuring 503 responses when unavailable.
Out of Scope Changes check ✅ Passed All changes align with the stated objective of splitting the ENSApi config model; no unrelated functionality modifications detected beyond the scope of issue #1806.
Docstring Coverage ✅ Passed Docstring coverage is 95.45% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/1806-split-ensapi-config-data-model
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch refactor/1806-split-ensapi-config-data-model

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@tk-o
Copy link
Copy Markdown
Member Author

tk-o commented Apr 21, 2026

@greptile review

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
apps/ensapi/src/lib/public-client.ts (1)

11-40: ⚠️ Potential issue | 🟠 Major

Perf regression: RPC configs are re-parsed on every call, defeating the cache.

buildRpcConfigsFromEnv(config, namespace) + RpcConfigsSchema.parse(...) now runs on every invocation, before the cache check. Since this is called on the request hot-path (e.g. forward-resolution.ts), every request pays a full Zod validation of the RPC config map even when the cached PublicClient exists. Previously the lookup was effectively O(1) for cache hits.

Also, the JSDoc is stale — it still says "Gets a viem#PublicClient for the specified chainId", but the parameter is now namespace.

♻️ Proposed fix: derive `rootChainId` cheaply, serve cached client fast, only parse on miss
-/**
- * Gets a viem#PublicClient for the specified `chainId` using the ENSApiConfig's RPCConfig. Caches
- * the instance itself to minimize unnecessary allocations.
- */
+/**
+ * Builds a viem#PublicClient for the ENS root chain of the given `namespace`, using the
+ * ENSApiConfig's RPCConfig. Caches the instance itself keyed by the derived root chain id
+ * to minimize unnecessary allocations and avoid re-validating RPC configs on every call.
+ */
 export function buildPublicClientForRootChain(namespace: ENSNamespaceId): PublicClient {
   const rootChainId = getENSRootChainId(namespace);
+
+  const cached = _cache.get(rootChainId);
+  if (cached) return cached;
+
   const unvalidatedRpcConfigs = buildRpcConfigsFromEnv(config, namespace);
   const rpcConfigs = RpcConfigsSchema.parse(unvalidatedRpcConfigs);
   const rpcConfig = rpcConfigs.get(rootChainId);
 
   if (!rpcConfig) {
     throw new Error(`Invariant: ENSApi does not have an RPC to chain id '${rootChainId}'.`);
   }
 
-  if (!_cache.has(rootChainId)) {
-    _cache.set(
-      rootChainId,
-      // Create an viem#PublicClient that uses a fallback() transport with all specified HTTP RPCs
-      createPublicClient({
-        transport: fallback(rpcConfig.httpRPCs.map((url) => http(url.toString()))),
-      }),
-    );
-  }
-
-  const publicClient = _cache.get(rootChainId);
-
-  // publicClient guaranteed to exist due to cache-setting logic above
-  if (!publicClient) throw new Error("never");
-
+  const publicClient = createPublicClient({
+    transport: fallback(rpcConfig.httpRPCs.map((url) => http(url.toString()))),
+  });
+  _cache.set(rootChainId, publicClient);
   return publicClient;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensapi/src/lib/public-client.ts` around lines 11 - 40, The function
buildPublicClientForRootChain currently parses RPC configs on every call causing
a perf regression; change the logic so you compute rootChainId using
getENSRootChainId(namespace) and check _cache.has(rootChainId) first, returning
the cached PublicClient if present, and only call buildRpcConfigsFromEnv(config,
namespace) and RpcConfigsSchema.parse(...) when the cache misses to construct
and set the client via createPublicClient/fallback/http as before; also update
the JSDoc to refer to the `namespace` parameter (not `chainId`) to keep comments
accurate.
apps/ensapi/src/config/config.schema.ts (2)

46-73: ⚠️ Potential issue | 🟡 Minor

Stale async + stale JSDoc on buildConfigFromEnvironment.

With the ensIndexerPublicConfig fetching removed, there are no awaits left in this function, yet it's still declared async and returns Promise<EnsApiConfig>. The JSDoc also still claims it's "fetching the EnsIndexerPublicConfig", which is no longer true. Either make it sync (preferable, since the whole point of this PR is that env-based config is statically importable) or keep it async but update the doc.

♻️ Proposed fix
 /**
- * Builds the EnsApiConfig from an EnsApiEnvironment object, fetching the EnsIndexerPublicConfig.
+ * Builds the EnsApiConfig from an EnsApiEnvironment object.
  *
  * `@returns` A validated EnsApiConfig object
  * `@throws` Error with formatted validation messages if environment parsing fails
  */
-export async function buildConfigFromEnvironment(env: EnsApiEnvironment): Promise<EnsApiConfig> {
+export function buildConfigFromEnvironment(env: EnsApiEnvironment): EnsApiConfig {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensapi/src/config/config.schema.ts` around lines 46 - 73, The
buildConfigFromEnvironment function is declared async and its JSDoc still says
it "fetches the EnsIndexerPublicConfig" even though there are no awaits or async
work; make it synchronous by removing the async modifier and changing the return
type from Promise<EnsApiConfig> to EnsApiConfig, and update the JSDoc to reflect
it simply builds/validates the config from the provided EnsApiEnvironment (no
fetching). Keep the existing error handling (ZodError branch using prettifyError
and logger.error) and ensure callers of buildConfigFromEnvironment are updated
to handle a direct EnsApiConfig return instead of awaiting it.

75-98: 🧹 Nitpick | 🔵 Trivial

Stale JSDoc on buildEnsApiPublicConfig.

@param config no longer matches the signature — the function now takes ensApiConfig and ensIndexerPublicConfig. Worth updating to document both params and note that namespace/isSubgraphCompatible come from the indexer config.

📝 Proposed doc fix
 /**
  * Builds the ENSApi public configuration from an EnsApiConfig object.
  *
- * `@param` config - The validated EnsApiConfig object
+ * `@param` ensApiConfig - The validated EnsApiConfig object (env-sourced)
+ * `@param` ensIndexerPublicConfig - The ENSIndexer public config (loaded from ENSDb),
+ *   used to derive `namespace` and `isSubgraphCompatible` for TheGraph fallback.
  * `@returns` A complete ENSApiPublicConfig object
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensapi/src/config/config.schema.ts` around lines 75 - 98, Update the
JSDoc for buildEnsApiPublicConfig to reflect its current signature: document
both parameters ensApiConfig (validated EnsApiConfig) and ensIndexerPublicConfig
(validated EnsIndexerPublicConfig), and note that namespace and
isSubgraphCompatible are read from ensIndexerPublicConfig; also ensure the
`@returns` tag correctly describes the returned EnsApiPublicConfig. Reference the
function name buildEnsApiPublicConfig and the parameter names ensApiConfig and
ensIndexerPublicConfig when making the doc changes.
packages/ensnode-sdk/src/stack-info/ensnode-stack-info.ts (1)

34-43: 🧹 Nitpick | 🔵 Trivial

Stale JSDoc — update to mention ENSIndexer and ENSRainbow inputs.

The comment only mentions ENSApi and ENSDb, but the signature now also takes ensIndexerPublicConfig (required) and ensRainbowPublicConfig (optional). Worth updating so the contract is clear — particularly that ensRainbow is intentionally optional and represents cold-start (which is already documented on the type field).

📝 Proposed doc fix
 /**
- * Build a complete {`@link` EnsNodeStackInfo} object from
- * the given public configs of ENSApi and ENSDb.
+ * Build a complete {`@link` EnsNodeStackInfo} object from the public configs of
+ * ENSApi, ENSDb, and ENSIndexer, plus an optional ENSRainbow public config
+ * (omit to represent ENSRainbow cold start).
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ensnode-sdk/src/stack-info/ensnode-stack-info.ts` around lines 34 -
43, Update the stale JSDoc for buildEnsNodeStackInfo to mention all inputs:
ensApiPublicConfig, ensDbPublicConfig, ensIndexerPublicConfig (required) and
ensRainbowPublicConfig (optional); explicitly note that ensRainbowPublicConfig
is optional and represents cold-start/disabled state for ENS Rainbow while
ensIndexerPublicConfig is now a required input, and ensure the description
aligns with the EnsNodeStackInfo type fields.
apps/ensapi/src/config/config.schema.test.ts (1)

61-82: ⚠️ Potential issue | 🟠 Major

This assertion contradicts the PR goal of an env-only EnsApiConfig.

Per the commit "Removed from EnsApiConfig any fields not sourced from environment variables", buildConfigFromEnvironment should no longer return ensIndexerPublicConfig, namespace, or rpcConfigs. The toStrictEqual on lines 63-81 still asserts all three. Either the code has not yet removed these fields from EnsApiConfig, or this test needs to be updated to reflect the new shape (mirroring what was done for the buildEnsApiPublicConfig tests below). Also, given the getEnsIndexerPublicConfig mock at lines 8-12, it's worth confirming whether buildConfigFromEnvironment still reads from ENSDb at all under the new design — if not, that mock can be dropped too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensapi/src/config/config.schema.test.ts` around lines 61 - 82, The test
asserts fields that should no longer be returned by buildConfigFromEnvironment;
update the expectation to only include environment-sourced fields (e.g., port,
ensDbUrl, theGraphApiKey, ensIndexerSchemaName,
referralProgramEditionConfigSetUrl) and remove assertions for
ensIndexerPublicConfig, namespace, and rpcConfigs from the toStrictEqual payload
for buildConfigFromEnvironment; also remove or adjust the
getEnsIndexerPublicConfig mock (lines 8-12) if buildConfigFromEnvironment no
longer reads ENSDb. Locate buildConfigFromEnvironment and the test block
referencing it to mirror the shape used by the buildEnsApiPublicConfig tests
below.
apps/ensadmin/src/app/mock/config-info/page.tsx (1)

22-49: ⚠️ Potential issue | 🟡 Minor

The mock data shape is not accurately represented by the type cast.

Line 22 asserts Record<string, SerializedENSApiPublicConfig>, but EnsApiPublicConfig (which SerializedENSApiPublicConfig aliases) contains only theGraphFallback and versionInfo. The actual mock data includes an additional ensIndexerPublicConfig field, which lines 46–48 depend on when calling deserializeEnsIndexerPublicConfig.

Change the type to accurately reflect the hybrid structure, such as:

type MockConfigData = Record<string, SerializedEnsApiPublicConfig & { ensIndexerPublicConfig: SerializedEnsIndexerPublicConfig }>;
const mockConfigData = mockDataJson as unknown as MockConfigData;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensadmin/src/app/mock/config-info/page.tsx` around lines 22 - 49, The
mockConfigData cast is too narrow: mockConfigData is typed as Record<string,
SerializedENSApiPublicConfig> but the actual entries also include
ensIndexerPublicConfig which deserializeEnsIndexerPublicConfig uses; update the
type of mockConfigData to reflect the combined shape (e.g., a Record keyed by
string whose value is SerializedENSApiPublicConfig & { ensIndexerPublicConfig:
SerializedEnsIndexerPublicConfig }) so that accesses in MockConfigPage
(selectedConfig, deserializeEnsApiPublicConfig,
deserializeEnsIndexerPublicConfig, and usage of ensRainbowPublicConfig) are
correctly typed and avoid unsafe casts.
apps/ensapi/src/omnigraph-api/yoga.ts (1)

15-57: 🛠️ Refactor suggestion | 🟠 Major

Memoize Yoga by namespace and pass context as a factory function to ensure fresh dataloaders and timestamps per request.

createYogaForNamespace is invoked on each HTTP request. Instantiating a complete Yoga server (schema compilation, plugin initialization, middleware wiring) per request is wasteful compared to caching by namespace. Additionally, the current context is precomputed once via createYogaContextForNamespace(namespace), which captures a fixed now timestamp and dataloader instances. If Yoga instances are cached without changing the context pattern, those stale values would leak across requests.

Yoga supports context as a factory function (invoked per request). Memoize Yoga instances by namespace and pass context as a function so dataloaders and now remain fresh for every operation. This is the standard GraphQL Yoga pattern.

♻️ Proposed fix
-export const createYogaForNamespace = (namespace: ENSNamespaceId) => {
-  const context = createYogaContextForNamespace(namespace);
-  const yoga = createYoga({
-    graphqlEndpoint: "*",
-    schema,
-    context,
-    ...
-  });
-  return yoga;
-};
+const yogaByNamespace = new Map<ENSNamespaceId, ReturnType<typeof createYoga>>();
+
+export const createYogaForNamespace = (namespace: ENSNamespaceId) => {
+  const cached = yogaByNamespace.get(namespace);
+  if (cached) return cached;
+
+  const yoga = createYoga({
+    graphqlEndpoint: "*",
+    schema,
+    // factory form: invoked per-request so dataloaders/now are fresh
+    context: () => createYogaContextForNamespace(namespace),
+    cors: false,
+    graphiql: { defaultQuery: `...` },
+    logging: logger,
+    plugins: [],
+  });
+
+  yogaByNamespace.set(namespace, yoga);
+  return yoga;
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensapi/src/omnigraph-api/yoga.ts` around lines 15 - 57,
createYogaForNamespace currently creates a new Yoga server on every request and
calls createYogaContextForNamespace(namespace) once, which captures a fixed now
and dataloader instances; instead, memoize the Yoga server per namespace (cache
by ENSNamespaceId inside createYogaForNamespace) and change the context option
to a factory function that calls createYogaContextForNamespace(namespace) per
request so dataloaders and timestamps are fresh for each operation; ensure the
cache key is the namespace and that the cached Yoga instance reuses schema,
plugins, logging, and graphiql settings but receives context as () =>
createYogaContextForNamespace(namespace).
apps/ensapi/src/lib/subgraph/indexing-status-to-subgraph-meta.ts (1)

10-24: 🧹 Nitpick | 🔵 Trivial

Update JSDoc to document the new ensIndexerPublicConfig parameter.

The @param list only mentions indexingStatus, but the signature now also takes ensIndexerPublicConfig. Either add a matching @param entry or drop the @param/@returns block entirely to keep it in sync.

📝 Proposed doc fix
  * `@param` indexingStatus - The indexing context from the indexing status middleware
- * `@returns` SubgraphMeta object or null if conversion is not possible
+ * `@param` ensIndexerPublicConfig - ENSIndexer public config used to derive ENS root chain id and deployment metadata
+ * `@returns` SubgraphMeta object or null if conversion is not possible
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensapi/src/lib/subgraph/indexing-status-to-subgraph-meta.ts` around
lines 10 - 24, The JSDoc for indexingContextToSubgraphMeta is out of sync: it
documents only the indexingStatus parameter but the function signature also
accepts ensIndexerPublicConfig; update the comment to include a new `@param` entry
describing ensIndexerPublicConfig (type/purpose and how it affects conversion)
and ensure the existing `@returns` remains accurate, or if you prefer, remove the
whole `@param/`@returns block—make sure the JSDoc and the function signature
(indexingContextToSubgraphMeta, indexingStatus, ensIndexerPublicConfig) match.
apps/ensapi/src/handlers/api/explore/name-tokens-api.ts (1)

86-127: 🧹 Nitpick | 🔵 Trivial

Drop lazyProxy here and hoist the namespace destructure; the lazy comment is stale.

Now that this code runs inside a request handler:

  • lazyProxy(() => getIndexedSubregistries(...)) is constructed and immediately consumed by .find() on the next line, so the indirection only costs a Proxy allocation per request without any deferral benefit.
  • The comment on lines 89–90 ("so that this module can be imported without env vars being present (e.g. during OpenAPI generation)") no longer applies — there is no module-level evaluation anymore.
  • { namespace } is destructured twice (line 87 and line 122) from the same c.var.stackInfo.ensIndexer. Hoist once at the top of the handler.
♻️ Proposed refactor
 app.openapi(getNameTokensRoute, async (c) => {
   ensureEnsNodeStackInfoAvailable(c);

+  const { namespace, plugins } = c.var.stackInfo.ensIndexer;
+
   // Check if Indexing Status resolution failed.
   if (c.var.indexingStatus instanceof Error) {
     ...
   }
@@
     const parentNode = namehashInterpretedName(parentName);
-    const { namespace, plugins } = c.var.stackInfo.ensIndexer;
-
-    // lazyProxy defers construction until first use so that this module can be
-    // imported without env vars being present (e.g. during OpenAPI generation).
-    const indexedSubregistries = lazyProxy(() =>
-      getIndexedSubregistries(namespace, plugins as PluginName[]),
-    );
-
-    const subregistry = indexedSubregistries.find((s) => s.node === parentNode);
+    const indexedSubregistries = getIndexedSubregistries(namespace, plugins as PluginName[]);
+    const subregistry = indexedSubregistries.find((s) => s.node === parentNode);
@@
-  const { namespace } = c.var.stackInfo.ensIndexer;
   const registeredNameTokens = await findRegisteredNameTokensForDomain(
     namespace,
     domainId,
     accurateAsOf,
   );

If lazyProxy / PluginName-typed imports become unused after this, drop them too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensapi/src/handlers/api/explore/name-tokens-api.ts` around lines 86 -
127, Remove the unnecessary lazyProxy indirection and hoist the namespace
destructure: extract const { namespace, plugins } = c.var.stackInfo.ensIndexer
once at the top of the handler, replace const indexedSubregistries =
lazyProxy(() => getIndexedSubregistries(namespace, plugins as PluginName[])) and
the subsequent indexedSubregistries.find(...) with a direct call
getIndexedSubregistries(namespace, plugins as PluginName[]) and find on its
result to avoid allocating a Proxy per request, and delete the stale lazyProxy
comment; if PluginName or lazyProxy imports become unused after this, remove
them as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/ensapi/src/middleware/stack-info.middleware.ts`:
- Around line 67-72: The current ensureEnsNodeStackInfoAvailable function
wrongly asserts success when c.var.stackInfo is undefined and throws a plain
Error for Error values; instead, add a pure type guard (e.g.,
isEnsNodeStackInfoAvailable(c) that returns boolean by checking c.var.stackInfo
is not undefined and not an Error) and replace/augment the assert function with
a middleware (e.g., requireEnsNodeStackInfo) that calls that guard and, on
failure, returns a 503 using the shared errorResponse helper (from
error-response.ts) rather than throwing; update callers to use the guard when
narrowing is optional or the middleware when a 503 response is desired, and
remove the throw of a plain Error from ensureEnsNodeStackInfoAvailable.

In `@apps/ensapi/src/omnigraph-api/context.ts`:
- Around line 25-37: createYogaContextForNamespace currently constructs
per-request values (loaders, now) but may be invoked only once during Yoga
server construction; ensure it's called per-request by wiring Yoga's context to
a factory that returns a fresh object each request (e.g. in
createYogaForNamespace/yoga.ts use context: () =>
createYogaContextForNamespace(namespace) so dataloaders and now aren't
long‑lived), or if you can't change yoga.ts, tighten the JSDoc on
createYogaContextForNamespace to state it must be invoked per-request and not
reused.

---

Outside diff comments:
In `@apps/ensadmin/src/app/mock/config-info/page.tsx`:
- Around line 22-49: The mockConfigData cast is too narrow: mockConfigData is
typed as Record<string, SerializedENSApiPublicConfig> but the actual entries
also include ensIndexerPublicConfig which deserializeEnsIndexerPublicConfig
uses; update the type of mockConfigData to reflect the combined shape (e.g., a
Record keyed by string whose value is SerializedENSApiPublicConfig & {
ensIndexerPublicConfig: SerializedEnsIndexerPublicConfig }) so that accesses in
MockConfigPage (selectedConfig, deserializeEnsApiPublicConfig,
deserializeEnsIndexerPublicConfig, and usage of ensRainbowPublicConfig) are
correctly typed and avoid unsafe casts.

In `@apps/ensapi/src/config/config.schema.test.ts`:
- Around line 61-82: The test asserts fields that should no longer be returned
by buildConfigFromEnvironment; update the expectation to only include
environment-sourced fields (e.g., port, ensDbUrl, theGraphApiKey,
ensIndexerSchemaName, referralProgramEditionConfigSetUrl) and remove assertions
for ensIndexerPublicConfig, namespace, and rpcConfigs from the toStrictEqual
payload for buildConfigFromEnvironment; also remove or adjust the
getEnsIndexerPublicConfig mock (lines 8-12) if buildConfigFromEnvironment no
longer reads ENSDb. Locate buildConfigFromEnvironment and the test block
referencing it to mirror the shape used by the buildEnsApiPublicConfig tests
below.

In `@apps/ensapi/src/config/config.schema.ts`:
- Around line 46-73: The buildConfigFromEnvironment function is declared async
and its JSDoc still says it "fetches the EnsIndexerPublicConfig" even though
there are no awaits or async work; make it synchronous by removing the async
modifier and changing the return type from Promise<EnsApiConfig> to
EnsApiConfig, and update the JSDoc to reflect it simply builds/validates the
config from the provided EnsApiEnvironment (no fetching). Keep the existing
error handling (ZodError branch using prettifyError and logger.error) and ensure
callers of buildConfigFromEnvironment are updated to handle a direct
EnsApiConfig return instead of awaiting it.
- Around line 75-98: Update the JSDoc for buildEnsApiPublicConfig to reflect its
current signature: document both parameters ensApiConfig (validated
EnsApiConfig) and ensIndexerPublicConfig (validated EnsIndexerPublicConfig), and
note that namespace and isSubgraphCompatible are read from
ensIndexerPublicConfig; also ensure the `@returns` tag correctly describes the
returned EnsApiPublicConfig. Reference the function name buildEnsApiPublicConfig
and the parameter names ensApiConfig and ensIndexerPublicConfig when making the
doc changes.

In `@apps/ensapi/src/handlers/api/explore/name-tokens-api.ts`:
- Around line 86-127: Remove the unnecessary lazyProxy indirection and hoist the
namespace destructure: extract const { namespace, plugins } =
c.var.stackInfo.ensIndexer once at the top of the handler, replace const
indexedSubregistries = lazyProxy(() => getIndexedSubregistries(namespace,
plugins as PluginName[])) and the subsequent indexedSubregistries.find(...) with
a direct call getIndexedSubregistries(namespace, plugins as PluginName[]) and
find on its result to avoid allocating a Proxy per request, and delete the stale
lazyProxy comment; if PluginName or lazyProxy imports become unused after this,
remove them as well.

In `@apps/ensapi/src/lib/public-client.ts`:
- Around line 11-40: The function buildPublicClientForRootChain currently parses
RPC configs on every call causing a perf regression; change the logic so you
compute rootChainId using getENSRootChainId(namespace) and check
_cache.has(rootChainId) first, returning the cached PublicClient if present, and
only call buildRpcConfigsFromEnv(config, namespace) and
RpcConfigsSchema.parse(...) when the cache misses to construct and set the
client via createPublicClient/fallback/http as before; also update the JSDoc to
refer to the `namespace` parameter (not `chainId`) to keep comments accurate.

In `@apps/ensapi/src/lib/subgraph/indexing-status-to-subgraph-meta.ts`:
- Around line 10-24: The JSDoc for indexingContextToSubgraphMeta is out of sync:
it documents only the indexingStatus parameter but the function signature also
accepts ensIndexerPublicConfig; update the comment to include a new `@param` entry
describing ensIndexerPublicConfig (type/purpose and how it affects conversion)
and ensure the existing `@returns` remains accurate, or if you prefer, remove the
whole `@param/`@returns block—make sure the JSDoc and the function signature
(indexingContextToSubgraphMeta, indexingStatus, ensIndexerPublicConfig) match.

In `@apps/ensapi/src/omnigraph-api/yoga.ts`:
- Around line 15-57: createYogaForNamespace currently creates a new Yoga server
on every request and calls createYogaContextForNamespace(namespace) once, which
captures a fixed now and dataloader instances; instead, memoize the Yoga server
per namespace (cache by ENSNamespaceId inside createYogaForNamespace) and change
the context option to a factory function that calls
createYogaContextForNamespace(namespace) per request so dataloaders and
timestamps are fresh for each operation; ensure the cache key is the namespace
and that the cached Yoga instance reuses schema, plugins, logging, and graphiql
settings but receives context as () => createYogaContextForNamespace(namespace).

In `@packages/ensnode-sdk/src/stack-info/ensnode-stack-info.ts`:
- Around line 34-43: Update the stale JSDoc for buildEnsNodeStackInfo to mention
all inputs: ensApiPublicConfig, ensDbPublicConfig, ensIndexerPublicConfig
(required) and ensRainbowPublicConfig (optional); explicitly note that
ensRainbowPublicConfig is optional and represents cold-start/disabled state for
ENS Rainbow while ensIndexerPublicConfig is now a required input, and ensure the
description aligns with the EnsNodeStackInfo type fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 105504b5-2034-4736-a3e8-fb48a181e818

📥 Commits

Reviewing files that changed from the base of the PR and between 9abb302 and 1fecc67.

📒 Files selected for processing (49)
  • apps/ensadmin/src/app/mock/config-info/page.tsx
  • apps/ensadmin/src/app/mock/indexing-status-api.mock.ts
  • apps/ensapi/src/cache/stack-info.cache.ts
  • apps/ensapi/src/config/config.schema.test.ts
  • apps/ensapi/src/config/config.schema.ts
  • apps/ensapi/src/config/redact.ts
  • apps/ensapi/src/handlers/api/explore/name-tokens-api.ts
  • apps/ensapi/src/handlers/api/meta/status-api.ts
  • apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.ts
  • apps/ensapi/src/handlers/api/resolution/resolution-api.ts
  • apps/ensapi/src/handlers/api/router.ts
  • apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.test.ts
  • apps/ensapi/src/handlers/subgraph/subgraph-api.ts
  • apps/ensapi/src/lib/name-tokens/find-name-tokens-for-domain.ts
  • apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts
  • apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts
  • apps/ensapi/src/lib/public-client.ts
  • apps/ensapi/src/lib/resolution/forward-resolution.ts
  • apps/ensapi/src/lib/resolution/multichain-primary-name-resolution.ts
  • apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.integration.test.ts
  • apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts
  • apps/ensapi/src/lib/resolution/reverse-resolution.ts
  • apps/ensapi/src/lib/subgraph/indexing-status-to-subgraph-meta.ts
  • apps/ensapi/src/middleware/can-accelerate.middleware.ts
  • apps/ensapi/src/middleware/name-tokens.middleware.ts
  • apps/ensapi/src/middleware/registrar-actions.middleware.ts
  • apps/ensapi/src/middleware/stack-info.middleware.ts
  • apps/ensapi/src/middleware/subgraph-meta.middleware.ts
  • apps/ensapi/src/middleware/thegraph-fallback.middleware.ts
  • apps/ensapi/src/omnigraph-api/builder.ts
  • apps/ensapi/src/omnigraph-api/context.ts
  • apps/ensapi/src/omnigraph-api/lib/find-domains/canonical-registries-cte.ts
  • apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts
  • apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-canonical.ts
  • apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts
  • apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts
  • apps/ensapi/src/omnigraph-api/schema/account.ts
  • apps/ensapi/src/omnigraph-api/schema/query.ts
  • apps/ensapi/src/omnigraph-api/schema/resolver.ts
  • apps/ensapi/src/omnigraph-api/yoga.ts
  • packages/ensnode-sdk/src/ensapi/config/conversions.test.ts
  • packages/ensnode-sdk/src/ensapi/config/deserialize.ts
  • packages/ensnode-sdk/src/ensapi/config/serialize.ts
  • packages/ensnode-sdk/src/ensapi/config/serialized-types.ts
  • packages/ensnode-sdk/src/ensapi/config/types.ts
  • packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts
  • packages/ensnode-sdk/src/ensnode/client.test.ts
  • packages/ensnode-sdk/src/stack-info/ensnode-stack-info.ts
  • packages/ensnode-sdk/src/stack-info/serialize/ensnode-stack-info.ts
💤 Files with no reviewable changes (6)
  • apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.test.ts
  • apps/ensadmin/src/app/mock/indexing-status-api.mock.ts
  • packages/ensnode-sdk/src/ensapi/config/conversions.test.ts
  • packages/ensnode-sdk/src/ensapi/config/types.ts
  • packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts
  • packages/ensnode-sdk/src/ensnode/client.test.ts

Comment on lines +67 to +72
export function ensureEnsNodeStackInfoAvailable<T extends Context>(
c: T,
): asserts c is T & { var: T["var"] & { stackInfo: EnsNodeStackInfo } } {
if (c.var.stackInfo instanceof Error) {
throw new Error(`ENSNode Stack Info is not available: ${c.var.stackInfo.message}`);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t assert success when stack info is missing or turn availability failures into a 500.

Line 70 only rejects Error, so an omitted/not-yet-run middleware leaves c.var.stackInfo === undefined and this assertion incorrectly narrows it to EnsNodeStackInfo. When it is an Error, throwing a plain Error also bypasses the PR’s intended 503 response path for stack-info-dependent routes. Split this into a boolean type guard plus a route/middleware 503 response, or have a dedicated “require stack info” middleware return the shared ENSApi error response before downstream handlers run.

As per coding guidelines, use the shared errorResponse helper (apps/ensapi/src/lib/handlers/error-response.ts) for all error responses in ENSApi.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensapi/src/middleware/stack-info.middleware.ts` around lines 67 - 72,
The current ensureEnsNodeStackInfoAvailable function wrongly asserts success
when c.var.stackInfo is undefined and throws a plain Error for Error values;
instead, add a pure type guard (e.g., isEnsNodeStackInfoAvailable(c) that
returns boolean by checking c.var.stackInfo is not undefined and not an Error)
and replace/augment the assert function with a middleware (e.g.,
requireEnsNodeStackInfo) that calls that guard and, on failure, returns a 503
using the shared errorResponse helper (from error-response.ts) rather than
throwing; update callers to use the guard when narrowing is optional or the
middleware when a 503 response is desired, and remove the throw of a plain Error
from ensureEnsNodeStackInfoAvailable.

Comment on lines 25 to 37
/**
* Constructs a new GraphQL Context per-request.
* Constructs a new GraphQL Context per-request for the given {@link ENSNamespaceId}.
*
* @dev make sure that anything that is per-request (like dataloaders) are newly created in this fn
*/
export const context = () => ({
export const createYogaContextForNamespace = (namespace: ENSNamespaceId) => ({
namespace,
now: BigInt(getUnixTime(new Date())),
loaders: {
v1CanonicalPath: createV1CanonicalPathLoader(),
v2CanonicalPath: createV2CanonicalPathLoader(),
v2CanonicalPath: createV2CanonicalPathLoader(namespace),
},
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Context freshness is coupled to Yoga construction, not to requests.

The JSDoc states per-request values (dataloaders, now) are created in this function, but that invariant only holds if the function is invoked per request. As used in yoga.ts today, it's invoked once per Yoga construction — correctness relies on createYogaForNamespace also being called per request (see the related comment). If Yoga is memoized by namespace (recommended), this object becomes long‑lived and dataloaders would batch/cache across unrelated requests, and now would drift.

Recommend wiring this as a per-request factory in yoga.ts (context: () => createYogaContextForNamespace(namespace)). No change required here if the yoga.ts change is adopted, but consider tightening the JSDoc to reflect that this function must be invoked per-request by the Yoga setup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ensapi/src/omnigraph-api/context.ts` around lines 25 - 37,
createYogaContextForNamespace currently constructs per-request values (loaders,
now) but may be invoked only once during Yoga server construction; ensure it's
called per-request by wiring Yoga's context to a factory that returns a fresh
object each request (e.g. in createYogaForNamespace/yoga.ts use context: () =>
createYogaContextForNamespace(namespace) so dataloaders and now aren't
long‑lived), or if you can't change yoga.ts, tighten the JSDoc on
createYogaContextForNamespace to state it must be invoked per-request and not
reused.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR continues the work from #1806 by splitting ENSApi configuration into (1) static env-based config and (2) dynamic, ENSDb-derived config carried via EnsNodeStackInfo and passed through middleware/context.

Changes:

  • Removed ensIndexerPublicConfig from EnsApiPublicConfig and updated SDK serialization/deserialization + tests accordingly.
  • Introduced stack-info-driven namespace/config plumbing across ENSApi (middlewares, resolution, omnigraph), replacing direct config.namespace reads.
  • Updated stack info building/caching to fetch EnsIndexerPublicConfig from ENSDb and assemble a full EnsNodeStackInfo.

Reviewed changes

Copilot reviewed 49 out of 49 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/ensnode-sdk/src/stack-info/serialize/ensnode-stack-info.ts Stops re-serializing ENSApi config; uses stackInfo field directly.
packages/ensnode-sdk/src/stack-info/ensnode-stack-info.ts Splits stack info builder inputs to accept indexer/rainbow configs separately.
packages/ensnode-sdk/src/ensnode/client.test.ts Updates client test fixtures to match new ENSApi public config shape.
packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts Removes indexer-public-config fields from ENSApi config schemas.
packages/ensnode-sdk/src/ensapi/config/types.ts Removes ensIndexerPublicConfig from EnsApiPublicConfig.
packages/ensnode-sdk/src/ensapi/config/serialized-types.ts Simplifies serialized type to alias EnsApiPublicConfig.
packages/ensnode-sdk/src/ensapi/config/serialize.ts Drops indexer serialization from ENSApi config serialization.
packages/ensnode-sdk/src/ensapi/config/deserialize.ts Drops indexer deserialization from ENSApi config deserialization.
packages/ensnode-sdk/src/ensapi/config/conversions.test.ts Updates ENSApi config conversion tests to new shape.
apps/ensapi/src/omnigraph-api/yoga.ts Creates namespace-specific Yoga/context factory.
apps/ensapi/src/omnigraph-api/schema/resolver.ts Switches from global config namespace to request context namespace.
apps/ensapi/src/omnigraph-api/schema/query.ts Switches domain filtering/ID derivation to use context namespace.
apps/ensapi/src/omnigraph-api/schema/account.ts Switches canonical filtering to use context namespace.
apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts Threads namespace into ENSv2 root registry lookup.
apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts Threads namespace into ENSv2 canonical path logic.
apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-canonical.ts Threads namespace into canonical registries CTE.
apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts Updates context typing to new namespace-aware context creator.
apps/ensapi/src/omnigraph-api/lib/find-domains/canonical-registries-cte.ts Makes canonical registries CTE namespace-dependent.
apps/ensapi/src/omnigraph-api/context.ts Adds namespace to GraphQL context and loader construction.
apps/ensapi/src/omnigraph-api/builder.ts Updates schema builder context typing.
apps/ensapi/src/middleware/thegraph-fallback.middleware.ts Uses stackInfo-derived namespace/subgraph-compatibility for fallback logic.
apps/ensapi/src/middleware/subgraph-meta.middleware.ts Passes stackInfo indexer config into subgraph meta mapping.
apps/ensapi/src/middleware/stack-info.middleware.ts Adds ensureEnsNodeStackInfoAvailable type guard helper.
apps/ensapi/src/middleware/registrar-actions.middleware.ts Switches registrar actions prerequisites to stackInfo indexer config.
apps/ensapi/src/middleware/name-tokens.middleware.ts Switches name tokens prerequisites to stackInfo indexer config.
apps/ensapi/src/middleware/can-accelerate.middleware.ts Uses stackInfo plugins for acceleration gating (ENSv2 bailout + plugin checks).
apps/ensapi/src/lib/subgraph/indexing-status-to-subgraph-meta.ts Removes global config usage; uses indexer config passed in.
apps/ensapi/src/lib/resolution/reverse-resolution.ts Threads namespace/plugins into reverse resolution and forwards into forward resolution.
apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.ts Threads namespace into datasource contract selection (UR v1/v2).
apps/ensapi/src/lib/resolution/resolve-with-universal-resolver.integration.test.ts Updates integration test to new public client API + namespace passing.
apps/ensapi/src/lib/resolution/multichain-primary-name-resolution.ts Threads namespace/plugins into primary-name resolution; updates chainId selection logic.
apps/ensapi/src/lib/resolution/forward-resolution.ts Threads namespace/plugins into forward resolution; switches to root-chain public client builder.
apps/ensapi/src/lib/public-client.ts Replaces chainId-based client getter with root-chain client builder per namespace.
apps/ensapi/src/lib/protocol-acceleration/get-records-from-index.ts Threads namespace for resolver defaulting detection.
apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts Threads namespace into resolver lookup paths and datasource contract selection.
apps/ensapi/src/lib/name-tokens/find-name-tokens-for-domain.ts Threads namespace into token ownership computation.
apps/ensapi/src/handlers/subgraph/subgraph-api.ts Ensures stackInfo middleware is installed and prerequisites use stackInfo config.
apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.test.ts Removes now-unneeded config mocking for namespace.
apps/ensapi/src/handlers/api/router.ts Adds stackInfoMiddleware at the API router level.
apps/ensapi/src/handlers/api/resolution/resolution-api.ts Uses stackInfo namespace/plugins for resolution calls.
apps/ensapi/src/handlers/api/omnigraph/omnigraph-api.ts Uses stackInfo namespace and creates Yoga instance for that namespace.
apps/ensapi/src/handlers/api/meta/status-api.ts Uses stackInfo guard and simplifies error conditions.
apps/ensapi/src/handlers/api/explore/name-tokens-api.ts Adds stackInfo middleware + uses stackInfo namespace/plugins in routing logic.
apps/ensapi/src/config/redact.ts Removes redaction of fields that no longer exist on env-based config.
apps/ensapi/src/config/config.schema.ts Removes ENSDb-fetched indexer config and rpc configs from env config; makes public config builder accept indexer config input.
apps/ensapi/src/config/config.schema.test.ts Updates tests for new buildEnsApiPublicConfig signature and config shape.
apps/ensapi/src/cache/stack-info.cache.ts Fetches indexer public config from ENSDb and builds full stack info.
apps/ensadmin/src/app/mock/indexing-status-api.mock.ts Updates mock ENSApi public config to new shape.
apps/ensadmin/src/app/mock/config-info/page.tsx Updates mock page to build stack info from split config pieces.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +67 to +72
export function ensureEnsNodeStackInfoAvailable<T extends Context>(
c: T,
): asserts c is T & { var: T["var"] & { stackInfo: EnsNodeStackInfo } } {
if (c.var.stackInfo instanceof Error) {
throw new Error(`ENSNode Stack Info is not available: ${c.var.stackInfo.message}`);
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensureEnsNodeStackInfoAvailable only checks for Error, but (1) it doesn’t handle c.var.stackInfo === undefined (e.g. if stackInfoMiddleware wasn’t applied), and (2) throwing here will be caught by app.onError and turned into a 500, even though the PR description indicates stack-info-dependent routes should return 503 when stack info isn’t available.

Consider changing this helper to either (a) return a boolean/EnsNodeStackInfo | null so callers can return a 503 response, or (b) throw a typed error that is mapped to 503 by a centralized error handler, and also explicitly guard against undefined.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +20
export function buildPublicClientForRootChain(namespace: ENSNamespaceId): PublicClient {
const rootChainId = getENSRootChainId(namespace);
const unvalidatedRpcConfigs = buildRpcConfigsFromEnv(config, namespace);
const rpcConfigs = RpcConfigsSchema.parse(unvalidatedRpcConfigs);
const rpcConfig = rpcConfigs.get(rootChainId);

Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildRpcConfigsFromEnv expects a raw RpcEnvironment (e.g. ALCHEMY_API_KEY, RPC_URL_<id>), but this function is being called with the parsed config object. Since EnsApiConfigSchema no longer includes those env vars, this will always yield empty RPC configs (except the hardcoded ens-test-env), causing rpcConfigs.get(rootChainId) to be undefined and resolution to fail at runtime.

Pass the actual runtime environment (e.g. process.env typed as RpcEnvironment/EnsApiEnvironment) or persist the needed RPC env vars/derived rpcConfigs in config at init time, and avoid re-parsing env on every call when the client is already cached.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +48
const ensApiPublicConfig = deserializeEnsApiPublicConfig(mockConfigData[selectedConfig]);
const ensIndexerPublicConfig = deserializeEnsIndexerPublicConfig(
mockConfigData[selectedConfig],
);
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deserializeEnsIndexerPublicConfig expects a SerializedEnsIndexerPublicConfig, but this code passes the entire SerializedENSApiPublicConfig object (which in the current mock JSON contains ensIndexerPublicConfig nested). As a result, deserialization will fail because required indexer fields aren’t at the top level.

Use the nested ensIndexerPublicConfig value from the mock JSON (or update the mock JSON to the new split shape), and update mockConfigData’s type accordingly so TypeScript reflects the actual structure.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +20
export const createYogaForNamespace = (namespace: ENSNamespaceId) => {
const context = createYogaContextForNamespace(namespace);
const yoga = createYoga({
graphqlEndpoint: "*",
schema,
context,
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createYogaForNamespace constructs a brand new Yoga server (and a single, precomputed context object with now) each time it’s called. In omnigraph-api.ts this appears to be invoked per request, which is likely very expensive and also makes it hard to ensure per-request context values if you later cache/reuse Yoga.

Consider caching the Yoga instance per namespace (e.g. a module-level Map<ENSNamespaceId, YogaServer>) and passing context as a function (e.g. () => createYogaContextForNamespace(namespace)) so dataloaders/now are recreated per request while reusing the server + schema.

Copilot uses AI. Check for mistakes.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 21, 2026

Greptile Summary

This WIP PR splits the ENSApi config data model: static config (env vars only) is handled by EnsApiConfig, while all dynamic runtime data flows exclusively through the EnsNodeStackInfo object fetched from cache on every request — returning 503 when unavailable. The separation is architecturally sound, but two in-progress inconsistencies need attention before merge:

  • config.schema.test.ts expects buildConfigFromEnvironment to produce ensIndexerPublicConfig, namespace, and rpcConfigs, and expects buildEnsApiPublicConfig to embed ensIndexerPublicConfig — fields absent from the current implementation; these tests will fail.
  • apps/ensadmin/src/app/mock/config-info/page.tsx passes the outer combined mock object to deserializeEnsIndexerPublicConfig instead of the nested .ensIndexerPublicConfig value, causing a runtime deserialization failure on every render.

Confidence Score: 4/5

Two P1 defects (failing tests + broken mock page) must be resolved before merging

The core refactor (env-only config, EnsNodeStackInfo for dynamic data, SWR cache) is clean and well-structured. However, the test file has concrete assertions that will fail against the current implementation, and the mock admin page will always hit the error branch at runtime due to a mismatched deserialization call. These are present defects on the changed code path, not speculative risks.

apps/ensapi/src/config/config.schema.test.ts and apps/ensadmin/src/app/mock/config-info/page.tsx

Important Files Changed

Filename Overview
apps/ensapi/src/config/config.schema.test.ts Tests expect fields (ensIndexerPublicConfig, namespace, rpcConfigs) that the current implementation does not produce; will fail as-is
apps/ensadmin/src/app/mock/config-info/page.tsx Passes the outer combined mock object to deserializeEnsIndexerPublicConfig instead of the nested .ensIndexerPublicConfig value; will always fail deserialization at runtime
apps/ensapi/src/config/config.schema.ts Refactored to env-var-only config; buildEnsApiPublicConfig no longer embeds ensIndexerPublicConfig in its return, but tests in config.schema.test.ts still expect it — mismatch causes test failures
apps/ensapi/src/cache/stack-info.cache.ts Correctly loads EnsNodeStackInfo from ENSDb on demand, caches it indefinitely on success, and retries after 1-minute error TTL; lazy-proxy defers construction appropriately
apps/ensapi/src/middleware/stack-info.middleware.ts New middleware cleanly surfaces `EnsNodeStackInfo
packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts Adds makeSerializedEnsApiPublicConfigSchema which is structurally identical to makeEnsApiPublicConfigSchema; redundant because SerializedEnsApiPublicConfig = EnsApiPublicConfig
packages/ensnode-sdk/src/stack-info/ensnode-stack-info.ts Clean interface for EnsNodeStackInfo and builder function; correctly holds the now-separate EnsApiPublicConfig and EnsIndexerPublicConfig
packages/ensnode-sdk/src/ensapi/config/types.ts EnsApiPublicConfig correctly trimmed to only theGraphFallback and versionInfo; old alias preserved with deprecation notice

Sequence Diagram

sequenceDiagram
    participant ENV as Env Vars
    participant CFG as EnsApiConfig (static)
    participant CACHE as stackInfoCache (SWR)
    participant DB as ENSDb Client
    participant MW as stackInfoMiddleware
    participant H as Request Handler

    ENV->>CFG: buildConfigFromEnvironment()
    Note over CFG: port, theGraphApiKey,<br/>ensDbUrl, etc.

    activate CACHE
    CACHE->>DB: getEnsIndexerPublicConfig()
    DB-->>CACHE: EnsIndexerPublicConfig
    CACHE->>DB: buildEnsDbPublicConfig()
    DB-->>CACHE: EnsDbPublicConfig
    Note over CACHE: Build EnsNodeStackInfo<br/>(TTL=∞, errorTTL=1min)
    deactivate CACHE

    H->>MW: HTTP Request
    MW->>CACHE: stackInfoCache.read()
    alt Stack info available
        CACHE-->>MW: EnsNodeStackInfo
        MW->>H: c.var.stackInfo = EnsNodeStackInfo
        H-->>H: ensureEnsNodeStackInfoAvailable(c)
        H-->>H: Use stackInfo.ensIndexer, stackInfo.ensApi, etc.
    else Stack info unavailable
        CACHE-->>MW: Error
        MW->>H: c.var.stackInfo = Error
        H-->>H: ensureEnsNodeStackInfoAvailable throws
        H-->>H: Return 503
    end
Loading

Comments Outside Diff (2)

  1. apps/ensapi/src/config/config.schema.test.ts, line 62-81 (link)

    P1 Test/implementation mismatch — tests will fail

    The test for buildConfigFromEnvironment expects the result to include ensIndexerPublicConfig, namespace, and rpcConfigs, but the current EnsApiConfigSchema in config.schema.ts only produces port, theGraphApiKey, referralProgramEditionConfigSetUrl, ensDbUrl, and ensIndexerSchemaName. The mocked ensDbClient.getEnsIndexerPublicConfig is never called by buildConfigFromEnvironment, so ensIndexerPublicConfig will be absent and the toStrictEqual assertion will fail.

    Similarly, lines 163–171 assert that buildEnsApiPublicConfig returns an ensIndexerPublicConfig field, but EnsApiPublicConfig is defined with only theGraphFallback and versionInfo — the function never adds ensIndexerPublicConfig to its return value.

  2. packages/ensnode-sdk/src/ensapi/config/zod-schemas.ts, line 37-44 (link)

    P2 Duplicate schemas — makeSerializedEnsApiPublicConfigSchema is identical to makeEnsApiPublicConfigSchema

    Because SerializedEnsApiPublicConfig is defined as type SerializedEnsApiPublicConfig = EnsApiPublicConfig (a straight alias), both factory functions produce the exact same Zod schema. The second function adds no value and may cause confusion for future maintainers about whether serialized vs runtime shapes are expected to diverge. Consider removing makeSerializedEnsApiPublicConfigSchema or leaving only a re-export alias if it must remain for backward compatibility.

Reviews (1): Last reviewed commit: "Use per-request config read for namepace..." | Re-trigger Greptile

Comment on lines +45 to +48
const ensApiPublicConfig = deserializeEnsApiPublicConfig(mockConfigData[selectedConfig]);
const ensIndexerPublicConfig = deserializeEnsIndexerPublicConfig(
mockConfigData[selectedConfig],
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Wrong deserialization target for ENSIndexer config

mockConfigData[selectedConfig] is the outer combined object { versionInfo, theGraphFallback, ensIndexerPublicConfig: {...} }, but deserializeEnsIndexerPublicConfig expects an object with ENSIndexer-level fields (labelSet, indexedChainIds, etc.) at the top level. The ENSIndexer fields are one level deeper inside ensIndexerPublicConfig. As a result, deserializeEnsIndexerPublicConfig will always fail validation here — the page will always display a "Deserialization Error" instead of the intended mock state.

The correct source for the ENSIndexer config is the nested property, e.g. (mockConfigData[selectedConfig] as any).ensIndexerPublicConfig.

Copy link
Copy Markdown
Contributor

@vercel vercel Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestions:

  1. Test expectations are out of sync with refactored buildConfigFromEnvironment function, expecting removed properties that are no longer part of EnsApiConfig type
  1. Tests for buildConfigFromEnvironment were not updated after refactoring removed RPC config validation and initialization from the config building phase

Fix on Vercel

rpcConfigs: new Map([
[
1,
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test expects buildEnsApiPublicConfig to return ensIndexerPublicConfig field, but the refactored function doesn't include it in the return value

Fix on Vercel

// Invariant: ENSApi must have an rpcConfig for the requested `chainId`
const rpcConfig = config.rpcConfigs.get(chainId);
export function buildPublicClientForRootChain(namespace: ENSNamespaceId): PublicClient {
const rootChainId = getENSRootChainId(namespace);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildPublicClientForRootChain passes EnsApiConfig object to buildRpcConfigsFromEnv instead of environment variables, causing RPC configuration to fail

Fix on Vercel

Copy link
Copy Markdown
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tk-o Overall I like where this is going. Shared some preliminary feedback 👍

},
} satisfies SerializedEnsApiPublicConfig;

const EXAMPLE_ENSDB_PUBLIC_RESPONSE = {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const EXAMPLE_ENSDB_PUBLIC_RESPONSE = {
const EXAMPLE_ENSDB_PUBLIC_CONFIG = {

@@ -69,35 +69,6 @@ const EXAMPLE_ENSAPI_CONFIG_RESPONSE = {
canFallback: false,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several lines above we should rename EXAMPLE_ENSAPI_CONFIG_RESPONSE to EXAMPLE_ENSAPI_PUBLIC_CONFIG

},
},
} satisfies SerializedEnsApiPublicConfig;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm interested to also see the following updates. Could we include them in this PR?

  1. Introduce a new type EnsIndexerStackInfo that has two fields: ensIndexer and ensRainbow, each the relevant public config type.
  2. Use EnsIndexerStackInfo to split ensRainbowPublicConfig from EnsIndexerPublicConfig.
  3. Remove the count from ensRainbowPublicConfig so that ENSRainbow can immediately return its config without waiting.
  4. Guarantee that both EnsIndexerStackInfo and EnsNodeStackInfo have all their fields non-optional. In other words, there's never a case where ensRainbow is undefined.
  5. Update the data model for ensRainbowPublicConfig so that it aligns with the versionInfo standards from the other config types. It shouldn't have version as a root-level field. Instead it should have a versionInfo object with a field inside of it for ensRainbow that might be a value such as "0.31.0".


// lazyProxy defers construction until first use so that this module can be
// imported without env vars being present (e.g. during OpenAPI generation).
const ensv1RegistryOld = lazyProxy(() =>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you send me a message in Slack when you have an opportunity to reply to this? I'm curious to understand if the work in this PR enables us to completely remove the use of this lazyProxy stuff? As I remember it was a source of some complexity for us and we had the goal of fully removing it.

export function getPublicClient(chainId: ChainId): PublicClient {
// Invariant: ENSApi must have an rpcConfig for the requested `chainId`
const rpcConfig = config.rpcConfigs.get(chainId);
export function buildPublicClientForRootChain(namespace: ENSNamespaceId): PublicClient {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tk-o Hey, please ask Matt to review this when you're ready. Please use special care to ensure this is 100% correct.

*/
export async function resolveForward<SELECTION extends ResolverRecordsSelection>(
namespace: ENSNamespaceId,
plugins: string[],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any other suggestions for how we might pass these values around?

I dislike how we are decomposing our config objects / stack objects into independent fields where invariants are being erased because now they are just random fields that are theoretically independent of each other.

Why can't we still have some singleton object such as stack (rather than config) that would hold the stack. You could implement some getter on it that would throw an error if for some reason the data for the stack hadn't been initialized already.

*
* @param c The Hono context
*/
export function ensureEnsNodeStackInfoAvailable<T extends Context>(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dislike this approach. If the stack info isn't loaded yet it shouldn't result in a HTTP 500 error. It should result in a HTTP 503 error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Split config objects in ENSApi

3 participants