What “fullstack” actually means when there is a chain underneath
A contract is not a product. A smart contract replaces one slice of a backend — the settlement layer — and the gap between a deployed contract and something a person can use is mostly ordinary software engineering.
Indexing pipelines · transaction lifecycle · RPC resilience · data topology
Executive summary
In traditional web development a backend database handles reads, writes, business logic and authorisation inside a controlled, private environment. In a decentralised application the smart contract replaces only a small fraction of that stack: the execution layer for financial and consensus-critical state.
Because blockchains are deliberately slow, public, and optimised for verification rather than query performance, a fullstack Web3 application needs an extensive conventional infrastructure layer to make on-chain state usable, responsive and resilient.
┌─────────────────────────────────────────────────────────┐
│ User interface │
│ (wallet state, optimistic UI, multi-chain context) │
└────────────────────────────┬────────────────────────────┘
│
┌─────────────────────┴─────────────────────┐
▼ ▼
┌──────────────┐ ┌─────────────────┐
│ Write path │ │ Read path │
└──────┬───────┘ └────────┬────────┘
│ │
▼ ▼
┌──────────────┐ ┌─────────────────┐
│ Wallet / RPC │ │ Subgraph / DB │
│ orchestrator │ │ (Postgres/GQL) │
└──────┬───────┘ └────────▲────────┘
│ │
│ (signed tx) │ (ingest/index)
▼ │
┌──────────────────────────────────────────────────┴──────┐
│ Blockchain / smart contract │
└─────────────────────────────────────────────────────────┘The read path: indexing and aggregation
Reading state directly from an EVM RPC node does not scale to a modern interface. Nodes are optimised for block consensus, not relational queries.
Where direct RPC runs out
- No complex filtering — you cannot natively ask for "every ERC-721 owned by user X, minted in the last 30 days, with attribute Y".
- Rate limits and cost — querying historical logs across thousands of blocks through a public or paid endpoint means throttling, a runaway bill, or both.
- Latency — node responses take hundreds of milliseconds each, and a page needing dozens of state checks compounds that into something users feel.
What you have to build instead
- Event ingestion — block listeners stream on-chain logs and state updates into a queue such as Kafka or BullMQ.
- Transform and store — a processing service decodes ABIs, parses raw log data and writes relational records into Postgres, ClickHouse or a GraphQL graph.
- Caching — Redis absorbs high-frequency reads such as balances and metadata, keeping client load times under 100ms.
The write path: transaction lifecycle management
Writing to a chain is asynchronous, non-deterministic, and can fail at several independent points. The interface has to model every stage explicitly rather than treating submission as success.
[Idle] ──► [Prompted in wallet] ──► [Broadcast / pending] ──► [Confirmed]
│ │ │
▼ ▼ ▼
[Rejected] [Replaced / dropped] [Reorged]- Mempool stalls — a transaction with a low gas fee can sit indefinitely. The app needs speed-up (resubmit at a higher fee with the same nonce) and cancellation paths.
- Replaced transactions — when a user speeds up or cancels in their wallet, the original hash becomes invalid. Track nonces, not just hashes.
- Reorgs — a block containing a confirmed transaction can be discarded several blocks deep. Financial actions need a confirmation threshold before off-chain state moves.
- Optimistic updates — rather than making users wait twelve seconds, apply state locally and keep the rollback path for when it drops.
The client edge: wallet and chain reality
The Web3 client runs in an unusually unstable environment. The wallet is an external extension or mobile app over which you have no control, and every one of its failure modes surfaces in your interface.
| Edge case | Consequence | Mitigation |
|---|---|---|
| Wrong network | Contract calls fail silently or revert | Detect chain ID on init; prompt a switch via wallet_switchEthereumChain |
| RPC outage | The app stops fetching on-chain data entirely | Client-side fallback providers with round-robin retry |
| Insufficient gas | The transaction fails before the user even signs | Simulate with eth_estimateGas before prompting, and warn early |
| User rejection | An unhandled promise rejection breaks the flow | Intercept code 4001 explicitly and reset local state quietly |
Data topology: what actually belongs on-chain
Putting everything on-chain is prohibitively expensive and creates real privacy problems. The architecture needs an explicit rule for where each class of data lives.
┌──────────────────────┐
│ Data classification │
└──────────┬───────────┘
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ On-chain │ │ Decentralised storage│ │ Traditional storage │
│ (Solidity/Vyper)│ │ (IPFS / Arweave) │ │ (Postgres/S3/Redis) │
└─────────────────┘ └──────────────────────┘ └─────────────────────┘
• Core balances • Asset media (NFTs) • User profiles
• Settlement logic • Immutable metadata • Search indexing
• Governance rules • Static dApp builds • Notification queuesDecoupling with EIP-712 signatures
To cut gas and transaction fatigue, modern architectures push as much as possible into off-chain cryptographic signatures:
- Users sign structured, human-readable messages off-chain, for zero gas.
- The application stores those signatures off-chain — order books, gasless approvals.
- Signatures settle on-chain in bulk, or lazily, only when settlement is genuinely required.
Conclusion
A smart contract is a secure settlement engine, not a product.
Building a working dApp means wrapping that settlement engine in ordinary software engineering: query indexing, asynchronous event processing, resilient RPC orchestration and defensive interface design. Skip those layers and you ship a working protocol nobody can actually use.