DO NOT ADD, REMOVE, OR MODIFY COMMENTS IN CODE — including punctuation and formatting in existing comments. Only touch comments if explicitly asked.
After ANY code changes, you MUST run verification (see Verification section). Do not report a task as complete or move on until all checks pass.
Leather is a Bitcoin & Stacks wallet — browser extension, mobile app (Expo/React Native), and web app.
Turborepo monorepo using pnpm, organized in CLEAN architecture layers. Use the monorepo-navigation skill for detailed package structure and decision trees.
Presentation: apps/ (extension, mobile, web), @leather.io/ui, @leather.io/features
Application: @leather.io/queries (React Query), @leather.io/services (orchestration, API calls, DI, caching)
Domain: @leather.io/models (types), @leather.io/bitcoin, @leather.io/stacks
State: @leather.io/state (Redux Toolkit slices for shared state)
Foundation: @leather.io/utils, @leather.io/constants, @leather.io/tokens, @leather.io/crypto
- State management: Redux Toolkit. Extension uses
{feature}.slice.ts; mobile uses{feature}.write.ts(slice + actions) and{feature}.read.ts(selectors + hooks). - Server state: React Query (
@tanstack/react-query). Queries live inpackages/queries/and app-levelsrc/queries/. - Feature flags: LaunchDarkly with
camelCaseflag keys in extension; also used in mobile.
Use the monorepo-navigation skill for the full decision tree on where new code goes.
First-time setup:
pnpm i && pnpm buildRun extension:
pnpm devRun web:
pnpm devRun mobile:
cd apps/mobile
pnpm 1password:env:dev # requires 1Password CLI — or ask developer to add EXPO_PUBLIC_LAUNCH_DARKLY to .env
pnpm iosMobile requires an apps/mobile/.env file with EXPO_PUBLIC_LAUNCH_DARKLY set. Run pnpm 1password:env:dev to generate it, or ask the developer to provide the LaunchDarkly key.
- Mobile app uses Expo with EAS Build. pnpm has known fingerprint-drift issues with EAS.
- SDK upgrades: check the official upgrade guide for deprecated/renamed APIs before starting.
- Firebase native modules: use
forceStaticLinkinginexpo-build-propertiesplugin. - Clear bundler cache:
npx expo start --dev-client --clear. BUILD_TARGETenv var controls platform builds:mobile,extension, orweb. Unset builds everything.
- Don't use enums.
- Default to
interfacefor object shapes. Name component propsComponentNameProps. - Use
functiondeclarations for top-level functions and React components. Arrow functions for callbacks only. - Destructure props directly in the function signature.
- Prefer Remeda (
keys,entries,pipe,filter, etc.) overObject.keys/Object.entriesfor typed utilities and non-trivial transforms. Use native methods for trivial cases. - No
ascasts,!non-null assertions, orany. Use runtime checks, type guards, orunknownwith narrowing. - Prefer
constoverlet. Prefer named constants over magic numbers or strings. - No nested ternary expressions.
- Use object method shorthand syntax in objects and interfaces (
{ foo() {} }not{ foo: () => {} }). - camelCase for file-level constants; SCREAMING_SNAKE_CASE in the constants package or
constants.tsfiles.
throwis acceptable for genuinely invalid states (wrong keychain type, missing required config).- For expected failure paths (user input, optional lookups), prefer returning
null,undefined, or typed result objects. - Never throw in React render paths, reducers, or selectors.
- In React render paths: use error boundaries for unexpected errors; return
nullor fallback UI for expected empty states.
- Kebab-case file names (e.g.,
alternate-header-layout.tsx). - Platform suffixes for cross-platform code:
.web.tsx,.native.tsx,.shared.ts. - Convention-named config files are exempt (e.g.,
babel.config.cjs,tsconfig.json). - No
index.ts(x)except barrel exports from library packages or file-based router requirements. - Use
*.spec.ts(x)for tests, co-located next to the file under test.
- Never import from a barrel export (
index.ts) within the same package's sub-modules. - Place
initialStatein write/slice modules, not shared read modules. - Metro require cycle warnings are bugs to fix, not warnings to ignore.
- Concrete anti-pattern: slice → utils → store → slice. Break by keeping
initialStatein write/slice files and never importing fromstore/index.tswithin slices.
- Sanitize HTML from external sources (NFT metadata, collectible descriptions) before rendering.
- Validate responses from IPFS gateways and untrusted origins.
- Never expose private keys, seeds, or mnemonics in error messages or logs.
- Conventional commits format with scope:
feat(mobile),refactor(web),fix(utils). - Imperative language. No body unless explicitly asked.
- Branches and PRs are always based against
dev, notmain.
You MUST run these after any code changes. Do not consider a task complete until they pass:
pnpm format
pnpm lint
pnpm typecheckIf working on mobile, run pnpm lingui from apps/mobile/ before running verification.
For faster feedback in a specific package:
pnpm --filter @leather.io/{package} lint
pnpm --filter @leather.io/{package} typecheck- Turborepo +
pnpm. - Vitest for unit/integration tests. Use the
testing-patternsskill for conventions. - Playwright for E2E tests (extension). Avoid
force: true— it hides accessibility issues. Never nest interactive elements.
- Add a UI component →
@leather.io/ui, use themonorepo-navigationskill - Add a Redux slice → Extension:
{feature}.slice.ts; Mobile:{feature}.write.ts+{feature}.read.ts - Add a React Query hook →
packages/queries/or app-levelsrc/queries/ - Run a single package's tests →
pnpm --filter @leather.io/{package} test:unit