A production-ready template for building full-stack decentralized applications with SvelteKit, Hardhat v3, and hardhat-deploy v2 with the rocketh deployment system.
This template extends the template-ethereum-contracts to include a fully configured web frontend, providing:
- Hot Contract Replacement (HCR): The equivalent of HMR (Hot Module Replacement) for smart contracts. Edit your contracts and see changes live while developing your app. This uses proxy patterns with a set of conventions to make it work seamlessly.
- Intuitive Deployment Scripts: Write deployment logic in plain TypeScript without learning a new DSL.
- Flexible Proxy Patterns: Declarative proxy deployment with
deployViaProxyfor upgradeable contracts. - Full Control: Access to all deployment parameters and lifecycle hooks.
- SvelteKit 5: Modern, fast, and reactive frontend framework with Svelte 5.
- Tailwind CSS 4: Utility-first CSS framework for rapid UI development.
- PWA Ready: Pre-configured Progressive Web App with service worker support.
- IPFS Compatible: Static adapter with relative paths for decentralized hosting.
- Auto-Generated Deployments: Contract ABIs and addresses automatically exported to the frontend.
- Zellij Layouts: Multiple pre-configured terminal layouts for different development scenarios.
- Live Reload: Changes to contracts automatically trigger recompilation, redeployment, and frontend updates.
- Type Safety: Full TypeScript support across contracts and frontend.
The non-obvious decisions in this template are written down, with the options that were rejected and why. The code cites them by number, in the form "ADR-0004 (work branch)".
They live on the work orphan branch rather than in the working tree, so that they never cascade into a fork and never conflict during a template merge. Nothing checks them out; read one with git show:
git show work:docs/adr/ # list them
git show work:docs/adr/0004-view-and-system-overlays.md # read oneStart with 0001-capabilities-vs-app-context (how things are passed down the component tree), 0002-synchronous-ssr-inert-app-context (why the app context is synchronous and renders on the server) and 0004-view-and-system-overlays (the two kinds of overlay, and the navigation seam). The same branch holds work/notes/ : findings, observations and open questions accumulated while building this.
.
├── contracts/ # Smart contracts package
│ ├── src/ # Solidity source files
│ │ └── GreetingsRegistry/ # Contract organized by feature
│ │ ├── GreetingsRegistry.sol # Main contract
│ │ └── GreetingsRegistry.t.sol # Solidity tests (forge-style)
│ ├── deploy/ # Deployment scripts
│ ├── deployments/ # Deployment artifacts per network
│ ├── generated/ # Auto-generated artifacts and ABIs
│ ├── rocketh/ # Rocketh configuration
│ │ ├── config.ts # Account & extension configuration
│ │ ├── deploy.ts # Deploy script setup
│ │ └── environment.ts # Environment setup for tests/scripts
│ ├── scripts/ # Utility scripts
│ └── test/ # TypeScript tests
│ └── utils/ # Test utilities
├── web/ # SvelteKit frontend
│ ├── src/
│ │ ├── lib/
│ │ │ ├── core/ # Reusable building blocks, independent of this
│ │ │ │ # app's routes: connection, transaction safety,
│ │ │ │ # capabilities, navigation, overlays, notifications,
│ │ │ │ # service worker, UI primitives, utils
│ │ │ ├── kit/ # The ONLY place that imports $app/* (see its README)
│ │ │ ├── context/ # The app context: what createContext() composes
│ │ │ ├── account/ # Per-account data, operations, connectors
│ │ │ ├── ui/ # This app's UI: navbar, banners, pending operations
│ │ │ ├── view/ # View models derived from onchain + account state
│ │ │ ├── onchain/ # Contract reads
│ │ │ ├── shadcn/ # Vendored shadcn-svelte components
│ │ │ └── deployments.ts # Auto-generated contract deployments
│ │ ├── routes/ # SvelteKit routes
│ │ ├── service-worker/ # PWA service worker
│ │ └── web-config.json # Branding: name, description, icon, links
│ ├── static/ # Static assets
│ └── svelte.config.js # SvelteKit configuration
├── dev/ # Zellij layout configurations
├── package.json # Root monorepo configuration
└── pnpm-workspace.yaml # PNPM workspace definition
pnpm iInstalling also generates web/src/lib/deployments.ts from the deployment records committed to this repo, so that pnpm web:check and the web unit tests work before you have deployed anything. A repo with no committed records gets it from a throwaway deploy instead, onto a chain that exists only for the length of the install (contracts/scripts/deploy-ephemeral.ts): the app type-checks against the real ABIs and deploy parameters, and its addresses are on no chain. That file is gitignored (it names a chain and its addresses, so it belongs to whoever deployed rather than to the repo), and deploying overwrites it: an export for your own chain always wins, and nothing regenerates it while it exists. See scripts/ensure-deployments.mjs.
We recommend installing Zellij for an optimal development experience with pnpm start.
Start everything with a single command (requires Zellij):
pnpm startThis launches:
- A local Ethereum node
- Contract auto-compilation on file changes
- Contract auto-deployment on changes
- Frontend development server with hot reload
- Svelte type checking
If you prefer running services individually:
-
Start the local Ethereum node:
pnpm contracts:node:local
-
In another terminal, compile and deploy:
pnpm contracts:compile pnpm contracts:deploy localhost --skip-prompts pnpm contracts:export localhost --ts ../web/src/lib/deployments.ts
-
Start the web development server:
pnpm web:dev
pnpm contracts:compilepnpm contracts:compile:watchpnpm contracts:testThis runs both:
- Solidity tests (forge-style, using
forge-std) - TypeScript tests (using Node.js test runner with
earlassertions)
-
Configure your environment variables in
.env.local:MNEMONIC_<network>="your mnemonic phrase" ETHERSCAN_API_KEY=<api-key> # For verification
-
Deploy:
pnpm contracts:deploy <network>
pnpm contracts:verify <network>pnpm web:devpnpm web:buildpnpm web:serveBuild contracts and web frontend together:
pnpm build <network>Export contract addresses and ABIs to the frontend:
pnpm contracts:export <network> --ts ../web/src/lib/deployments.tsThis template provides multiple Zellij layouts for different development scenarios:
Runs everything locally:
- Local Ethereum node
- Contract compilation, deployment, and TypeScript build (all watching for changes)
- Web development server
- Svelte type checking
Use when you have contracts already deployed and want to develop the frontend:
- Exports existing deployment info
- Runs web development server
Develop against a remote network (testnet/mainnet):
- Watches and deploys to the remote network
- Runs web development server locally
Configure accounts in contracts/rocketh/config.ts:
export const config = {
accounts: {
deployer: { default: 0 }, // First account from mnemonic
admin: { default: 1 }, // Second account
},
// ...
} as const satisfies UserConfig;Networks are configured in contracts/hardhat.config.ts using helper functions:
addNetworksFromEnv(): Auto-configure networks fromETH_NODE_URI_*environment variablesaddNetworksFromKnownList(): Add configurations for well-known networksaddForkConfiguration(): Enable forking mode viaHARDHAT_FORKenv var
Configure the web app in web/src/web-config.json. This is the single place to rebrand: name drives the landing page hero, the document <title>, the social/meta tags, and the PWA manifest, and pnpm generate-pwa-icons regenerates the icons from icon.
{
"name": "Jolly Roger",
"title": "Jolly Roger",
"description": "Build and Deploy for Eternity",
"canonicalURL": "http://localhost:8080",
"repoURL": "",
"communityURL": "",
"themeColor": "#000000",
"icon": "static/icon.svg"
}Replace icon (web/static/icon.svg) with your own logo; the landing page and every PWA icon derive from it.
repoURL and communityURL add the source and community links to the navbar. Both default to empty, which hides the link: a fork should point at its own repository, not at this template's. Set them here rather than in web/src/routes/+layout.svelte, which is the most-edited file in the template and therefore the most expensive place to park a constant.
The frontend ships several ready-made routes you can keep, adapt, or remove:
- Demo (
/demo) - the canonical read/write-a-contract example (GreetingsRegistry). - Transactions (
/transactions) - the pending-operation / transaction tracker UI. - Contracts (
/contracts) - a generic read/write UI generated from deployed ABIs. - Explorer (
/explorer) - a built-in block/transaction/address explorer.
These are wired only through links in web/src/lib/ui/navbar/navbar.svelte (and the landing page for Demo). To disable a feature, remove its link there; to remove it entirely, also delete its folder under web/src/routes/. The reusable building blocks live in web/src/lib/core/ and are independent of these routes.
Deploy scripts are located in contracts/deploy/ and are executed in order (prefixed with numbers):
import { deployScript, artifacts } from "../rocketh/deploy.js";
export default deployScript(
async (env) => {
const { deployer, admin } = env.namedAccounts;
// Deploy an upgradeable contract
const deployment = await env.deployViaProxy(
"GreetingsRegistry",
{
account: deployer,
artifact: artifacts.GreetingsRegistry,
args: ["prefix:"],
},
{
owner: admin,
linkedData: {
/* metadata stored with deployment */
},
},
);
// Interact with the deployed contract
const contract = env.viem.getContract(deployment);
const message = await contract.read.messages([deployer]);
},
{ tags: ["GreetingsRegistry"] },
);Contract deployments are automatically exported to web/src/lib/deployments.ts. Import them in your Svelte components:
import deployments from "$lib/deployments";
// Access contract address
const address = deployments.contracts.GreetingsRegistry.address;
// Access contract ABI
const abi = deployments.contracts.GreetingsRegistry.abi;| Variable | Description |
|---|---|
ETH_NODE_URI_<network> |
RPC endpoint for the network |
MNEMONIC_<network> |
Mnemonic for account derivation |
MNEMONIC |
Fallback mnemonic if network-specific not set |
ETHERSCAN_API_KEY |
API key for contract verification |
Set SECRET as the value to use Hardhat's secret store:
ETH_NODE_URI_mainnet=SECRET # Uses configVariable('SECRET_ETH_NODE_URI_mainnet')Every one is inlined at build time, so a change needs a rebuild, and every one is PUBLIC: it ships to the browser. Never put a secret or a key-bearing URL in PUBLIC_CHAIN_INFO_NODE_URL, which is handed to the user's wallet.
| Variable | Description |
|---|---|
PUBLIC_NODE_URL |
The app's own RPC. Empty means the app reads the chain only through the connected wallet, and the UI says so instead of reporting a fault. |
PUBLIC_CHAIN_INFO_NODE_URL |
The RPC handed to the WALLET, so it can add/switch to an unknown chain. Deliberately separate from the above, which may be private. |
PUBLIC_WALLET_HOST |
Hosted sign-in service. Empty means wallet-only sign-in, which is a supported configuration and not an error. |
PUBLIC_USE_BURNER_WALLET |
Node URL to run a dev burner wallet against ($PUBLIC_NODE_URL locally). Empty disables it. |
PUBLIC_IMPERSONATE_ADDRESSES |
Comma-separated addresses the burner offers to impersonate. Dev only. |
PUBLIC_USE_INTERNAL_EXPLORER |
true to link addresses and transactions to the built-in /explorer instead of an external block explorer. |
PUBLIC_EXPLORER_BLOCK_INDEX_ENABLED |
true to enable the explorer's block-index listing. |
PUBLIC_ENS_NODE_URL |
Mainnet RPC used for ENS name and avatar lookups. Empty disables ENS resolution, which is a pure enhancement. |
PUBLIC_FAUCET_LINK |
Faucet URL opened in a popup for the user to claim from. |
PUBLIC_FAUCET_API |
Faucet HTTP API, claimed from directly when set. Takes precedence over the link. |
PUBLIC_OPERATION_RETENTION_DAYS |
How long finalized operations stay in local account data. |
PUBLIC_ENABLE_SW_IN_DEV |
true to register the service worker during development, which is off by default because it caches aggressively. |
PUBLIC_ERUDA_PLUGINS |
Mobile console. Substituted into src/app.html at build time rather than read as a module. Empty disables it (fail-closed). |
The contracts package can be published for external consumption:
{
"exports": {
"./deploy/*": "./dist/deploy/*",
"./rocketh/*": "./dist/rocketh/*",
"./artifacts/*": "./dist/generated/artifacts/*",
"./abis/*": "./dist/generated/abis/*",
"./deployments/*": "./deployments/*",
"./src/*": "./src/*"
}
}pnpm contracts:build// Import ABIs
import { Abi_GreetingsRegistry } from "jolly-roger-contracts/abis/GreetingsRegistry.js";
// Import deployment info
import GreetingsRegistry from "jolly-roger-contracts/deployments/sepolia/GreetingsRegistry.json";Solidity linting is configured with slippy:
pnpm contracts:lintpnpm format # Format all code
pnpm format:check # Check formatting