Architecture
Overview
Runeya is a TypeScript/JavaScript monorepo organized in three application layers.
┌──────────────────────────────────────────────────────┐
│ Web UI (Vue 3) │
│ Vite 6 · PrimeVue 4 · Pinia · tRPC Client v11 │
└────────────────────────┬─────────────────────────────┘
│ tRPC (HTTP + WebSocket)
┌────────────────────────▼─────────────────────────────┐
│ Local Server (Express) │
│ tRPC v11 · JSON persistence │
└────────────────────────┬─────────────────────────────┘
│ HTTP Bearer + WebSocket
┌────────────────────────▼─────────────────────────────┐
│ Agent (Express) │
│ tRPC v11 · execa · stateless · ring buffer logs │
└────────────────────────┬─────────────────────────────┘
│ execa (child_process)
┌────────────────────────▼─────────────────────────────┐
│ Your processes / your applications │
└──────────────────────────────────────────────────────┘Tech Stack
| Layer | Technology | Version |
|---|---|---|
| Runtime | Node.js | 22+ |
| Package Manager | Yarn | 4 (Berry) |
| Build orchestrator | Turborepo | 2.x |
| Build (packages/apps) | tsup | Latest |
| Build (web UI) | Vite | 6 |
| API | tRPC | v11 |
| Validation | Zod | Latest |
| Frontend framework | Vue | 3.5 |
| UI Components | PrimeVue | 4 (Aura) |
| State management | Pinia | Latest |
| Test framework | Vitest | Latest |
| Process execution | execa | 9 (ESM) |
Monorepo Packages
runeya/
├── apps/
│ ├── agent/ # @runeya/apps-agent — process executor
│ ├── cli/ # @runeya/apps-cli — runeya binary (single entry point)
│ ├── docs/ # @runeya/apps-docs — this documentation site
│ ├── server/ # @runeya/apps-server — local server
│ └── web/ # @runeya/apps-web — Vue 3 interface
├── packages/
│ ├── shared/ # @runeya/packages-shared — shared Zod schemas
│ ├── mcp-server/ # @runeya/packages-mcp-server — MCP Server (Model Context Protocol)
│ ├── runner-native/ # @runeya/packages-runner-native — process execution via execa
│ └── runner-docker/ # @runeya/packages-runner-docker — process execution via Docker
└── scripts/
├── check-monorepo/ # Monorepo convention validation
├── generate-ci/ # GitHub Actions workflow generation
├── tsup-config/ # Shared tsup config
└── retrigger-all-build/ # Cascade rebuild triggerKey Architectural Patterns
tRPC v11 — Async Generator Subscriptions
typescript
// ✅ Correct pattern (tRPC v11)
mySubscription: protectedProcedure.subscription(async function* () {
for await (const event of eventEmitter) {
yield event
}
})
// ❌ Obsolete pattern (tRPC v10)
mySubscription: protectedProcedure.subscription(() => observable(...))Server Singletons
State services are module-level singletons:
typescript
// apps/server/src/services/service-store.ts
export const serviceStore = new ServiceStore()
// apps/agent/src/process-manager.ts
export const processManager = new ProcessManager()Atomic JSON Writes
typescript
// 1. Write to .tmp
await fs.writeFile(path + '.tmp', JSON.stringify(data))
// 2. Atomic rename
await fs.rename(path + '.tmp', path)Lazy Deployment (server → agent)
The agent only receives a service configuration when that service is started for the first time — never at server boot.
typescript
// In process.start (server)
await agentClient.process.deploy(resolvedConfig) // idempotent
await agentClient.process.start({ id: serviceId })Server ↔ Agent Communication
HTTP (requests/commands)
POST /api/trpc/process.deploy Authorization: Bearer <passphrase>
POST /api/trpc/process.start Authorization: Bearer <passphrase>
POST /api/trpc/process.stop Authorization: Bearer <passphrase>WebSocket (real-time subscriptions)
ws://localhost:9546 — tRPC subscriptions (logs, status, metrics)
passphrase in Authorization header (never query params)
ws://localhost:9546/health — dedicated health checkServer → Agent Authentication
Agent startup:
passphrase = crypto.randomBytes(32).toString('hex')
Server registration:
agents.json ← encrypt(passphrase, AES-256-GCM)
Each request:
Authorization: Bearer <passphrase>
Agent validation:
crypto.timingSafeEqual(expected, received)Zod Schemas — Single Source of Truth
All API types are defined in packages/shared via Zod and automatically inferred by tRPC:
typescript
// packages/shared/src/schemas/service.schema.ts
export const ServiceConfigSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(128),
command: z.string().max(2048),
// ...
})
export type ServiceConfig = z.infer<typeof ServiceConfigSchema>
// → automatically inferred type, no duplication