A repository template / starter-kit for building decentralized applications (dApps). Built by BootNode based on 5+ years of dApp development. Docs: https://docs.dappbooster.dev/ Components: https://components.dappbooster.dev/
System architecture, data flow, provider hierarchy, and structural conventions are documented in architecture.md. Read it when working on tasks that involve the system's structure or patterns.
- Node 24.15.0+ (enforced via
engines.nodein package.json;.nvmrcnames the version we develop and test on).pnpm-workspace.yamlsetsengineStrict: true, sopnpm installrefuses to run on anything older. - pnpm 12.4.2 (enforced via
packageManagerin package.json; corepack will block npm/yarn)
pnpm install(postinstall automatically runspnpm wagmi-generate)cp .env.example .env.local- Edit
.env.local:PUBLIC_APP_NAMEis mandatoryPUBLIC_WALLETCONNECT_PROJECT_IDis needed for wallet connection to work- RPC vars (
PUBLIC_RPC_*) are optional -- wagmi falls back to default public RPCs - Subgraph vars are all-or-nothing: if ANY
PUBLIC_SUBGRAPHS_*var is missing or empty, codegen will skip and the app will crash at runtime. Either set them all or remove all subgraph-related code.
pnpm subgraph-codegen(only if subgraph vars are configured)pnpm dev
Three hooks run automatically and will block on failure:
- pre-commit: lint-staged in two passes, then gitleaks on the staged changes. The first pass (
.lintstagedrc.format.mjs) writes: Biome formats and fixes. The second (.lintstagedrc.mjs) only reads: Vitest runs the tests related to the staged files. They are split because lint-staged runs globs concurrently, so a single pass let a reformat land while another task was reading the same file. - commit-msg: commitlint enforces conventional commit format. Valid types:
feat,fix,docs,test,ci,refactor,perf,chore,revert,style,build,hotfix,wip,release. PR titles are also validated via CI. - pre-push:
pnpm typecheck, then gitleaks over the commits being pushed. The second scan is the backstop for a bypassed pre-commit.
Use Conventional Commits:
Format: type(scope): subject
- Scope is optional:
feat: add loginandfeat(auth): add loginare both valid - Subject uses imperative mood, lowercase after the colon, no trailing period
- Body (optional): separated by a blank line, explains what and why
Prefixes:
| Prefix | Purpose |
|---|---|
feat |
New feature |
fix |
Bug fix |
chore |
Maintenance, dependencies, config |
docs |
Documentation only |
refactor |
Code change that neither fixes a bug nor adds a feature |
test |
Adding or updating tests |
style |
Formatting, whitespace, semicolons |
ci |
CI/CD pipeline changes |
perf |
Performance improvement |
build |
Build system or external dependencies |
revert |
Reverts a previous commit |
hotfix |
Urgent fix that bypasses the normal release cycle |
wip |
Work in progress (avoid on main) |
release |
Release-related changes |
-
mainis the only long-lived branch. Branch off it, open the PR against it, delete the branch after merge. There is nodevelop. -
Releases are a tag on
mainplus a GitHub release, not a separate branch -
Every PR must reference an issue (
Closes #N)No related issue? Use
No related issue.as the first line of the Summary section. -
Mirror the issue's acceptance criteria in the PR
-
Self-review your diff before requesting peer review
-
Keep PRs small and focused -- one issue, one PR
-
PR titles use the same conventional commit format (
feat: add user dashboard) -
Use
/sdlc:create-prto create PRs -- it reads the template and fills every section automatically
GitHub form dropdowns (like the Priority field in issue templates) only work through the web UI. When issues are created via gh CLI or REST API, dropdown values become unstructured body text -- not queryable, not consistent. Labels are the API-reliable mechanism for structured metadata.
Priority (bugs, features, and epics):
| Label | Description |
|---|---|
priority: critical |
Blocking work, system down, or security issue |
priority: high |
Must be addressed in current sprint |
priority: medium |
Should be addressed soon |
priority: low |
Nice to have, can wait |
Labels are queryable: gh issue list --label "priority: high".
The /sdlc:create-issue skill applies these labels automatically when creating issues via CLI. Bug, feature, and epic templates include a Priority dropdown for web UI users, but labels are the source of truth for programmatic workflows.
- No semicolons in TypeScript/JavaScript
- Single quotes
- 2-space indentation, 100 char line width
- Import organization handled by Biome
- Path aliases:
@/src/*and@packageJSON - All env vars prefixed with
PUBLIC_and validated insrc/env.ts - JSDoc comments on exported functions/components (follow existing patterns)
- Chakra UI props are the primary styling method. No CSS modules, no Tailwind, no styled-components.
- Theme is defined in
src/components/ui/provider.tsxusingcreateSystemextendingdefaultConfig. - Use semantic tokens for colors:
bg,primary,text,danger,ok,warning. These support light/dark mode. Do not hardcode color values. - Fonts: Manrope (body/heading), Roboto Mono (mono/code).
- Complex components export style objects from a
styles.tsfile, consumed via Chakra'scssprop or spread into component props.
- Simple components: single file (e.g.,
ExplorerLink.tsx,SwitchNetwork.tsx) - Complex components: folder with:
index.tsx-- main component and public APIComponents.tsx-- sub-components (styled primitives, layout pieces)styles.ts-- style objectsuseComponentName.ts-- dedicated hook (optional)
- Page components go in
src/components/pageComponents/<pagename>/ - Shared/reusable components go in
src/components/sharedComponents/
- Save ABI in
src/constants/contracts/abis/YourContract.ts(export as const) - Register in
src/constants/contracts/contracts.tswith name, ABI, and addresses per chain - Run
pnpm wagmi-generateto auto-generate typed hooks insrc/hooks/generated.ts
The contracts array uses as const satisfies ContractConfig<Abi>[] for full type inference. Follow this pattern.
Use getContract(name, chainId) to retrieve a typed contract config at runtime (e.g., getContract('WETH', sepolia.id) returns { abi, address }).
- Create route file in
src/routes/using.lazy.tsxextension for code-split lazy loading (e.g.,yourpage.lazy.tsx). Use plain.tsxonly for routes that must be eagerly loaded. - Create page component in
src/components/pageComponents/yourpage/ - Route tree auto-generates via TanStack Router plugin
- Import chain from
viem/chainsinsrc/lib/networks.config.ts - Add to the appropriate chain array (devChains or prodChains)
- Add transport entry with optional custom RPC from env
- Add RPC env var to
src/env.tsif needed
- Defined and validated with Zod in
src/env.ts - Access via
import { env } from '@/src/env'(neverimport.meta.envdirectly) - New variables MUST be added to both
.env.exampleandsrc/env.ts
These files are gitignored and regenerated from source:
src/hooks/generated.ts-- regenerate withpnpm wagmi-generatesrc/routeTree.gen.ts-- regenerate withpnpm routes:generatesrc/subgraphs/gql/-- regenerate withpnpm subgraph-codegen
pnpm-workspace.yaml holds parts of the tree to one version, and renovate.json disables the matching major updates so nobody re-proposes them by accident. Each hold has a reason:
- wagmi stays on 2. connectkit and rainbowkit have no wagmi 3 release.
@reown/appkit-adapter-wagmiandportoleave@wagmi/coreand@wagmi/connectorsas open peers, which pnpm fills with 3.x and 8.x on a fresh resolve, so theoverridesblock pins both to the exact versions wagmi 2 depends on. Two copies means two connector registries and a build that cannot resolve@wagmi/core/tempo. Renovate leaves those two alone entirely: they move when wagmi moves. - graphql stays on 16.
@bootnodedev/db-subgraphasks for^16, and a second copy of graphql in the tree breaks schema identity checks at runtime. - TypeScript stays on 6. TypeScript 7 is the Go-native compiler and no longer ships the JS compiler API typedoc reads, so
pnpm typedoc:buildcannot run on it. @graphql-codegen/cliis overridden to^7so@bootnodedev/db-subgraph, which asks for^5, uses the copy we do.
Renovate itself opens one batched pull request a week, waits three days after a release before proposing it, and never auto-merges. It does nothing until the Renovate GitHub App is installed on the repo.
scripts/install-gitleaks.sh downloads the version named in .gitleaks-version into bin/ (gitignored) and checks its sha256. The git hooks and CI all call it, so everyone runs the same version against the same rules.
Accepted findings live in .gitleaksignore, one fingerprint per line. Today there is one: the published Anvil test account private key, in a test file on an old branch. A new secret produces a new fingerprint and still fails the scan.
pnpm knip reports files, exports and dependencies nothing reaches. knip.json treats the template's public surface as entry points -- shared components, hooks, the wagmi CLI config, the alternative wallet configs -- because those are meant to be unused until a project picks them up.
Unused files, unused dependencies and unlisted dependencies fail. Unused exports, unused types and duplicate named/default exports are reported as warnings: what is left is components and helpers the template offers on purpose, plus the export const X / export default X pair those components use.
- Framework: Vitest with jsdom environment
- Test files: colocated as
*.test.ts/*.test.tsx - Mocking:
vi.mock()for module mocks - Component testing: Testing Library + jest-dom matchers
- Run:
pnpm test(single run) orpnpm test:watch - What to test: Business logic, API integrations, component behavior
- What not to test: Styling, third-party library internals, trivial getters/setters
- Coverage: Aim for meaningful coverage, not a number. Cover the paths that matter.
- Do not commit secrets, API keys, or credentials
- Do not modify CI/CD pipelines without team review
- Do not skip tests or linting to make a build pass
- When in doubt, ask -- don't assume
- Prefer small, focused diffs over broad refactors
- Preserve existing UX unless the task explicitly changes it
- Avoid introducing new patterns when a project pattern already exists
- Update docs only when behavior or workflow changes
Run before declaring work done:
pnpm lintpnpm typecheckpnpm testpnpm knippnpm build(when feasible for runtime-impacting changes)
CI runs the same commands on every pull request, plus a gitleaks scan of the whole history and commitlint over the PR's commits and title. The typecheck job also builds the typedoc reference and the docs site, and is skipped on pull requests from forks because it needs the subgraph secrets.