name: frontend-playbook description: Build-to-production pipeline for Scaffold-ETH 2 dApps. Fork mode, IPFS deployment, Vercel deployment, ENS subdomain setup, custom domains, and production checklist. metadata: author: Andre Ginja version: 1.0.0
Frontend Playbook: Build to Production
This skill covers the mechanics of getting your Scaffold-ETH 2 dApp from a local dev server to a production URL that real users visit. If you skip steps here, your deployment will break in ways that are obvious to users and embarrassing for you.
What You Probably Got Wrong
You ran
yarn chaininstead ofyarn fork. Your local chain has no USDC, no Uniswap, no Chainlink oracles. If your dApp touches ANY existing protocol, you need a fork. Always.You deployed to IPFS without
trailingSlash: true. Every route except/returns a 404. Your users see a blank page on/dashboard,/swap,/profile. This is the single most common IPFS deployment bug.You did not clean the build directory before deploying. Stale
.nextandoutdirectories contain old assets. Your deployment serves a mix of old and new code. Pages randomly break.You set environment variables in
.env.localbut not in Vercel. Your local build works. Your Vercel build fails silently or serves a broken app with undefined config values.You assumed IPFS gateways are fast. Public IPFS gateways are slow. Your dApp takes 10-30 seconds to load on first visit. Pin your content and use a dedicated gateway.
Fork Mode: The Right Way to Develop Locally
When to Use Fork Mode
Use yarn fork instead of yarn chain whenever your dApp interacts with:
- Any ERC-20 token (USDC, WETH, DAI, etc.)
- Any DeFi protocol (Uniswap, Aave, Compound, etc.)
- Chainlink price feeds
- ENS
- Any deployed contract you do not own
Starting a Fork
# Fork Ethereum mainnet
yarn fork --network mainnet
# Fork Base
yarn fork --network base
# Fork Optimism
yarn fork --network optimism
# Fork Arbitrum
yarn fork --network arbitrum
This starts Anvil with a fork of the specified chain at the latest block. All contract state from that chain is available locally.
Using Forked State
On a forked chain, you can impersonate any account:
# In your deploy script or test, impersonate a whale
cast send --unlocked --from 0xBE0eB53F46cd790Cd13851d5EFf43D12404d33E8 \
--to YOUR_ADDRESS \
--value 100ether
You can interact with real Uniswap pools, real Aave markets, real Chainlink feeds — all locally, with zero gas cost and instant confirmation.
Fork Mode Gotchas
Fork state is frozen at the block you forked from. New transactions on the live chain are not reflected. If you need fresh state, restart the fork.
RPC rate limits. Forking makes many RPC calls to the upstream provider. Use an Alchemy or Infura key with reasonable limits. Public RPCs will rate-limit you quickly.
Block timestamps. The forked chain starts at the fork block's timestamp. If your contract has time-dependent logic (vesting, lockups), you may need to advance time:
# Advance time by 1 day
cast rpc evm_increaseTime 86400
cast rpc evm_mine
IPFS Deployment
IPFS deployment gives you censorship resistance. No server can be taken down. The content is addressed by its hash — as long as someone pins it, it is available.
Step 1: Configure Next.js for Static Export
In packages/nextjs/next.config.js:
const nextConfig = {
output: "export",
trailingSlash: true, // CRITICAL — without this, routes return 404 on IPFS
images: {
unoptimized: true, // Required for static export
},
};
Why trailingSlash: true is critical:
Without it, Next.js generates:
/dashboard.html
/swap.html
IPFS serves files by exact path. When a user visits /dashboard, IPFS looks for a file literally named dashboard (no extension). It does not find it. 404.
With trailingSlash: true, Next.js generates:
/dashboard/index.html
/swap/index.html
IPFS resolves /dashboard/ to /dashboard/index.html. It works.
This is the number one IPFS deployment bug. It will waste hours of your time if you forget it.
Step 2: Clean Build
# Remove ALL build artifacts
rm -rf packages/nextjs/.next packages/nextjs/out
# Build the static export
cd packages/nextjs && yarn build
Always clean before deploying. Stale build artifacts cause:
- Old pages appearing alongside new ones
- CSS mismatches (old stylesheet served with new HTML)
- JavaScript errors from version mismatches
- Asset 404s from deleted files still referenced in old manifests
Step 3: Deploy via BuidlGuidl IPFS
yarn ipfs
This uses BuidlGuidl's IPFS pinning service. It:
- Builds your app (static export)
- Uploads the
outdirectory to IPFS - Pins the content so it stays available
- Returns an IPFS CID (Content Identifier)
You will get a URL like:
https://ipfs.io/ipfs/QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco
Step 4: Verify the Deployment
Do not skip this. Open the IPFS URL and verify:
- The root page (
/) loads correctly - Navigate to every route — they should all load
- Wallet connect works
- Contract interactions work
- Images and assets load
- No console errors
Alternative: Manual IPFS Deployment
If you are not using BuidlGuidl's service:
# Install IPFS CLI
npm install -g ipfs-car
# Build
rm -rf packages/nextjs/.next packages/nextjs/out
cd packages/nextjs && yarn build
# Upload to web3.storage, Pinata, or nft.storage
# Using Pinata as example:
npx pinata-cli upload packages/nextjs/out
IPFS Gateway Performance
Public gateways (ipfs.io, gateway.pinata.cloud, dweb.link) are slow. For production:
- Use a dedicated gateway — Pinata, Infura IPFS, or self-hosted
- Pin on multiple services — redundancy prevents downtime
- Use a custom domain — point your domain to the gateway (covered below)
Vercel Deployment
Vercel is the fastest path to production. It handles builds, CDN, SSL, and preview deployments automatically.
Step 1: Connect Repository
- Push your code to GitHub
- Go to vercel.com and import the repository
- Configure the project:
| Setting | Value |
|---|---|
| Framework Preset | Next.js |
| Root Directory | packages/nextjs |
| Build Command | yarn build (or leave default) |
| Output Directory | Leave default (.next) |
| Install Command | yarn install |
Step 2: Environment Variables
In the Vercel dashboard, go to Settings > Environment Variables. Add:
NEXT_PUBLIC_ALCHEMY_API_KEY=your_key_here
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=your_project_id
Do NOT add:
DEPLOYER_PRIVATE_KEY— this is for contract deployment, not frontend- Any secret that should not be in client-side JavaScript
Remember: NEXT_PUBLIC_ variables are embedded in the client bundle. They are visible to anyone. Only put non-secret configuration here.
Step 3: Monorepo Configuration
Scaffold-ETH 2 is a monorepo. Vercel needs to know which package to build.
Create vercel.json in the monorepo root if needed:
{
"buildCommand": "cd packages/nextjs && yarn build",
"installCommand": "yarn install",
"framework": "nextjs",
"outputDirectory": "packages/nextjs/.next"
}
Or configure via the Vercel dashboard — set Root Directory to packages/nextjs.
Step 4: Deploy
# Install Vercel CLI
npm i -g vercel
# Login
vercel login
# Deploy preview
vercel
# Deploy production
vercel --prod
Or just push to main — Vercel auto-deploys on push if you connected the repo.
Vercel Gotchas
Monorepo root: If Vercel builds from the wrong directory, all imports fail. Double-check the root directory setting.
Environment variables must be set in Vercel dashboard, not just in
.env.local. Your local.env.localis not uploaded to Vercel.Build errors: If the build fails on Vercel but works locally, check:
- Node.js version (set in Settings > General)
- Missing env vars
- Case-sensitive imports (Mac is case-insensitive, Linux is not)
Preview deployments: Every PR gets a preview URL. Use this for QA before merging to main.
ENS Subdomain Setup
ENS gives your dApp a human-readable name. Instead of https://my-dapp.vercel.app, users go to mydapp.eth (via ENS-aware browsers) or mydapp.eth.limo (via any browser).
Option 1: Point ENS to IPFS Content
This is the decentralized option. Your frontend lives on IPFS, your name lives on ENS.
Get an ENS name — Register at app.ens.domains
Set the Content Hash:
- Go to your ENS name on app.ens.domains
- Click "Records"
- Set "Content Hash" to your IPFS CID:
ipfs://QmXoypizjW3WknFiJnKLwHCnL72vedxjQkDDP1mXWo6uco - Confirm the transaction
Access via .eth.limo:
- Your dApp is now available at
https://yourname.eth.limo - This works in any browser — no special extension needed
.eth.limois a public gateway that resolves ENS content hashes
- Your dApp is now available at
Update on redeploy: Every time you redeploy to IPFS, you get a new CID. You must update the ENS content hash (onchain transaction, costs gas).
Option 2: Point ENS to a URL (CNAME)
If you are using Vercel or another host:
- Set a Text Record on your ENS name:
- Key:
url - Value:
https://my-dapp.vercel.app
- Key:
This is simpler but less decentralized — your host can go down.
Subdomain Setup
If you own yourbrand.eth, you can create subdomains like app.yourbrand.eth:
- Go to your ENS name settings
- Click "Subnames"
- Create
appsubdomain - Set the content hash on the subdomain
Cost: One onchain transaction for the subdomain creation + one for setting the content hash.
Custom Domain Setup
With Vercel
- In Vercel dashboard: Settings > Domains
- Add your domain (e.g.,
app.yourdapp.com) - Update your DNS:
- A Record:
76.76.21.21 - CNAME:
cname.vercel-dns.com(for subdomains)
- A Record:
- SSL is automatic
With IPFS + Custom Domain
Use a service like Fleek or Cloudflare to proxy your custom domain to IPFS:
- Cloudflare: Add a CNAME record pointing to
cloudflare-ipfs.com - DNSLink: Set a TXT record:
_dnslink.app.yourdapp.com -> dnslink=/ipfs/QmYourCID - Update the TXT record on every redeploy
Go-to-Production Checklist
Run through every item before sharing the URL publicly.
Build Verification
- Clean build:
rm -rf .next outthenyarn buildcompletes without errors - No TypeScript errors
- No ESLint warnings that indicate bugs (unused vars are fine, undefined vars are not)
- Build output size is reasonable (< 5MB for initial load)
Deployment Verification
- Production URL loads (not just the root — check every route)
- IPFS:
trailingSlash: trueis set in next.config.js - IPFS: Content is pinned on at least one reliable service
- Vercel: Environment variables are set in dashboard
- Vercel: Root directory points to
packages/nextjs - Custom domain resolves correctly
- SSL certificate is valid (no mixed content warnings)
Wallet and Network
- Wallet connect works on desktop (MetaMask, Coinbase Wallet)
- Wallet connect works on mobile (WalletConnect, in-app browsers)
- Network switching works — prompts user to switch if on wrong chain
- Correct chain ID configured in
scaffold.config.ts
Contract Integration
-
deployedContracts.tshas the production contract addresses - All contract interactions work on the production chain
- Block explorer links point to the correct explorer for the chain
- Contract is verified on the block explorer
Content and Branding
- Default Scaffold-ETH branding is removed or customized
- App title and meta tags are set
- Favicon is custom (not the Scaffold-ETH default)
- Open Graph image is set for social sharing
- No "localhost" or "test" strings visible in the UI
Security
- No API keys in client-side code (search for "api_key", "apikey", "secret")
- No private keys anywhere in the repository
-
.envfiles are in.gitignore - RPC URLs do not contain embedded keys
- CSP headers configured (if applicable)
Performance
- First page load under 3 seconds on a 3G connection
- No layout shift when wallet connects
- Images are optimized
- Fonts are preloaded or use system fonts
Monitoring
- Error tracking set up (Sentry, LogRocket, or at minimum console.error logging)
- Contract monitored on block explorer (set up alerts for unusual activity)
- Uptime monitoring for Vercel deployments (UptimeRobot, Betterstack)
Common Gotchas Reference
| Problem | Cause | Fix |
|---|---|---|
All routes except / return 404 on IPFS |
Missing trailingSlash: true |
Add to next.config.js |
| Stale pages in production | Did not clean .next and out before build |
rm -rf .next out then rebuild |
| Build works locally, fails on Vercel | Missing env vars or case-sensitive imports | Check Vercel build logs |
| Wallet does not connect | Wrong chain in scaffold.config.ts |
Verify targetNetworks |
| "Module not found" on Vercel | Monorepo root directory wrong | Set root to packages/nextjs |
| Images broken on IPFS | Using Next.js Image optimization | Set images: { unoptimized: true } |
| ENS content hash update is expensive | Mainnet gas for ENS update | Budget 0.005-0.02 ETH per update |
| IPFS loads slowly | Using public gateway | Pin content and use dedicated gateway |
| CSS not loading in production | Tailwind purging too aggressively | Check content paths in Tailwind config |
| API routes return 404 on IPFS | IPFS is static hosting — no server | Use output: "export", move API logic onchain or to a separate backend |
Every one of these has cost someone hours of debugging. Do not learn them the hard way.