
Six Modules, One Identity: Architecting a Web3 Super App

Building a Web3 super app means one codebase, one wallet, one identity — and six modules that each need to work independently while sharing state. This post walks through the architecture decisions that actually matter when you're doing this at production scale.
What Does "Super App" Actually Mean in a Web3 Context?
In Web2, super apps like WeChat bundle messaging, payments, and mini-programs under one authenticated session. The user never re-logs in. Web3 adds a layer: the wallet is the identity, and every module needs to read from and write to on-chain state without forcing the user to re-sign constantly.
The six modules a typical Web3 super app bundles are: a DEX or swap interface, an NFT marketplace, a portfolio tracker, a governance dashboard, a staking interface, and a social or messaging layer. Each of these is, on its own, a full product. Getting them to share identity, session state, and gas context without stepping on each other is the real engineering problem.
How Do You Share Identity Across Six Isolated Modules?
This is where most teams go wrong early. They treat wallet connection as a component-level concern. Each module connects to MetaMask or WalletConnect independently. The user ends up signing four separate connection requests before they've done anything useful.
The fix is a singleton identity layer that sits above all six modules.
The Identity Service
The identity service holds exactly one wallet session. It exposes three things: the connected address, the active chain ID, and a signing function. Modules never call window.ethereum directly. They call the identity service.
In practice, this is a context provider if you're in React, or a Zustand/Jotai store if you want modules to be more independently deployable. The signing function wraps EIP-712 typed data signing, so every module gets consistent structured signatures rather than raw eth_sign (which wallets increasingly warn against).
Session persistence across page reloads uses localStorage for the address and chain, plus a re-connection handshake on mount. You don't store private keys. You store enough state to re-establish context without prompting the user again.
Cross-Module Authorisation
Permissions per module matter when you have institutional users. A portfolio-only user should not trigger DEX approval flows. We've used a simple on-chain role registry (an ERC-2535 Diamond facet works well here) to store module permissions against an address. The identity service reads this on connection and sets a permissions bitmap that every module checks before rendering sensitive actions.
The Module Boundary Problem: Micro-Frontends vs. Monorepo
If you go micro-frontend (Module Federation in Webpack 5, or its Vite equivalent @originjs/vite-plugin-federation), each module ships independently. A team can deploy the staking module without touching the DEX. That sounds good. The trade-off is that shared dependencies get complicated fast. React must be a singleton. So does your identity service, your Web3 provider, and your design system.
A monorepo with Turborepo and strict package boundaries gives you most of the organisational benefit with fewer runtime surprises. Shared packages live in packages/, each module in apps/modules/, and you enforce boundaries with ESLint's import/no-restricted-paths. The build pipeline becomes more complex, but you're not debugging why two versions of ethers.js are hydrating simultaneously.
| Approach | Independent deploys | Shared singleton risk | Team autonomy | Recommended for |
|---|---|---|---|---|
| Module Federation | Yes | High | High | 4+ teams, strict ownership |
| Monorepo (Turborepo) | With CI discipline | Low | Medium | 1–3 teams, shared codebase |
| Single SPA | Yes | Medium | Medium | Legacy migration path |
For most Series A teams, the monorepo wins. Module Federation pays off when you have genuinely separate teams who cannot coordinate deploys.
/// Not sure where to start?
Get the architecture before you commit
Tell us what you're building and we'll map the technical approach, stack, and rough timeline. No cost, no obligation, no sales call required.
How Do You Handle Gas and Transaction State Across Modules?
Each module generates transactions. The staking module stakes. The DEX swaps. The governance module submits votes. If a user has three pending transactions from three modules and the gas price spikes, you need a single transaction queue, not three separate ones.
Build a transaction manager service alongside the identity service. It holds a queue of pending, confirmed, and failed transactions. Each module submits a transaction request object (target contract, ABI-encoded calldata, value, gas estimate) rather than calling sendTransaction directly. The manager batches where possible using EIP-5792 wallet batch calls, handles nonce management, and surfaces a unified notification layer.
Gas estimation deserves its own attention. Use eth_estimateGas with a 20% buffer as a floor, but also integrate a mempool API (Blocknative or Alchemy's Gas Manager) for priority fee recommendations. Hard-coded gas limits will break your modules during high-congestion periods.
State Synchronisation After Confirmation
On-chain state changes need to propagate back to all modules that care about them. A user stakes tokens; the portfolio tracker needs to update. The naive approach is polling. It works but it's wasteful.
Use WebSocket subscriptions via eth_subscribe for logs on contracts your modules care about. Filter by topic0 (the event signature hash). When a relevant event lands, publish to an internal event bus (a simple EventEmitter or a more structured solution like mitt). Modules subscribe to the bus, not to the chain directly. This decouples your modules from RPC provider specifics and lets you swap providers without touching module code.
Handling Multi-Chain State Without Making the User Think About It
A DEX may operate on Arbitrum. Staking may be on Ethereum mainnet. NFTs may live on Polygon. The user should not be context-switching chain IDs manually.
The identity service tracks the active chain. Each module declares its required chain in a config object. When a module mounts, it checks the active chain against its requirement and, if they differ, triggers a silent wallet_switchEthereumChain call. If the user's wallet doesn't support that chain, it falls back to the wallet_addEthereumChain prompt.
The trickier case is when a user action spans chains — bridging is the obvious example. For that, you need to model the transaction as a multi-step flow with explicit state at each step: source chain approval, bridge submission, destination chain confirmation. Don't try to hide this complexity from the user. Show them where their assets are in transit. Hiding it creates support tickets.
Conclusion
The architectural decisions that define a Web3 super app aren't about smart contract design. They're about the seams between modules: how identity flows, how transactions queue, how chain context propagates, and how events synchronise state without polling.
If you're starting from scratch, get the identity service and transaction manager right before you write a single module. Retrofitting a singleton identity layer into six modules that already talk to MetaMask independently is painful work.
If you're mid-build and the seams are already messy, the cleanest path is to extract the identity service first and route all wallet calls through it. The transaction manager can follow. Doing it the other way around adds complexity before you've solved the harder problem.
We're happy to review your current architecture and tell you where the fault lines are before they become production incidents.
FAQ
Can a Web3 super app work without a centralised identity service?
Technically yes, but users will sign multiple connection requests per session. Every module that calls window.ethereum independently creates a separate handshake. A singleton identity layer is not optional for a good user experience — it's the baseline that makes multi-module apps feel coherent rather than stitched together.
Which wallet connection libraries are production-ready for a multi-module setup? RainbowKit backed by wagmi (v2) and viem handles multi-chain well and has a stable hook API. WalletConnect v2 supports session namespaces across chains in one connection. Avoid building on ethers.js providers directly at the UI layer — abstract them behind your identity service so the underlying library is swappable.
How do you handle users who switch wallets mid-session?
Listen for the accountsChanged and chainChanged events on the provider. On either event, reset the identity service state, clear the transaction queue of any pending items tied to the old address, and re-run the connection handshake. Modules that hold local address state need to re-derive it from the identity service on these events, not from their own cache.
Is Module Federation worth the added complexity for a Web3 app? Only if you have four or more teams who cannot coordinate releases. The main risk is duplicate singleton instances — two copies of your Web3 provider or identity service running simultaneously. If you do use Module Federation, mark shared packages as singletons explicitly in the Webpack config and test wallet interactions across module boundaries in CI.
What's the right data layer for cross-module portfolio state? A GraphQL API backed by The Graph Protocol for historical and indexed data, combined with your WebSocket event bus for real-time updates. The Graph gives you fast, structured queries over on-chain history without running your own indexer. For user-specific off-chain data (labels, notes, preferences), a standard Postgres instance behind a REST or GraphQL API is fine.
Have a project in mind? Contact Sodio Technologies to discuss your requirements and explore the right technology solution for your business.
/// Work with us
Talk to the engineers who'd build it
You'll get a technical scope, timeline and cost estimate from the people doing the work, not an account manager. In-house team, no subcontracting, since 2016.
