Agents must run `npm run start:dev` then `npm run generate` when touching GraphQL resolvers, types, or client operations to keep generated types in sync with the server schema. Co-authored-by: Bufo <bufo24@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Rajat Khanduri <38693805+wthrajat@users.noreply.github.com>
8.4 KiB
Agent Guide
This file provides guidance to AI coding agents working with this repository.
What is ThunderHub
ThunderHub is a Lightning Network node manager. It provides a web UI and GraphQL API for managing LND nodes — channels, payments, invoices, forwards, peers, chain transactions, and Boltz submarine swaps. It integrates with Amboss for node analytics and health monitoring.
Commands
npm run build # Build NestJS server + Vite client
npm run build:nest # Build server only
npm run build:client # Build client only
npm run start:dev # Dev: NestJS watch + Vite dev server (concurrent)
npm run lint # ESLint with auto-fix
npm run lint:check # ESLint without fix (used in CI)
npm run test # Jest (rootDir: src/, matches *.spec.ts)
npm run test -- --testPathPattern="channels" # Run tests matching a pattern
npm run test:e2e # E2E tests (config: test/jest-e2e.json)
npm run generate # GraphQL codegen — requires server running at localhost:3000; not for CI
Note: Package manager is npm, not pnpm/yarn. Node version: see .nvmrc.
Verifying changes
After making code changes, run these in order to confirm correctness:
npm run lint:check # Check for lint errors (no auto-fix)
npm run test # Run unit tests
npm run build # Confirm the full build succeeds
Run a targeted test when touching a specific module:
npm run test -- --testPathPattern="<module-name>"
Regenerating GraphQL types
When changes touch GraphQL resolvers, types, queries, or mutations, you must regenerate the typed client code:
- Start the dev server:
npm run start:dev - Once the server is listening on
localhost:3000, run:npm run generate - Commit the updated
schema.gqland__generated__/*.generated.tsxfiles alongside your changes.
Skipping this step will cause the client to use stale types that may not match the server schema.
Architecture
Monorepo with two apps under src/:
src/server/— NestJS backend (GraphQL API via Apollo, code-first schema)src/client/— React frontend (Vite, Apollo Client, React Router)
Each has its own tsconfig.json. The root tsconfig only includes src/server. The client tsconfig at src/client/tsconfig.json is strict mode with @/* aliased to ./src/*.
Server (src/server/)
Entry: main.ts bootstraps NestJS with Helmet, Winston logger, and optional BASE_PATH prefix.
Root module: app.module.ts wires up GraphQL (Apollo Driver, code-first), static file serving, JWT auth context, dataloaders, and scheduled tasks.
Config: config/configuration.ts is the single place all env vars are read. Returns a typed ConfigType. Uses @nestjs/config (global). Env files: .env.local overrides .env.
Module layout under modules/:
api/— GraphQL resolvers organized by domain. Each subdomain (channels, invoices, wallet, boltz, amboss, etc.) has*.module.ts,*.resolver.ts,*.types.ts, and optionally*.helpers.ts.node/— LND abstraction layer.NodeServiceis the facade resolvers call; it resolves the account by user ID, then delegates toLndService.LndServicewraps thelightningnpm package directly.accounts/— In-memory account store.AccountsServiceimplementsOnModuleInit: at startup, reads SSO config and account config files, creates authenticated LND gRPC connections (authenticatedLndGrpc), and storesEnrichedAccountobjects (account data +lndhandle) in a map keyed by account hash.security/— Three global guards registered asAPP_GUARD:GqlAuthGuard(JWT via passport),RolesGuard,GqlThrottlerGuard. Key decorators:@Public()(skip auth),@Roles(...),@CurrentUser()(extractsUserIdfrom GQL context).dataloader/— Creates per-requestDataLoaderinstances for batching Amboss API lookups (nodesLoader,edgesLoader). Injected into GraphQL context.fetch/— HTTP client with optional SOCKS proxy (Tor) support.graphqlFetchWithProxy()for external GraphQL APIs.sub/— LND event subscriptions (invoices, payments, forwards, channels, backups) usingasync.auto()with retry logic. Emits events toSseServicefor real-time client updates.sse/— Server-sent events endpoint for pushing LND events to the client.
Request flow (server)
HTTP request → GqlAuthGuard (JWT validation via passport) → RolesGuard → ThrottlerGuard
→ Resolver receives @CurrentUser() with { id: accountHash }
→ Resolver calls NodeService.method(user.id, ...)
→ NodeService looks up EnrichedAccount by hash (in-memory map)
→ NodeService delegates to LndService.method(account, ...)
→ LndService calls lightning library with account.lnd handle
Async error helpers (utils/async.ts)
to<T>(promise)— Awaits and returns result, throws on errortoWithError<T>(promise)— Returns[data, undefined] | [undefined, error]tuple (Go-style)
The lightning library returns errors as arrays [title, string, { err }]; lnd.helpers.ts has a dedicated to() that transforms these.
Client (src/client/src/)
- Styling: Hybrid — styled-components (with
styled-themingfor dark/light) + Tailwind CSS. New components use shadcn/ui (New York style, configured incomponents.jsonat repo root). - Imports: Use
@/path alias (maps tosrc/client/src/). - Config: Runtime config fetched from server at
/api/configon bootstrap (not build-time env vars). - Context pattern: Dual-context for state + dispatch with custom hooks (
useXState(),useXDispatch()). Contexts: Config, Price, SSE, Chat, Dash, Notification. - GraphQL: Operations in
graphql/queries/andgraphql/mutations/asgqltemplate literals. Codegen generates__generated__/*.generated.tsxfiles with typed Apollo hooks next to each operation file. - Real-time: SSE (
EventSource) for receiving LND events;useListenerhook triggers Apollo cache refetches and toast notifications. - Icons: Lucide React.
GraphQL workflow
- Schema is code-first: resolvers define it via NestJS/GraphQL decorators.
schema.gqlis auto-generated in dev mode. - Client operations are
.tsfiles withgqltemplate literals insrc/client/src/graphql/. - Run
npm run generate(requires server running atlocalhost:3000) to produce typed hooks in__generated__/directories. - Do not edit
schema.gqlor*.generated.tsx— they are auto-generated.
Pre-commit hooks
Husky runs lint-staged on commit for *.ts and *.tsx files:
prettier --writejest --bail --findRelatedTests --passWithNoTestseslint --fix
Prettier config
Single quotes, trailing commas (es5), 2-space tabs, 80 char width, no parens on single arrow params. See .prettierrc.
Coding conventions
Server
- Use
toWithError<T>(promise)(fromutils/async.ts) for LND calls that may fail — returns[data, undefined] | [undefined, error]. Useto<T>(promise)when you want to throw on error. - The
lightninglibrary errors are arrays; use the dedicatedto()inlnd.helpers.ts(not the generic one) when wrapping rawlndcalls. - Tests are co-located as
*.spec.tsnext to the file under test.
Client
- New UI components must use shadcn/ui (New York style). Do not add new styled-components.
- Use the
@/import alias for all client-side imports (maps tosrc/client/src/). - Follow the dual-context pattern (
useXState()/useXDispatch()) for new shared state. - Icons: use Lucide React only.
Auto-generated files — do not edit
The following files are generated automatically and must not be edited by hand:
| File/pattern | Generated by |
|---|---|
schema.gql |
NestJS GraphQL (code-first, on server start) |
src/client/src/graphql/**/__generated__/*.generated.tsx |
npm run generate (GraphQL codegen) |
To regenerate: start the dev server (npm run start:dev) then run npm run generate.