Making Multi-Chain a Config Problem, Not a Codebase Problem

The first extra chain is usually an integration. The third one is an architecture problem.

I worked on a decentralized NFT marketplace built on the Cosmos ecosystem. The product lets creators launch full collections without deploying their own contracts, and enforces royalties on secondary sales directly on-chain — so creators receive a share of every resale automatically, without relying on platform policy. Each chain it runs on has its own community, its own market, and its own set of creators and collectors.

That last part is what made multi-chain real rather than cosmetic. The marketplace was not porting a single audience across chains. It was serving distinct communities on Terra 1, Terra 2, Injective, Xion, and Orai, each expecting the same quality product on their chain of choice.

The codebase, though, had not been designed for multi-chain. It had grown into it, one chain at a time, in the way that feels expedient at the start and unsustainable by the end.

The Before State

The original approach to adding a new chain was the obvious one: copy the existing codebase, update the chain-specific constants, adjust the environment variables, and maintain a separate GitHub repository per chain.

That works for the second chain. By the fifth, it had created a set of problems that were more expensive than the initial engineering would have been.

Diverging codebases. Each repository started as a copy of the others but drifted independently. A bug fixed in one chain's repo had to be manually identified, ported, and verified across every other repo. A new feature built for Injective was not automatically available on Xion. The repositories had started as siblings and turned into distant cousins.

Merge overhead. Any shared improvement — a UI fix, a contract interaction change, a new wallet adapter — required opening pull requests against multiple repositories. Each merge carried its own review, its own CI run, its own chance of a conflict. The larger the shared change, the more painful the process.

Separate build and deploy environments. Five chains meant five sets of environment configuration, five build pipelines, and five separate deployments to coordinate. A release that touched shared logic required shipping five times.

Regressions through omission. The most common failure was not a wrong code change — it was a missing one. A fix merged to Terra 2 and forgotten on Orai. A feature enabled on Injective that never reached the other chains. The bugs were not from bad code. They were from the overhead of keeping five copies of the same application consistent by hand.

The Architecture Change

The goal was to move from one repo per chain to one repo that understood all chains through configuration. That required solving two distinct problems: what goes in the config, and what the application code does with it.

The Chain Config Schema

Every chain needed a typed configuration entry that captured everything the application needed to know about it:

interface ChainConfig {
  chainId: string
  chainName: string
  bech32Prefix: string
  rpc: string
  lcd: string
  nativeCurrency: {
    denom: string
    decimals: number
    display: string
  }
  contracts: {
    marketplace: string
    nftFactory: string
  }
  features: {
    auctions: boolean
    lazyMinting: boolean
    collections: boolean
  }
  explorerUrl: string
  wallets: WalletAdapter[]
}

Each of the five chains became an entry in a validated config map. Contract addresses, RPC and LCD endpoints, bech32 prefixes, native currency details, supported feature flags, and wallet adapters all lived in one place per chain. When something needed to change — an RPC endpoint, a redeployed contract address — it changed in one file, in one pull request, reviewed once.

Capability Flags Over Chain Checks

The second problem was how product code consumed this config. The original codebase was full of chain-specific conditionals: checks that tested whether the active chain was Injective, or Terra 2, or Xion, and branched accordingly.

That pattern couples product logic to network identity in a way that does not scale. Every new chain requires finding every branch and deciding whether it applies. Every removed chain leaves dead branches behind.

The replacement was capability-driven logic. Product code asks whether a feature is supported, not which chain is active:

// before
if (chainId === 'injective-1' || chainId === 'xion-1') {
  showAuctionUI()
}

// after
if (chain.features.auctions) {
  showAuctionUI()
}

The chain config owns the decision. The product code owns the behavior. Adding a new chain does not require reading all the old chain checks to decide which ones apply.

Wallet Adapters

The Cosmos ecosystem has a common wallet standard but each chain needs to be registered separately with wallets like Keplr. That registration includes chain-specific details: the chain ID, the bech32 prefix, gas settings, currency info, and RPC endpoint.

Rather than handling this ad hoc in each integration, the wallet adapter layer read directly from the chain config. Registering a new chain with any wallet became a function of the config entry, not a manual one-off step.

The Integration Checklist

Typing the config and moving chain logic into adapters created a side effect worth naming: the integration surface for a new chain became explicit. Adding a chain meant filling in a config entry and walking a known checklist — contract addresses, RPC health check, wallet registration, feature flag decisions, explorer link, and a smoke test against each capability. The checklist was the same every time. That made it reviewable, delegatable, and faster to complete without missing anything.

What the Merge Problem Looked Like in Practice

The clearest way to describe the cost of the old approach is to describe a specific failure mode: shipping the same fix to five repos.

A bug in the marketplace interaction code would be identified, diagnosed, and fixed in whichever chain's repo it was first noticed. The fix would be merged and deployed. Then the question was: does this fix apply to the other chains? If yes — and it usually did, because most logic was shared — someone had to open four more pull requests, against four more repos, with four more CI runs, and four more deployments.

That process is not hard. It is just friction that accumulates. Over time, it trains you to delay fixes, to bundle changes, or to skip the cross-chain work on the assumption that someone else will catch it. Some fixes never made it across all five repos.

In the unified codebase, that class of problem stops existing. A fix merged once is deployed everywhere. There is no omission case because there is no separate codebase to forget.

For a marketplace where royalty enforcement is the core creator promise, that reliability matters more than it would in a typical product. A bug that reached only two of five chains was still a broken promise to creators on those chains.

Product Decisions

Make config the boundary, not the escape valve. A chain config is only useful if the application actually reads from it rather than working around it with in-code constants. The discipline of routing all chain-specific state through the config is what gives the architecture leverage.

Type the config and validate it at startup. A loose config object catches mistakes at runtime in the wrong environment. A typed, validated schema catches them at build time in the right one.

Keep escape hatches small and explicit. Some chains will have behavior that does not fit the shared model. That is acceptable. The goal is not zero per-chain code — it is a small, explicit, named integration surface rather than scattered conditionals throughout the app.

Build the checklist before you need it. The integration checklist for a new chain is most useful the first time you try to add one. Writing it after the fact, under pressure, from memory, is a good way to miss something.

Lessons

Multi-chain is a codebase problem before it is a blockchain problem. The Cosmos chains in this project are similar enough at the protocol level that the engineering differences are manageable. The unsustainable part was five separate repositories, not five separate chains.

Separate repos are a high-overhead way to handle configuration differences. Environment variables and chain-specific constants are not a good reason to fork a codebase. A typed config entry is almost always the right level of separation.

The test matrix becomes explicit when the integration surface does. Before the refactor, it was unclear what needed to be verified when adding a chain. After it, the checklist was the test matrix. That clarity alone made new integrations faster and safer.

The goal is a smaller, predictable, reviewable integration surface. Not zero code per chain. Not a framework that handles every possible variation. Just a model where the cost of the next chain is measurable, bounded, and the same as the one before it.