This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Ecency Vision is a Next.js 15-based web client for the Hive blockchain, organized as a pnpm monorepo workspace. The codebase consists of a main Next.js application (apps/web), a self-hosted blog SPA (apps/self-hosted), and four publishable packages (@ecency/sdk, @ecency/wallets, @ecency/render-helper, @ecency/ui).
# Install dependencies (run from workspace root)
pnpm install
# Start main app dev server
pnpm dev
# or: pnpm --filter @ecency/web dev
# Start package dev servers (watch mode)
pnpm dev:sdk
pnpm dev:wallets
pnpm dev:ui
# Start self-hosted app dev server
pnpm dev:self# Build main app
pnpm build
# or: pnpm --filter @ecency/web build
# Build all packages
pnpm build:packages
# or: pnpm -F @ecency/sdk,@ecency/wallets,@ecency/render-helper build
# Build production server
pnpm start# Run tests (all use Vitest)
pnpm test
# Lint all packages
pnpm lint
# Type check
pnpm typecheck# For main app (Vitest)
pnpm --filter @ecency/web test -- path/to/test.spec.tsx
# For packages (Vitest)
pnpm --filter @ecency/sdk test -- path/to/test.spec.ts# Build before publishing
pnpm build:packages
# Publish individual packages
pnpm publish:sdk
pnpm publish:wallets
pnpm publish:render-helper
pnpm publish:ui- apps/web - Main Next.js application (
@ecency/web)- apps/web/src/features/post-renderer - Post rendering components (migrated from packages/renderer)
- apps/self-hosted - Self-hosted blog SPA (
@ecency/self-hosted) — Rsbuild + TanStack Router - packages/sdk - Core Hive SDK with React Query integration (
@ecency/sdk) - packages/wallets - Multi-chain wallet management (
@ecency/wallets) - packages/render-helper - Markdown rendering utilities (
@ecency/render-helper) - packages/ui - Shared UI component library (
@ecency/ui)
All packages use workspace:* protocol for local dependencies. The main app transpiles workspace packages during build (configured in next.config.js).
Packages use dual builds (tsup) to optimize for different environments:
- Browser build: ESM with TypeScript declarations
- Node build: ESM + CJS for SSR compatibility
Exports are platform-specific via package.json exports field.
The monorepo follows a layered architecture with clear separation of concerns:
┌─────────────────────────────────────────────┐
│ @ecency/web (Next.js App) │
│ - UI Components & Pages │
│ - SDK mutation wrappers (api/sdk-mutations)│
│ - App-specific mutation logic │
│ - Feature modules │
│ - Post renderer (features/post-renderer) │
└──────────────────┬──────────────────────────┘
│ imports
┌─────────────┼──────────┬──────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌────────────┐ ┌────────┐ ┌──────────┐ ┌────────┐
│ @ecency/ │ │@ecency/│ │ @ecency/ │ │@ecency/│
│ sdk │ │wallets │ │ render- │ │ ui │
│ - Queries │ │- Asset │ │ helper │ │- Shared│
│ - Mutations│ │ queries│ │- Markdown│ │ UI │
│ - Types │ │- Keys │ │- Content │ │ comps │
│ - QueryKeys│ └───┬────┘ └──────────┘ └────────┘
└──────┬─────┘ │
└───────────┘ wallets imports sdk
┌─────────────────────────────────────────────┐
│ @ecency/self-hosted (Rsbuild SPA) │
│ - Blog/Community hosting │
│ - TanStack Router │
│ - Runtime config (Docker/Nginx) │
└──────────────────┬──────────────────────────┘
│ imports sdk, render-helper, wallets, ui
Purpose: Hive blockchain queries and mutations
Scope:
- ✅ Hive blockchain queries (posts, comments, accounts, communities)
- ✅ Hive blockchain mutations (vote, comment, transfer, delegate, etc.)
- ✅ Hive notifications
- ✅ Core Hive types and interfaces
- ✅ React Query integration for Hive APIs
- ✅ Centralized
QueryKeysfor all cache key management - ✅ Broadcast adapter pattern for platform-specific auth (web, mobile)
- ✅ Lightweight utilities (no heavy dependencies)
Entry Points:
@ecency/sdk- Full SDK with React Query hooks, mutations, and queries@ecency/sdk/hive- React-free entry point exporting only the transaction engine (signing, RPC, crypto). Use this for server-side tools, CLI scripts, or non-React environments to avoid pulling in React/react-query dependencies.
Dependencies: NONE (except peer deps: @hiveio/dhive, hivesigner, @tanstack/react-query)
Bundle Size: ~229KB (full) / ~39KB (hive-only)
Why separate: This package is published to npm for use by other Hive applications. It must remain focused and lightweight.
Purpose: Asset queries and wallet management across multiple blockchains
Scope:
- ✅ Hive assets (HIVE, HBD, Hive Power) — queries and balance display
- ✅ Hive Engine tokens — queries and balance display
- ✅ SPK Network (SPK, LARYNX) — queries and balance display
- ✅ Points system — queries
- ✅ External blockchains (BTC, ETH, SOL, TON, TRON, APT, BNB)
- ✅ Wallet balance/transaction queries
- ✅ Multi-chain key management
Note: All blockchain mutations (transfers, delegations, staking, etc.) have been migrated to @ecency/sdk. This package now focuses on queries, balance display, and multi-chain key management.
Dependencies:
- @ecency/sdk (for Hive core functionality)
- @okxweb3 packages (for external blockchain support)
- Heavy crypto libraries
Bundle Size: ~131KB
Why separate: Wallet functionality requires heavy external dependencies. Users who only need Hive SDK shouldn't be forced to download BTC/ETH/SOL libraries.
Purpose: Framework-agnostic markdown and content rendering utilities
Scope:
- ✅ Markdown to HTML conversion
- ✅ Content sanitization
- ✅ Post formatting utilities
- ✅ Image proxification
- ✅ Content summarization
Dependencies: Minimal (htmlparser2, remarkable, xss, he, lolight)
Bundle Size: ~10-12KB
Why separate: Framework-agnostic utility library that can be used in any JavaScript environment (browser, Node.js, React Native). Published to npm for reuse by other Hive applications.
Purpose: Shared React UI components used across apps
Scope:
- ✅ Reusable React UI components
- ✅ Shared between web and self-hosted apps
Why separate: Avoids duplicating UI primitives across multiple apps in the monorepo.
Purpose: Lightweight SPA for self-hosted Hive blogs and communities
Stack: Rsbuild (not Next.js) + TanStack Router + Zustand + TailwindCSS 4
Scope:
- ✅ Runtime config system (hot-swappable config.json for Docker/Nginx)
- ✅ Blog post listing, single post view, comments
- ✅ Auth (Keychain, HiveSigner, HiveAuth)
- ✅ Publish/create posts (TipTap editor)
- ✅ Voting, reblog, comment creation
- ✅ Configurable sidebar, themes, branding
Key files:
apps/self-hosted/src/core/configuration-loader.ts— Runtime config loadingapps/self-hosted/src/providers/sdk/broadcast-adapter.ts— Auth bridge to SDKapps/self-hosted/src/routes/__root.tsx— Root layout
Deployment: Docker multi-stage build with Nginx serving static files
Purpose: Full-featured Hive web client
Scope:
- ✅ All UI components and pages
- ✅ App-specific business logic
- ✅ Feature modules
- ✅ App-specific query wrappers (using both SDK and wallets)
- ✅ State management (Zustand + React Query)
Dependencies: All workspace packages + app-specific libraries
❌ DO NOT:
- Move wallet/asset queries from
@ecency/walletsto@ecency/sdk- Reason: Would bloat SDK with multi-chain dependencies
- Add heavy dependencies to
@ecency/sdk- Reason: SDK must remain lightweight for external consumers
- Create circular dependencies between packages
- Reason: Breaks build order and package independence
- Add app-specific logic to packages
- Reason: Packages should be reusable
- Use hardcoded query key arrays — use
QueryKeysfrom@ecency/sdk- Reason: Single source of truth for cache invalidation
✅ DO:
- Put all Hive blockchain mutations in
@ecency/sdk - Put wallet/asset queries in
@ecency/wallets - Create web-specific mutation wrappers in
apps/web/src/api/sdk-mutations/ - Use
QueryKeysfrom@ecency/sdkfor all cache key references - Rebuild packages after changes:
pnpm build:packages
Query Organization:
// In @ecency/sdk - Hive blockchain queries + mutations
export function getPostQueryOptions(author: string, permlink: string) { ... }
export function useVote(username: string, auth?: AuthContextV2) { ... }
// In @ecency/wallets - Asset/wallet queries (no mutations)
export function getHiveEngineTokensBalancesQueryOptions(username: string) { ... }
export function getSpkWalletQueryOptions(username?: string) { ... }
// In @ecency/web - SDK mutation wrappers (apps/web/src/api/sdk-mutations/)
// Each wrapper adds the web broadcast adapter and active user context
import { useVote } from "@ecency/sdk";
import { createWebBroadcastAdapter } from "@/providers/sdk";
export function useVoteMutation() {
const { activeUser } = useActiveAccount();
const adapter = createWebBroadcastAdapter();
return useVote(activeUser?.username, { adapter });
}After modifying files in packages/sdk or packages/wallets:
# Rebuild all packages
pnpm build:packages
# Or rebuild individual packages
pnpm --filter @ecency/sdk build
pnpm --filter @ecency/wallets buildThe web app will not see package changes until they are rebuilt.
- Next.js 15.4 with App Router
- State Management: Zustand (global state) + @tanstack/react-query (server state)
- Styling: TailwindCSS + SCSS modules
- Editor: TipTap
- Forms: react-hook-form + yup
- Blockchain: @hiveio/dhive, hivesigner, hive-auth-client
- Monitoring: Sentry
apps/web/src/
├── app/ # Next.js App Router pages & layouts
│ ├── (dynamicPages)/ # Dynamic routes (profiles, entries)
│ ├── (staticPages)/ # Static pages
│ └── api/ # API routes
├── api/ # API layer
│ ├── queries/ # App-specific React Query queries (most queries now in SDK)
│ ├── mutations/ # App-specific mutation logic (reply, transfer, etc.)
│ ├── sdk-mutations/ # SDK mutation wrappers (60 hooks)
│ ├── format-error.ts # Error formatting utility
│ ├── bridge.ts # Main API bridge
│ ├── hive.ts # Hive blockchain API
│ └── private-api.ts # Ecency private API
├── core/ # Core infrastructure
│ ├── global-store/ # Zustand global state
│ │ ├── modules/ # State modules (auth, UI, users, etc.)
│ │ └── initialization/ # Client/server initialization
│ ├── react-query/ # React Query configuration
│ └── caches/ # Caching utilities
├── features/ # Feature modules (25 features)
│ ├── shared/ # 64 shared components
│ ├── ui/ # UI component library (22 components)
│ └── [feature-name]/ # Domain-specific features
├── entities/ # Domain entities/types
├── enums/ # TypeScript enums
├── utils/ # Utility functions (79+ files)
├── config/ # Feature flag configuration system
├── routes.ts # Route definitions
└── middleware.ts # Next.js middleware
Global State (Zustand):
- Located in
src/core/global-store/modules/ - Separate client/server initialization to support SSR
- Modules: authentication, global, ui, users, notifications, signing-key, config
Server State (React Query):
- Most queries now live in
@ecency/sdkas query option builders;src/api/queries/has app-specific queries only - 60 SDK mutation wrappers in
src/api/sdk-mutations/ - App-specific mutation orchestration in
src/api/mutations/ QueryKeysfrom@ecency/sdkfor cache key management- Separate QueryClient instances for client/server
@/*→src/*@ui/*→src/features/ui/*
These are configured in:
apps/web/tsconfig.json(TypeScript)apps/web/vitest.config.ts(Vitest)apps/web/next.config.js(Webpack)
The app uses Next.js App Router with complex URL rewrites for legacy compatibility:
- Profile pages:
/:author(@.+)/:section - Entry pages:
/:category/:author/:permlink - Community pages:
/:filter/:community(hive-\d+) - Feed pages:
/:filter(hot|created|...)/:tag
Rewrites are defined in next.config.js. Route constants live in src/routes.ts.
The EcencyConfigManager (src/config/) provides:
- Feature flag configuration
- Conditional rendering components
- Environment-based configuration
- Template-based config structure
API Files:
bridge.ts- Main API bridge with complex routing logichive.ts- Hive blockchain APIhive-engine.ts- Hive Engine token APIprivate-api.ts- Ecency private APIformat-error.ts- Error formatting for user-facing error messages
Mutation Architecture:
All blockchain mutations live in @ecency/sdk and are wrapped for web use in apps/web/src/api/sdk-mutations/. Each wrapper hook:
- Gets the active user via
useActiveAccount() - Creates a web broadcast adapter via
createWebBroadcastAdapter() - Passes both to the SDK mutation hook
@ecency/sdk (platform-agnostic mutations)
↓ wrapped by
apps/web/src/api/sdk-mutations/ (60 web-specific hooks)
↓ used by
apps/web/src/api/mutations/ (app-specific orchestration: optimistic updates, error handling)
↓ used by
Feature components
App-specific mutation logic (optimistic cache updates, error handling, multi-step flows) remains in apps/web/src/api/mutations/.
Cache Key Management:
- Use
QueryKeysfrom@ecency/sdkas the single source of truth - Legacy
QueryIdentifiersenum still exists for web-app-only queries (polls, threespeak, market) - New queries should define keys in SDK's
QueryKeyswhen possible
Multi-method authentication support:
- Private Key (
key) - Direct key signing (posting key stored, active via auth upgrade) - Keychain (
keychain) - Browser extension integration - HiveSigner (
hivesigner) - OAuth token-based broadcasting - HiveAuth (
hiveauth) - QR code + mobile app flow
Broadcast Adapter Pattern (apps/web/src/providers/sdk/web-broadcast-adapter.ts):
The SDK uses a platform adapter pattern to decouple mutations from auth. The web app provides createWebBroadcastAdapter() which implements the PlatformAdapter interface:
SDK mutation (platform-agnostic)
↓ calls adapter methods
Web Broadcast Adapter (web-specific)
↓ resolves auth via
localStorage (keys/tokens) → Keychain extension → HiveSigner API → HiveAuth protocol
Smart Auth Strategy (in SDK's useBroadcastMutation):
- Calls
adapter.getLoginType()to determine user's auth method - For posting ops with granted posting authority: tries HiveSigner token first (faster)
- Falls back to user's actual auth method if token fails
- For active ops: shows auth upgrade dialog via
adapter.showAuthUpgradeUI()
Auth Upgrade Flow (apps/web/src/features/shared/auth-upgrade/):
When an operation requires active authority but the user logged in with posting key:
- SDK detects auth failure → calls
adapter.showAuthUpgradeUI() - Web adapter dispatches
ecency-auth-upgradeCustomEvent - Auth upgrade dialog appears, user selects method (enter active key, use Keychain, etc.)
- Dialog resolves the promise with chosen method + optional temp key
- SDK retries broadcast with the chosen method
- Temp active key auto-clears after 60s or on next auth flow
The web app uses Vitest + React Testing Library. Tests are located in src/specs/:
core/- Core functionality tests (React Query helpers, hooks)features/- Component tests organized by featureutils/- Utility function tests
# All tests
pnpm test
# Specific test file
pnpm test path/to/test.spec.tsx
# Watch mode
pnpm test --watch
# Update snapshots
pnpm test -u
# UI mode
pnpm test --uiUse src/specs/test-utils.tsx for common testing patterns:
import { renderWithQueryClient, mockFullAccount, setupModalContainers } from '@/specs/test-utils';
describe('MyComponent', () => {
beforeEach(setupModalContainers);
test('renders correctly', () => {
const { container } = renderWithQueryClient(
<MyComponent />,
{ queryClient: seedQueryClient({ posts: mockData }) }
);
});
});Global Mocks (setup-any-spec.ts):
- External packages (@ecency/sdk, @ecency/wallets, @ecency/render-helper)
- i18next (returns keys as-is)
@/utils(only exportsrandomandgetAccessToken)@/core/hooks/use-active-account(returns null active user)- uuid, react-tweet
Per-Test Mocks:
- API queries/mutations specific to the component
- Component-specific dependencies
Important: @/utils Global Mock Limitation
The global mock for @/utils only provides random and getAccessToken. If your component imports other utilities (e.g., parseAsset, dateToFormattedUtc, formatNumber), the test will fail with "No export is defined on the mock." Fix by adding a local re-mock with importActual at the top of your test file:
vi.mock("@/utils", async () => ({
...(await vi.importActual("@/utils")),
random: vi.fn(),
getAccessToken: vi.fn(() => "mock-token")
}));This preserves all real utility exports while keeping the globally-mocked functions stubbed.
1. Utility Functions (pure functions):
import { myUtilFunction } from '@/utils';
describe('myUtilFunction', () => {
it('should handle edge case', () => {
expect(myUtilFunction(input)).toBe(expected);
});
});2. React Components:
import { render, screen, fireEvent } from '@testing-library/react';
import { renderWithQueryClient } from '@/specs/test-utils';
import { vi } from 'vitest';
describe('MyComponent', () => {
test('user interaction', () => {
renderWithQueryClient(<MyComponent />);
fireEvent.click(screen.getByRole('button'));
expect(screen.getByText('Updated')).toBeInTheDocument();
});
});3. Components with React Query:
import { QueryClient } from '@tanstack/react-query';
import { vi } from 'vitest';
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } }
});
beforeEach(() => {
queryClient.setQueryData(['key'], mockData);
});
test('displays data', () => {
renderWithQueryClient(<MyComponent />, { queryClient });
expect(screen.getByText(mockData.title)).toBeInTheDocument();
});- ✅ Test user-visible behavior, not implementation details
- ✅ Use
screen.getByRoleovergetByTestIdwhen possible - ✅ Mock external dependencies with
vi.fn(), not internal functions - ✅ Keep tests focused and isolated
- ✅ Use factories for mock data (see test-utils.tsx)
- ✅ Clean up after tests (DOM, timers, mocks)
- ❌ Don't test library code (React Query, Zustand)
- ❌ Don't mock too much (makes tests brittle)
- ❌ Don't use snapshots for everything (hard to maintain)
- All new features require tests
- Bug fixes should include regression tests
- Aim for >80% coverage on business logic
- 100% coverage not required for UI components
- Test environment: jsdom
- Test location:
src/specs/ - Test pattern:
*.spec.tsxco-located with components - Setup file:
src/specs/setup-any-spec.ts - Mocked modules: i18next, @ecency/sdk, @ecency/wallets, @ecency/render-helper
- Configuration:
apps/web/vitest.config.ts
- Each package has its own Vitest configuration
- Test files co-located with source code
- Follow feature-based organization in
src/features/ - Shared components go in
features/shared/ - UI primitives go in
features/ui/ - Avoid unnecessary re-renders (performance-critical app)
- Create tests for new components
- Use Zustand for client-side global state
- Use React Query for server state (API data)
- Check
src/core/global-store/modules/for existing state modules - Query identifiers defined in centralized enum
- TailwindCSS is primary styling method
- Dark mode via
classstrategy - SCSS modules for component-specific styles
- Custom theme in
tailwind.config.ts
- TypeScript strict mode enabled
- All new code should include proper types
- ESLint and type checking ignored during builds (check before commit)
- Run
pnpm lintandpnpm testbefore creating PRs
- All new strings must be added to
en-US.jsononly - Use i18next for translations
- Translation system in
src/features/i18n/
- Branch from
develop(notmaster) - Include tests with changes
- Update tests if modifying existing code
- Mark PR as WIP if not ready
- Include description, screenshots/videos, and link to issues
- Ensure linting and tests pass before requesting review
Copy apps/web/.env.template to apps/web/.env and configure:
NEXT_PUBLIC_HS_CLIENT_ID # HiveSigner client ID
NEXT_PUBLIC_HS_CLIENT_SECRET # HiveSigner secret
NEXT_PUBLIC_APP_BASE # App base URL
NEXT_PUBLIC_APP_NAME # App name
NEXT_PUBLIC_APP_TITLE # App title
NEXT_PUBLIC_APP_DESCRIPTION # App descriptionSee README.md for detailed environment variable documentation.
This project uses pnpm@10.26.1. The packageManager field in package.json ensures the correct version is used. Always run commands from the workspace root unless specifically targeting a package.
The production build:
- Generates UUID-based build IDs
- Enables production browser source maps
- Includes PWA support (8MB cache limit)
- Integrates Sentry for error tracking
- Transpiles workspace packages
- Uses sass-embedded for fast SCSS compilation
package.json- Workspace scripts and configurationapps/web/next.config.js- Next.js configuration, rewrites, Sentryapps/web/src/app/layout.tsx- Root layout and providersapps/web/src/core/global-store/index.ts- Global state setupapps/web/src/core/react-query/index.ts- React Query setupapps/web/src/api/sdk-mutations/index.ts- All SDK mutation wrapper exportspackages/sdk/src/modules/core/query-keys.ts- Centralized cache key definitionsapps/web/src/providers/sdk/web-broadcast-adapter.ts- Web-specific auth/broadcast adapterapps/web/src/routes.ts- Route constantsapps/web/tailwind.config.ts- Design system