PolyPay is a privacy-preserving payroll platform built on Horizen blockchain. It enables organizations, DAOs, and global teams to run payroll privately using zero-knowledge proofs (Noir circuits). Key features: private payments, private multisig approvals, escrow/milestone-based transfers, real-time notifications via WebSocket, and JWT authentication.
Monorepo (Yarn Workspaces, Node.js >= 20.18.3)
| Layer | Stack |
|---|---|
| Frontend | Next.js 15, React 19, TypeScript 5.8, Tailwind CSS 4 + DaisyUI 5 |
| State | Zustand 5 (persist middleware), TanStack React Query 5 |
| Forms | React Hook Form 7 + Zod 3 |
| Web3 | wagmi 2, viem 2, RainbowKit 2, Uniswap SDK |
| ZK | @noir-lang/noir_js, @aztec/bb.js |
| UI | Radix UI + shadcn/ui pattern, CVA for variants, Lucide React icons |
| Notifications | Sonner (primary), react-hot-toast (secondary) |
| Real-time | socket.io-client |
| Backend | NestJS 11, TypeScript 5.7, Prisma 7 (PostgreSQL), JWT auth |
| Smart Contracts | Hardhat 2, Solidity, OpenZeppelin 5, Poseidon circuits |
| Shared | @polypay/shared - DTOs with class-validator/class-transformer |
/
├── docker/ # Docker Compose, Dockerfiles, .env.example
├── docs/ # Gitbook documentation
├── packages/
│ ├── nextjs/ # Frontend (Next.js App Router)
│ │ ├── app/ # Pages (dashboard, transfer, batch, contact-book, quest, leaderboard)
│ │ ├── components/
│ │ │ ├── ui/ # Base UI components (shadcn pattern: button, input, dialog...)
│ │ │ ├── form/ # Form components (Form, FormField, FormInput, FormTextarea)
│ │ │ ├── Common/ # Shared components (Sidebar, DisclaimerChecker)
│ │ │ ├── Dashboard/ # Dashboard feature components
│ │ │ ├── Transfer/ # Transfer flow components
│ │ │ ├── Batch/ # Batch operation components
│ │ │ ├── NewAccount/ # Account creation components
│ │ │ ├── modals/ # Modal dialogs (~22 modals, lazy-loaded via ModalRegistry)
│ │ │ ├── skeletons/ # Loading skeleton components
│ │ │ └── icons/ # Custom icon components
│ │ ├── hooks/
│ │ │ ├── api/ # React Query hooks (useTransaction, useAccount, useNotifications...)
│ │ │ ├── app/ # App logic hooks (useAuth, useGenerateProof, useSocketEvent...)
│ │ │ ├── form/ # Form hooks (useZodForm)
│ │ │ └── scaffold-eth/ # Scaffold-eth hooks
│ │ ├── services/
│ │ │ ├── api/ # Axios API services (apiClient, authApi, transactionApi...)
│ │ │ ├── store/ # Zustand stores (useIdentityStore, useAccountStore...)
│ │ │ ├── web3/ # Web3 utilities
│ │ │ ├── socket/ # Socket.io client
│ │ │ └── queryClient.ts # React Query config
│ │ ├── utils/ # Utilities (formatError, errorHandler, signer, network...)
│ │ ├── lib/form/ # Form schemas (Zod) and validation helpers
│ │ ├── types/ # TypeScript types (modal, form, abitype)
│ │ ├── constants/ # Constants (API_BASE_URL, timing)
│ │ ├── configs/ # Route config, scaffold config
│ │ └── contracts/ # Contract ABIs
│ ├── backend/ # NestJS Backend
│ │ └── src/
│ │ ├── auth/ # JWT authentication module
│ │ ├── account/ # Multi-sig account management
│ │ ├── transaction/ # Transaction CRUD & voting
│ │ ├── user/ # User management with ZK commitments
│ │ ├── notification/ # Real-time notifications (WebSocket)
│ │ ├── quest/ # Quest/gamification system
│ │ ├── admin/ # Admin analytics
│ │ ├── zkverify/ # ZK proof verification (Horizen)
│ │ ├── common/ # Shared utilities & middleware
│ │ ├── database/ # Prisma module
│ │ └── ... # ~24 feature modules total
│ ├── shared/ # Shared DTOs and types (@polypay/shared)
│ └── hardhat/ # Smart contracts & deployment
# Root level
yarn lint # ESLint (frontend + hardhat)
yarn next:check-types # TypeScript type checking (frontend)
yarn next:build # Production build (frontend)
yarn build # Build all packages (shared → backend → frontend)
yarn test # Run hardhat tests
yarn format # Prettier format all packages
# Frontend (packages/nextjs)
yarn dev # Dev server (port 3000)
yarn build # Next.js production build
yarn lint # ESLint
yarn check-types # TypeScript check
yarn format # Prettier
# Backend (packages/backend)
yarn start:dev # Dev server with watch (port 4000)
yarn lint # ESLint
yarn format # Prettier
yarn test # Jest unit tests
yarn test:cov # Jest with coverage
yarn test:e2e # End-to-end tests
yarn test:e2e:staging # Staging E2E tests
# Smart Contracts (packages/hardhat)
yarn chain # Local Hardhat node
yarn compile # Compile contracts
yarn deploy # Deploy contracts
yarn test # Hardhat tests- Components: PascalCase files and exports (
FormField.tsx,NotificationPanel.tsx) - Hooks: camelCase with
useprefix (useZodForm.ts,useTransaction.ts) - Utils/Services: camelCase (
apiClient.ts,formatError.ts) - Types/Interfaces: PascalCase (
ModalProps,IdentityState) - Stores: camelCase with
useprefix (useIdentityStore.ts,useAccountStore.ts) - API services: camelCase with
Apisuffix (transactionApi,authApi) - Constants: UPPER_SNAKE_CASE (
API_BASE_URL,DEFAULT_PAGE_SIZE)
- React / Next.js
- Third-party libraries
- @heroicons
- @polypay/shared
- Local imports (
~~/...)
- Print width: 120
- Tab width: 2 spaces
- Arrow parens: avoid
- Trailing comma: all
- Path alias:
~~/*→ project root,@polypay/shared→ shared package
- "use client" directive for client-side components
- Functional components with hooks
- Named exports for UI components, default exports for pages
- Props interfaces defined inline in component files
- Source of truth:
packages/nextjs/styles/tokens.ts— all color definitions live here - Sync script:
packages/nextjs/styles/sync-colors.ts— generates CSS variables intoglobals.cssand Tailwind color config intotailwind.config.ts - Command:
yarn sync-colors(frompackages/nextjs) - Flow: Edit
tokens.ts→ runyarn sync-colors→ CSS vars + Tailwind config auto-updated - Never edit colors directly in
globals.cssortailwind.config.ts— they will be overwritten by the sync script
// services/store/useIdentityStore.ts
// All stores use create() with persist middleware for localStorage
const useIdentityStore = create<IdentityState>()(
persist(
(set) => ({
accessToken: null,
// ...state and actions
setTokens: (access, refresh) => set({ accessToken: access, refreshToken: refresh }),
logout: () => set({ accessToken: null, isAuthenticated: false }),
}),
{ name: "identity-storage" }
)
);
// Use getState() for sync access outside React (e.g., in Axios interceptors)// services/api/transactionApi.ts
// Object export pattern with typed async methods
export const transactionApi = {
getAll: async (params) => { const { data } = await apiClient.get(...); return data; },
create: async (dto) => { const { data } = await apiClient.post(...); return data; },
};// hooks/api/useTransaction.ts
// Query key factory pattern for cache invalidation
const transactionKeys = { all: ["transactions"], list: (filters) => [...] };
// useInfiniteQuery for pagination, useMutation with onSuccess invalidation// useZodForm hook → Form component → FormField with Controller
const form = useZodForm({ schema: contactSchema });
// Zod schemas in lib/form/schemas.ts, reusable validators in lib/form/validation.ts// utils/formatError.ts - pattern matching for friendly error messages
// utils/errorHandler.ts - ErrorCode enum, parseError(), handleError()
// Axios interceptor handles 401 auto-refresh with token rotationimport { notification } from "~~/utils/scaffold-eth";
notification.success("Transaction submitted!");
notification.error(formatErrorMessage(error));// hooks/app/useSocketEvent.ts - wrapper with auto-cleanup on unmount
useSocketEvent("transaction:updated", (data) => { /* handle */ });// Lazy-loaded via ModalRegistry with dynamic imports to prevent SSR issues
// components/modals/ - ~22 modal components// hooks/api/useAuthenticatedQuery.ts
// Wrapper that auto-disables queries when user is not authenticated// configs/routes.config.ts - centralized route definitions
// hooks/app/useAppRouter.ts - type-safe navigation (goToDashboard, goToTransfer, etc.)- Node.js >= 20.18.3
- Yarn (workspaces)
- Docker & Docker Compose (for PostgreSQL)
- Use
@polypay/sharedDTOs for API contracts between frontend and backend - Use
useZodFormhook for all forms with Zod schemas - Use
notification.success/error/infofor user feedback - Use
formatErrorMessage()for user-friendly error messages - Use
useAuthenticatedQuerywrapper for queries requiring auth - Use query key factory pattern for React Query cache management
- Use
useSocketEventhook for real-time subscriptions (auto-cleanup) - Use
~~/*path alias for local imports - Use Zustand
persistmiddleware for state that needs to survive refresh - Use
apiClient(configured Axios instance) for all API calls - Follow feature-based component organization
- Use Radix UI + CVA for new UI components (shadcn pattern)
- Don't import from
@polypay/shareddirectly in smart contracts - Don't make API calls without going through
apiClient(it handles auth tokens) - Don't create new notification systems - use existing
notificationutility or Sonner - Don't hardcode API URLs - use
API_BASE_URLfrom constants - Don't skip Zod validation for forms - always define schemas
- Don't store auth tokens manually - use
useIdentityStore - Don't create new Zustand stores without
persistmiddleware unless state is truly ephemeral - Don't use
useQuerydirectly for authenticated endpoints - useuseAuthenticatedQuery - Don't put business logic in components - extract to custom hooks in
hooks/app/ - Don't use inline styles - use Tailwind CSS classes
- Don't commit
.envfiles or expose secrets