-
Notifications
You must be signed in to change notification settings - Fork 1
System Architecture
The SecondLayer platform is a Ukrainian legal tech monorepo providing AI-powered legal document analysis, semantic search over court decisions (EDRSR), legislation retrieval, parliament data, business registry lookups, consultations, and payments. It employs a Triple Transport System for its Model Context Protocol (MCP) servers, enabling communication via MCP stdio, HTTP REST API, and Server-Sent Events (SSE). The architecture is built on a modular, service-oriented foundation with a clean separation between domain ports, adapters, and infrastructure.
SecondLayer/
├── mcp_backend/ # Primary MCP server - court cases, legal docs, legislation, OSINT, due diligence
├── mcp_rada/ # Parliament data server (deputies, bills, legislation, voting)
├── mcp_openreyestr/ # State Register server (legal entities, beneficiaries, debtors, notaries)
├── lexwebapp/ # Web frontend (React 19, Vite 8, TailwindCSS, Zustand 5)
├── packages/shared/ # Shared TypeScript package (@secondlayer/shared)
├── platform/ # Platform service (separate frontend, Vite + TailwindCSS)
├── mobile/ # Mobile app (Flutter/Dart)
├── services/
│ ├── edrsr-fulltext-worker/ # Python worker for EDRSR full-text indexing
│ └── opendata-importers/ # Python importers for government open data
├── opendata-sync/ # Scheduled sync service for Ukrainian open data sources
├── terminal-service/ # Terminal service
├── deployment/ # Docker configs, compose files, nginx, manage-gateway.sh
├── scripts/ # Utility scripts (deploy, RADA sync, EDRSR import, testing)
├── tests/ # E2E tests (Playwright)
├── docs/ # Research papers, API docs, reports
├── config/ # MCP client configs
└── legacy/ # Archived code
Each MCP server supports three communication protocols, enabling flexible integration across different client types:
- MCP stdio: Standard MCP protocol for local AI client integration (Claude Desktop, IDE extensions).
-
HTTP API: REST endpoints (
POST /api/tools/:toolName) for web application integration, with batch support (POST /api/tools/batch). -
SSE (Server-Sent Events): Streaming endpoints (
POST /api/tools/:toolName/stream) for long-running operations, plus a dedicated MCP-over-SSE endpoint (/sse) for remote MCP clients.
| Service | Package | Port | Description |
|---|---|---|---|
| mcp_backend | secondlayer-mcp |
3000 | Primary server: court decisions (EDRSR), legislation, semantic search, document analysis, ECHR practice, OSINT, due diligence, workflows |
| mcp_rada | rada-mcp |
3001 | Ukrainian parliament: deputies, factions, committees, bills, legislation texts, voting records |
| mcp_openreyestr | openreyestr-mcp |
3005 | NAIS state registries: legal entities, beneficiaries, debtors, notaries |
| Document Service | - | 3002 | OCR and document processing (Google Cloud Vision) |
| EDRSR FTS Worker | - | - | Python worker for full-text search indexing of court decisions |
| OpenData Sync | opendata-sync |
- | Scheduled sync of government open data sources |
In production, a unified gateway (ENABLE_UNIFIED_GATEWAY=true) aggregates all services behind a single endpoint. Tool routing is handled by the ToolRegistry which maps tool names to either local BaseToolHandler instances or remote services via RemoteServiceClient.
flowchart TD
Client[Client Request] --> Gateway[Unified Gateway / ToolRegistry]
Gateway --> |"no prefix"| Backend[mcp_backend tools]
Gateway --> |"rada_*"| Rada[mcp_rada tools]
Gateway --> |"openreyestr_*"| OpenReyestr[mcp_openreyestr tools]
Backend --> DB[(PostgreSQL + PgBouncer)]
Backend --> Vector[(Qdrant)]
Backend --> Cache[(Redis)]
Backend --> MinIO[(MinIO / S3)]
Rada --> RadaDB[(PostgreSQL - rada schema)]
OpenReyestr --> OpenDB[(PostgreSQL - openreyestr)]
Tool categories registered in mcp_backend:
- Court decisions (search, extended, unified, hybrid, semantic, FTS)
- Legislation (lookup, monitoring, cross-references)
- ECHR practice, court sessions, legal acts
- Due diligence, OSINT proxy, spending data
- Document analysis, vault, workflows
- Open data registries, analyze-data, decision layer
- Nextcloud integration
The shared package (packages/shared/) provides reusable infrastructure used across all three MCP servers. It must be built before other services (cd packages/shared && npm run build).
| Module | Exports | Purpose |
|---|---|---|
utils/logger |
createLogger, logger
|
Winston-based structured logging |
utils/model-selector |
ModelSelector |
Budget-aware model selection across providers |
utils/openai-client |
OpenAIClientManager, getOpenAIManager
|
OpenAI API client with cost tracking |
utils/anthropic-client |
AnthropicClientManager, getAnthropicManager
|
Anthropic API client |
utils/bedrock-client |
BedrockClientManager, getBedrockManager
|
AWS Bedrock client (eu-central-1) |
utils/llm-client-manager |
LLMClientManager, getLLMManager
|
Unified multi-provider LLM interface with streaming, fallback, budget routing |
database/base-database |
BaseDatabase |
PostgreSQL connection pool management |
http/sse-handler |
SSEHandler |
Server-Sent Events streaming helper |
http/base-http-server |
BaseHTTPServer |
Express-based HTTP server with CORS, auth, health checks |
services/base-cost-tracker |
BaseCostTracker |
Per-request API cost tracking |
types |
shared TypeScript types |
AuthenticatedRequest, health check types, tool call types |
The ModelSelector supports three providers with budget-based model routing:
| Budget | OpenAI | Bedrock | Anthropic (direct) |
|---|---|---|---|
| Quick | gpt-5-nano | claude-haiku-4-5 | claude-haiku-4-5 |
| Standard | gpt-5-mini | claude-sonnet-4-6 | claude-sonnet-4-6 |
| Deep | gpt-5.1 | claude-opus-4-6 | claude-opus-4-6 |
Provider strategy is configured via LLM_PROVIDER_STRATEGY (openai-first or bedrock-first). Bedrock fallback models provide automatic failover.
Embeddings use Voyage AI (voyage-multilingual-2) by default.
Services are composed through factory functions that wire up dependencies at startup. This avoids service locators and enables testability.
mcp_backend/src/factories/
├── core-services.ts # createBackendCoreServices() - DB, adapters, query planner, sectionizer, embeddings
├── app-services.ts # createAppServices() - conversations, billing, auth, chat, workflows, consultations
├── billing-services.ts # createBillingServices() - subscriptions, credits, payments
├── tool-services.ts # createToolServices() - ToolRegistry, tool handlers, document processing
└── core-loader.ts # Lazy loading / initialization orchestration
createBackendCoreServices() composes:
-
Database(PostgreSQL connection pool) -
DocumentService(document retrieval and caching) -
EdsrLocalAdapter(multiple domain-specific instances: court decisions, practice, sessions, legal acts, ECHR) -
QueryPlanner,SemanticSectionizer,EmbeddingService -
LegalPatternStore,CitationValidator,ShepardizationService -
LegislationService,LegislationTools -
ReyestrDownloadService,ImportTaskService
createToolServices() registers all tool handlers into the ToolRegistry:
-
CourtDecisionTools,EdsrExtendedTools,EdsrUnifiedSearchTool -
LegalAdviceTools,ProceduralTools,CourtSessionTools -
LegalActsTools,ECHRPracticeTools,CourtStatusTools -
DueDiligenceTools,OsintProxyTools,SpendingTools -
OpenDataTools,OpenDataRegistriesTools,Tier1OpenDataTools -
DocumentAnalysisTools,BatchDocumentTools,VaultTools -
NextcloudTools,DecisionLayerTools,ImportTaskTools -
WorkflowMemoryTools,AnalyzeDataTool,RegistrySearchTool
mcp_rada/src/factories/
└── rada-services.ts # createRadaCoreServices()
Composes: Database, RadaAPIAdapter, ZakonRadaAdapter, CostTracker, DeputyService, FactionService, CommitteeService, BillService, LegislationService, VotingService, CrossReferenceService, MCPRadaAPI.
Adapters isolate external data sources and third-party APIs behind stable interfaces, enabling independent evolution.
| Adapter | Purpose |
|---|---|
EdsrLocalAdapter |
Court decision retrieval from local PostgreSQL/Qdrant (replaces deprecated ZakonOnline API). Instantiated per domain: court decisions, practice, sessions, legal acts, ECHR. |
RadaLegislationAdapter |
Fetches legislation full text from Verkhovna Rada API (zakon.rada.gov.ua), parses HTML with Cheerio. |
OsintProxyAdapter |
Proxies OSINT tool calls to an external OSINT API service via HTTP. |
| Adapter | Purpose |
|---|---|
LLMAdapter |
Adapts the shared LLMClientManager behind the ILLMPort domain interface. |
CacheAdapter |
Adapts Redis client behind the ICachePort domain interface. |
Clean architecture boundary - defines interfaces that infrastructure adapters implement:
-
IDatabase- database access contract -
IEmbeddingPort- vector embedding and similarity search -
ILLMPort- LLM completion (chat, streaming) -
ICachePort- key-value caching
All tools extend BaseToolHandler which defines:
abstract class BaseToolHandler {
abstract getToolDefinitions(): ToolDefinition[];
abstract executeTool(name: string, args: any): Promise<ToolResult | null>;
async executeToolStream?(name: string, args: any, callback: StreamEventCallback): Promise<ToolResult | null>;
}The ToolRegistry aggregates handlers and provides unified dispatch with per-tool timeout configuration. Remote service tools (RADA, OpenReyestr) are routed via RemoteServiceClient.
The frontend (lexwebapp/) is a React 19 application built with Vite 8, TailwindCSS, Zustand 5 for state management, and TanStack Query for server-state caching.
-
Routing: React Router with protected routes and
AuthGuard -
Service Layer: Typed API clients (
AuthService,ConversationService,MCPService,BillingService,ConsultationService,MatterService,WorkflowService, etc.) - State Management: Zustand stores (chat, UI, settings, upload, encryption, consultations, workflows, timers) + TanStack Query for server cache
-
Streaming:
SSEClientfor real-time tool execution progress - UI Components: Custom design system with TailwindCSS
sequenceDiagram
participant UI as UI Component
participant Hook as useMCPTool Hook
participant Service as MCPService
participant SSE as SSEClient
participant API as Backend API
UI->>Hook: executeTool(args)
Hook->>Service: callStream(tool, args)
Service->>SSE: connect(endpoint)
SSE->>API: POST /api/tools/:name/stream
API-->>SSE: SSE Event (Thinking)
SSE-->>Hook: onProgress(step)
Hook-->>UI: Update Rendering
API-->>SSE: SSE Event (Complete)
SSE-->>Service: Final JSON
Service-->>Hook: Result
Hook-->>UI: Final State
| Service | PostgreSQL | Vector Store | Cache | Object Storage |
|---|---|---|---|---|
| mcp_backend | Port 5432 (+ PgBouncer) | Qdrant (6333/6334) | Redis 7 (6379) | MinIO (S3-compatible) |
| mcp_rada | Shared PG, separate schema (rada) |
- | Shared Redis | - |
| mcp_openreyestr | Port 5435 (dedicated) | - | Redis (6382) | - |
Production adds:
- PgBouncer for connection pooling
- Prometheus + Grafana + cAdvisor for monitoring
- Postgres/Redis exporters for metrics
- Coturn for WebRTC (video calls)
- Nextcloud for task management (Deck boards)
| Environment | URL | Notes |
|---|---|---|
| Local |
localhost:3000 / https://local.legal.org.ua
|
Full stack in Docker Compose |
| Production | https://legal.org.ua |
Blue-green deployment |
There is no staging environment.
Production uses blue-green deployment tracked via .active-colors file per service group:
flowchart LR
Nginx[Nginx Reverse Proxy] --> |active color| Active[Active containers]
Nginx -.-> |inactive color| Standby[Standby containers]
subgraph Active
AppBlue[app-prod]
RadaBlue[rada-mcp-app-prod]
OpenBlue[app-openreyestr-prod]
LexBlue[lexwebapp-prod]
end
subgraph Standby
AppGreen[app-prod-green]
RadaGreen[rada-mcp-app-prod-green]
OpenGreen[app-openreyestr-prod-green]
LexGreen[lexwebapp-prod-green]
end
New deployments build the inactive color, run migrations, start containers, then switch Nginx upstreams.
-
ci-local-deploy.yml(on push to main): detect changed services, build, test, deploy to local Docker, health check. -
deploy-prod.yml(after successful local CI or manual): pre-deploy tests, blue-green deploy via SSH, migration, preview, switch, semantic version tagging.
Both pipelines include self-healing via Claude Code agent (creates autofix PRs on failure).
| Dockerfile | Service |
|---|---|
Dockerfile.mono-backend |
mcp_backend |
Dockerfile.mono-rada |
mcp_rada |
Dockerfile.mono-openreyestr |
mcp_openreyestr |
Dockerfile.document-service |
Document processing / OCR |
Dockerfile.edrsr-fulltext-worker |
EDRSR full-text indexing worker |
Dockerfile.opendata-sync |
Open data synchronization |
Dockerfile.terminal-service |
Terminal service |
Dockerfile.local-scraper |
Local data scraping |
Dockerfile.offshore-import |
Offshore data import |
Dockerfile.r-retrieval |
R-based retrieval experiments |
cd deployment
./manage-gateway.sh start local # Start all services
./manage-gateway.sh deploy local # Full rebuild (--no-cache)
./manage-gateway.sh logs local # View logs
./manage-gateway.sh stop local # Stop
./manage-gateway.sh status # Container status
./manage-gateway.sh health # Health checksCompose invocation requires explicit env file:
docker compose -f docker-compose.local.yml --env-file .env.local up -d
docker compose -f docker-compose.prod.yml --env-file .env.prod up -dThe platform supports multiple authentication methods:
- Bearer token (
SECONDARY_LAYER_KEYS) for API clients - JWT session tokens for web users
- Google OAuth
- Authentik (OIDC)
- Diia (Ukrainian digital identity)
- Password (local accounts)
- WebAuthn (passkeys/FIDO2)
| Layer | Technology |
|---|---|
| Runtime | Node.js 20+ (TypeScript 5.3) |
| AI Providers | OpenAI (GPT-5 family), Anthropic (Claude via Bedrock + direct), Voyage AI (embeddings) |
| Databases | PostgreSQL 15 (+ PgBouncer), Redis 7, Qdrant (vectors) |
| Object Storage | MinIO (S3-compatible) |
| Frontend | React 19, Vite 8, TailwindCSS, Zustand 5, TanStack Query |
| Mobile | Flutter (Dart) |
| MCP SDK |
@modelcontextprotocol/sdk v1.27+ |
| Infrastructure | Docker Compose, Nginx, Prometheus, Grafana |
| CI/CD | GitHub Actions (self-hosted runner) |
| Payments | Monobank API (dual UAH/USD balance) |
| Task Management | Nextcloud Deck, Plane |
Relevant source files
- CLAUDE.md
- packages/shared/src/index.ts
- mcp_backend/src/factories/
- mcp_backend/src/adapters/
- mcp_backend/src/domain/ports/
- mcp_backend/src/api/tool-registry.ts
- mcp_backend/src/api/base-tool-handler.ts
- mcp_rada/src/factories/rada-services.ts
- deployment/docker-compose.local.yml
- deployment/docker-compose.prod.yml