This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
- When working with MCP servers, always verify which MCP server is connected and use the correct one before attempting operations. Check
mcp__tool availability first rather than falling back to SSH or codebase search. - Available MCP servers:
mcp_backend,mcp_rada,mcp_openreyestr, Nextcloud (Deck/Tables/Calendar), Thunderbird, AWS API. Use the correct prefix (mcp__nextcloud__,mcp__thunderbird__,mcp__awslabs-aws-api-mcp-server__). - Never use SSH or codebase grep as a workaround when the appropriate MCP tool is available.
- This is a monorepo with shared/backend/frontend packages. The app runs in Docker locally (nginx, backend, frontend, DB, MinIO).
- Frontend API calls go through nginx. When changing API URLs or paths, trace the FULL request path (frontend → nginx → backend) before making changes to avoid cascading breakage.
- Features should render results in the right panel/evidence panel, NOT in the chat window, unless explicitly told otherwise.
- This project uses TypeScript throughout. After making multi-file changes, always run the TypeScript build (
tscor the build script) and fix all type errors before committing. - When removing or refactoring tools/functions, grep for ALL references across the monorepo before deleting to avoid stale references.
- Production deployment is fully automated via CI/CD. Merge PR to main triggers the pipeline. NEVER deploy manually via SSH.
- CI/CD runs on self-hosted runners (on local.lex). Two prods, one codebase (split is env-side, never a code fork):
ci-local-deploy.yml— push to main: builds, tests, and itsdeploy-localjob deploys legal.org.ua prod on cthulhu/local.lex (runner labellocal-deploy— never plainlocal, which also matches the prod runner)deploy-lawrider.yml— after successful CI or manual: blue-green deploy to lawrider.ch on AWS via SSH (marker tags remaindeploy-prod-*; domain cutover =VITE_API_URL_PRODin the box's.env.prod+LAWRIDER_API_URLrepo variable for the CI test build)
- Blue-green deployment: prod uses
.active-colorsfile indeployment/to track which color (blue/green) is active per service group (backend, frontend). New deploys go to the inactive color, then traffic is switched. - Nginx must be
--force-recreatedafter ANY upstream/backend change (bind mount staleness). - Never manually recreate prod containers; use the deploy pipeline.
- After making changes to CI/CD workflows or Dockerfiles, verify the build passes locally or check for missing dependencies (e.g.,
npm cirequires lock files, volume mounts in docker-compose). Never assume CI will pass without validation. - There is NO stage environment. There are TWO prods: legal.org.ua (local.lex, UA product) and lawrider.ch (AWS, multi-jurisdiction incl. CH/UK/NL/PL).
- After deploying, always check container health and logs for errors.
- After making code changes, always rebuild Docker images before testing (
docker compose build <service>thendocker compose up -d). Never test against stale containers. - Local dev runs in Docker. Do NOT attempt to restart backend processes directly — always use docker compose commands.
- Compose files have NO
env_file:directives — secrets are injected via shell env substitution. Always pass--env-file:- Local:
docker compose -f docker-compose.local.yml --env-file .env.local up -d - Prod:
docker compose -f docker-compose.prod.yml --env-file .env.prod up -d
- Local:
- Always create feature branches from
mainunless explicitly told otherwise. - Standard flow: commit → push → create PR → merge → switch back to main.
- Never create branches off other feature branches without asking.
- When asked to commit and push, commit ONLY the files relevant to the current task. Do not stage unrelated files or over-scope commits.
- If the user says a number or short response after listing options, ask for clarification rather than assuming intent.
- Prefer action over analysis. When the user asks to implement something, start coding after brief analysis (< 2 minutes exploration). Do NOT spend the whole session producing plans without writing code.
- If asked to fix a specific bug, stay focused on that bug. Do not go down rabbit holes investigating tangentially related issues unless explicitly asked.
- This is a TypeScript monorepo. Primary language is TypeScript. Always ensure builds pass (
npm run buildor equivalent) before creating PRs or deploying.
- Monorepo with shared/backend/frontend packages
- Language: TypeScript (primary), YAML for configs, Shell for deploy scripts
- Infrastructure: Docker Compose for local dev, MinIO for storage, Nginx as reverse proxy
- Auth: Google OAuth, Authentik (OIDC), Diia, password, WebAuthn
- Payments: Monobank (live, dual UAH/USD balance system)
- Architecture: MCP tools pattern with LLM orchestration
When the user asks for a specific task (e.g., 'commit frontend changes'), do exactly that. Do not expand scope to investigate related issues, refactor adjacent code, or explore the codebase unless explicitly asked. If you see something worth investigating, mention it briefly and ask before proceeding.
- This project uses Ukrainian (uk-UA) for all user-facing strings and error messages.
- All UI text, toast notifications, and error messages should be in Ukrainian unless told otherwise.
- Primary stack: TypeScript, Docker Compose, PostgreSQL (with PgBouncer), Redis, Qdrant.
- Only two environments: local and prod — always double-check VITE_API_URL and similar env vars match the target environment.
- When changing PostgreSQL auth or connection pooling config, verify auth method compatibility (MD5 vs SCRAM-SHA-256).
- Never change SSH-related paths (home directory, authorized_keys) on remote servers without explicit confirmation.
- SSH to prod as
ubuntu, not root. Usessh prodalias (key at~/.ssh/secondlayer-prod). - When deploying to production or working with infrastructure, verify SSH keys match the target region/instance before proceeding. For multi-server setups, confirm IP bindings and firewall rules (ufw, postfix mynetworks) for each new service.
- When debugging production issues, start with logs and targeted debug output FIRST. Do not extensively explore the codebase before checking actual runtime behavior.
- When asked to check errors or logs, always check the REMOTE server logs (via SSH) unless explicitly told to check locally.
- When diagnosing search/query issues, log the actual generated query before assuming the logic is correct.
- When fixing bugs, deploy the fix AND verify it works in the target environment before marking done.
Branch main is protected — direct pushes are blocked for everyone (including admins). All changes must go through a Pull Request with at least 1 approving review.
After completing code changes, always: 1) Build and verify no errors, 2) Create a feature branch (git checkout -b <descriptive-name>), 3) Commit with a descriptive message, 4) Push the branch and create a PR via gh pr create, 5) Deploy locally if requested. Do NOT over-scope commits — only stage files relevant to the current task. NEVER push directly to main.
- Backend services run in Docker containers, NOT as local processes. Do not attempt to restart backend services outside Docker.
- Always run
docker compose buildafter code changes before testing — do not test against stale images. - Run commands from the correct workspace directory (check with
pwdbefore executing). - Use the correct env passwords from
.envfiles, not defaults.
Primary stack: TypeScript, YAML, Shell. When creating new files, default to TypeScript (the project already uses TypeScript throughout).
Before implementing non-trivial features, use a task agent to investigate: (1) current state of the system/service, (2) exact API limits or format requirements, (3) existing configs or files that would conflict. Report findings before writing any code.
SecondLayer is a monorepo for a Ukrainian legal tech platform. It provides AI-powered legal document analysis, semantic search over court decisions (via EDRSR — state court decision registry), legislation retrieval, parliament data, business registry lookups, consultations, billing, and payments via MCP (Model Context Protocol) servers.
SecondLayer/
├── mcp_backend/ # Primary MCP server - court cases, legal docs, consultations, billing
├── mcp_rada/ # Parliament data server (deputies, bills, legislation)
├── mcp_openreyestr/ # State Register server (business entities, beneficiaries, debtors)
├── lexwebapp/ # Web frontend (React 19, Vite, TailwindCSS)
├── packages/shared/ # Shared TypeScript types and utilities (@secondlayer/shared)
├── platform/ # Platform service (separate frontend)
├── mobile/ # Mobile app (Flutter)
├── opendata-sync/ # Open data synchronization service
├── terminal-service/ # Terminal service
├── deployment/ # Docker configs, compose files, nginx, manage-gateway.sh
├── scripts/ # Utility scripts (deploy, rada sync, testing, conversion)
├── tests/ # E2E tests (Playwright), test fixtures
├── docs/ # Documentation, API explorer, reports
├── config/ # MCP client configs
├── legacy/ # Archived code
└── lexconfig/ # Application config files
All three MCP servers support:
- MCP stdio - Standard MCP protocol for Claude Desktop integration
- HTTP API - REST endpoints (
/api/tools/:toolName) for web apps - SSE (Server-Sent Events) - Remote MCP over HTTPS for distributed clients
- Runtime: Node.js 20+ (
.nvmrc: 20) with TypeScript 5.3 - Databases: PostgreSQL 15, Redis 7, Qdrant (vector DB)
- AI: OpenAI API (GPT-4o, text-embedding-ada-002), optional Anthropic (Claude)
- External APIs: EDRSR (state court decision registry), Verkhovna Rada Open Data, data.gov.ua (OpenReyestr)
- Framework: Express.js, MCP SDK (
@modelcontextprotocol/sdk) - Frontend: React 19, Vite, TailwindCSS 3, Zustand (state), TanStack Query (data fetching), Vitest
- Payments: Monobank API (dual UAH/USD balance)
- Mobile: Flutter (Dart)
Production aggregates all services behind a single endpoint (ENABLE_UNIFIED_GATEWAY=true):
- Backend tools (no prefix) - court cases, semantic search, legislation, patterns, citations, vault
- RADA tools (
rada_*) - deputies, bills, legislation, voting - OpenReyestr tools (
openreyestr_*) - entity search, beneficiaries, debtors
Tool registry: mcp_backend/src/api/tool-registry.ts maps tool names to handler classes.
Key exports used across all services:
getOpenAIManager/getAnthropicManager- LLM client singletonsLLMManager- Unified interface for OpenAI/Anthropic with fallbackModelSelector- Budget-aware, multi-provider model selection. Provider order viaLLM_PROVIDER_STRATEGY(prod =bedrock-first). Prod tiers (Bedrock, eu-central-1): quick → Claude Haiku 4.5, standard → Claude Sonnet 4.6, deep → Claude Opus 4.6; throttle fallback → Amazon Nova (micro/pro). OpenAI fallback tiers: quick → gpt-5-nano, standard → gpt-5-mini, deep → gpt-5.1logger- Winston-based structured loggingBaseDatabase- PostgreSQL connection pool managementCostTracker/CostCalculator- Per-request API cost trackingSSEHandler- Server-Sent Events streamingAuthenticatedRequest- Express request type with auth context
Build shared before other services: cd packages/shared && npm run build
mcp_backend/src/factories/core-services.ts→createBackendCoreServices()composes all backend servicesmcp_backend/src/factories/app-services.ts→ application-level services (auth, billing, etc.)mcp_backend/src/factories/billing-services.ts→ billing and payment servicesmcp_rada/src/factories/rada-services.ts→ composes RADA services- Factories wire up database, Redis, Qdrant, embedding service, cost tracker
- QueryPlanner - Classifies user intent, selects search strategy
- DocumentService - Document retrieval and caching
- EmbeddingService - Vector embeddings via OpenAI text-embedding-ada-002
- SemanticSectionizer - Breaks documents into logical sections (articles, parts)
- ChatService - Chat orchestration with intent classification and context building
- ConsultationService - Legal consultation management with E2EE
- BillingService - Subscription and credit management
- MonobankService - Payment processing via Monobank API
- CostTracker - Tracks OpenAI/Anthropic/RADA API costs per request
- LegislationService - Legislation text retrieval with intelligent sectioning
- VaultService - Secure document storage and retrieval
- DiiaService - Diia digital identity integration
- AuthentikService - OIDC authentication via Authentik
- EdsrFtsService - Full-text search over EDRSR court decisions
- EdsrCacheService - Caching layer for EDRSR data
- EDRSRLocalAdapter - Local EDRSR court decision registry adapter
- RadaLegislationAdapter - Fetches legislation from Verkhovna Rada API (zakon.rada.gov.ua)
- ZOAdapter - Legacy adapter (ZakonOnline API is deprecated and being removed)
- State: Zustand stores (
src/stores/) for auth, documents, UI state - Data fetching: TanStack React Query (
src/lib/react-query.ts) with configured stale times - Services: API client layer (
src/services/) wrapping backend HTTP endpoints - Routing: React Router with protected routes
- UI: TailwindCSS + custom component library (
src/components/ui/) - Build: Vite with production config
cd mcp_backend
npm run dev:http # HTTP server (port 3000)
npm run dev # MCP stdio mode
npm run dev:sse # SSE mode for remote MCP
npm run build && npm start:http # Production HTTP
npm run db:setup # Create DB and run migrations
npm run migrate # Run migrations only
npm test # Jest tests
npm run lintcd mcp_rada
npm run dev:http # HTTP server (port 3001)
npm run dev # MCP stdio mode
npm run db:setup
npm run sync:deputies # Fetch deputy data from RADA API
npm run sync:laws # Fetch legislation texts
npm run cleanup:cache # Clean expired cache entries
npm run build && npm testcd mcp_openreyestr
npm run dev:http # HTTP server (port 3005)
npm run dev # MCP stdio mode
npm run db:setup && npm run migrate
npm run import:entities # Import legal entities from XML
npm run import:debtors # Import debtors registry
npm run sync:registries # Sync all NAIS registries
npm run build && npm testcd lexwebapp
npm run dev # Vite dev server
npm run build # Production build
npm run test # Vitest
npm run test:coverage
npm run lintnpm run install:all # Install all dependencies (root + workspaces)
npm run backend # Start mcp_backend HTTP
npm run frontend # Start lexwebapp dev server| Service | HTTP | PostgreSQL | Redis | Qdrant |
|---|---|---|---|---|
| mcp_backend | 3000 | 5432 | 6379 | 6333-6334 |
| mcp_rada | 3001 | 5433 | 6380 | 6335-6336 |
| mcp_openreyestr | 3005 | 5435 | 6382 | - |
Deployment environments:
- Local:
localhost:3000/https://local.legal.org.ua - Prod:
https://legal.org.ua
Each service has .env.example with all required variables. Key vars:
DATABASE_URL/POSTGRES_*- PostgreSQL connectionREDIS_HOST,REDIS_PORT- Redis cacheQDRANT_URL- Vector databaseOPENAI_API_KEY- AI embeddings and analysisSECONDARY_LAYER_KEYS- API authentication tokens (comma-separated)OPENAI_MODEL_QUICK/STANDARD/DEEP- Budget-aware model selectionJWT_SECRET- JWT authentication secretENABLE_UNIFIED_GATEWAY- Enable unified gateway modeMONOBANK_TOKEN- Monobank payment API tokenPUBLIC_URL- Public URL for payment callbacks
Migrations in mcp_backend/src/migrations/, mcp_rada/src/migrations/, mcp_openreyestr/src/migrations/.
cd mcp_backend && npm run migrate # Builds then runs dist/migrations/migrate.js
cd mcp_rada && npm run migrate
cd mcp_openreyestr && npm run migrateAll Dockerfiles and compose files in deployment/. Compose files use context: .. (repo root).
cd deployment
# Local development
./manage-gateway.sh start local # Start all services
./manage-gateway.sh deploy local # Full rebuild (--no-cache)
./manage-gateway.sh logs local
./manage-gateway.sh stop local
# Status
./manage-gateway.sh status
./manage-gateway.sh healthDockerfiles: Dockerfile.mono-backend, Dockerfile.mono-rada, Dockerfile.mono-openreyestr, Dockerfile.document-service, Dockerfile.opendata-sync, Dockerfile.terminal-service
Two GitHub Actions workflows on self-hosted runner:
-
ci-local-deploy.yml(on push to main):- Detects which services changed
- Builds shared package, then changed services
- Runs unit tests (Jest for backend, Vitest for frontend)
- Deploys to local Docker environment
- Health checks all deployed services
- Self-heals build failures via Claude Code agent (creates autofix PRs)
-
deploy-prod.yml(after successful local CI, or manual trigger):- Pre-deploy tests (build + test changed services)
- Blue-green deploy: builds inactive color on prod via SSH, runs migrations, starts new containers
- Preview phase: new version accessible alongside old one
- Switch phase: updates nginx upstreams to point to new color
- Tags releases with semantic versioning (backend:
v*, frontend:fe-v*) - Self-heals test failures via Claude Code agent
| Service | Framework | Command |
|---|---|---|
| mcp_backend | Jest (ts-jest) | npm test |
| mcp_rada | Jest (ts-jest) | npm test |
| mcp_openreyestr | Jest (ts-jest) | npm test |
| lexwebapp | Vitest | npm run test |
| E2E | Playwright | cd tests && npx playwright test |
Backend Jest config: maxWorkers=1, testTimeout=120000 (tests may call external APIs).
Test files: __tests__/ directories alongside source. E2E specs in tests/e2e/.
# Single test file
cd mcp_backend && npx jest --no-cache path/to/file.test.ts
cd lexwebapp && npx vitest run path/to/file.test.ts
cd tests && npx playwright test e2e/specific-test.spec.ts- Create tool handler file in
mcp_backend/src/api/tools/ - Implement handler method extending the tool pattern
- Register in
src/api/tool-registry.ts - Add HTTP route in
http-server.tsif needed - Write tests in
src/api/__tests__/
LegislationService fetches full text from RADA API, splits into logical sections (articles, parts, chapters), stores in PostgreSQL. Requesting "Constitution Article 124" returns just that article, not the whole document.
Aliases recognized: "constitution", "цивільний кодекс", "кримінальний кодекс", etc.
Every tool execution tracks: OpenAI tokens (prompt + completion), model/tier, execution time, external API calls. Stored in cost_tracking table, aggregated in monthly_api_usage.
Express app with:
- Auth: Bearer token (
SECONDARY_LAYER_KEYS) + JWT/OAuth/OIDC/Diia/WebAuthn - Tool execution:
POST /api/tools/:toolName - SSE streaming:
POST /api/tools/:toolName/stream - Batch:
POST /api/tools/batch - Health:
GET /health
- Cache TTLs: Deputies 7d, Bills 1d, Laws 30d (RADA server)
- Model selection:
ModelSelectorfrom shared package for budget-aware choice - SSE streaming: For long-running ops, use SSE endpoints (works through gateway)
- Multi-auth: Bearer token (API clients) + JWT/OAuth/OIDC/Diia/password/WebAuthn (web users)
- Monobank payments: Live with dual UAH/USD balance system, PUBLIC_URL required for callbacks
- Blue-green prod: Check
.active-colorsin deployment/ for current active color per service
scripts/deploy/- Deployment automationscripts/rada/- RADA data sync and import (deputies, laws)scripts/edrsr/- Court registry (EDRSR) data: download, import, syncscripts/opendata/- Open data imports (NIPO patents/trademarks, etc.)scripts/testing/- Test runner scripts for various scenariosscripts/utilities/- File conversion (DOCX/PDF to text), MinIO cleanup
- SSH to prod as
ubuntuuser:ssh prod(key at~/.ssh/secondlayer-prod). - Always try SSH before HTTPS for git clones on servers.
- Use port 587 for mail relay instead of 25.
- Verify correct server hostname/IP before attempting connections. Don't waste attempts on wrong hosts.
- If a database, config, or system state already exists, do NOT re-initialize it. Always check current state first (
aide --check,psql \dt, etc.) before running destructive init commands. Ask the user before re-initializing anything. - Before running any destructive command (init, reset, force-push, rm -rf, overwrite), first show the current state of what's about to change, explain why the destructive action is necessary, and wait for explicit user confirmation.
- When working with iptables/firewall rules, always test grep patterns against actual rule output format before writing scripts. Use
iptables -Soriptables -L -nto verify exact syntax before pattern matching.
Before modifying shared configuration values (env vars like VITE_API_URL, API base paths, port numbers, OAuth redirect URIs), trace ALL downstream usages across frontend, backend, and deploy scripts. Never change these without verifying the full impact chain.
- Task management is via Nextcloud (Deck boards). Linear is fully deprecated.
- UI text should be in Ukrainian (uk-UA) unless specified otherwise.
Search results, documents, and evidence MUST render in the right side panel — never in the chat window. When implementing features that return structured data, always verify the rendering target matches the design intent.
- For large PostgreSQL operations (migrations, bulk imports, index creation), always: 1) Set appropriate
statement_timeout, 2) Use bulk INSERT strategies over batch DELETE+INSERT, 3) Account for competing queries and locks, 4) Use screen/tmux with reconnection plans. - For production DB operations on large tables (millions of rows), prefer
CREATE INDEX CONCURRENTLY, partition-based strategies, and off-peak scheduling.
- When scraping external APIs (spending.gov.ua, Rada, UIPV), expect rate limiting and global throttling. Design scripts with: configurable concurrency, per-IP rate limits, resume capability, and graceful error handling from the start.
- Always implement checkpoint/resume logic so interrupted imports can continue without re-downloading.
- Test with a small batch first to discover rate limits and schema issues before scaling up.
- When writing SQL in JavaScript string literals, use double-dollar quoting ($$) or parameterized queries instead of single quotes to avoid JS string escaping issues.
- For PostgreSQL migrations, always use
IF NOT EXISTS/CREATE OR REPLACE/DO $$ BEGIN ... EXCEPTION WHEN ... END $$patterns to make migrations idempotent.