Skip to content

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

LayerTechnologyVersion
RuntimeNode.js22+
Package ManagerYarn4 (Berry)
Build orchestratorTurborepo2.x
Build (packages/apps)tsupLatest
Build (web UI)Vite6
APItRPCv11
ValidationZodLatest
Frontend frameworkVue3.5
UI ComponentsPrimeVue4 (Aura)
State managementPiniaLatest
Test frameworkVitestLatest
Process executionexeca9 (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 trigger

Key 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 check

Server → 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

Released under the MIT License.