System Architecture
This page is for contributors who need a map of the codebase before diving in. It covers how the major subsystems relate — the event bus, gateway, plugin host, session manager, and access control layer — and links to deeper explanations where the design gets interesting.
HoloMUSH is a modern MUSH platform with an event-oriented architecture, dual protocol support (telnet + web), and extensibility through plugins.
High-Level Overview
Section titled “High-Level Overview”Core Components
Section titled “Core Components”Event System
Section titled “Event System”The event bus is the heart of HoloMUSH. All game actions produce events that flow through the system:
| Concept | Description |
|---|---|
| Events | Immutable records with ULID ordering |
| Streams | Logical channels (typically per-location) |
| Subscribe | Clients and plugins register for event types |
| Replay | Reconnecting clients catch up from their last seen event |
Events enable:
- Decoupling - Components communicate through events, not direct calls
- Persistence - All actions are recorded in an append-only audit log
- Plugins - Extensions react to and emit events
Event Ordering Model
Section titled “Event Ordering Model”HoloMUSH uses per-stream ordering, not a global event loop. This is a deliberate architectural choice:
| Approach | Trade-off |
|---|---|
| Global event loop | Simple consistency, but bottleneck at scale |
| Per-stream order | Parallelism across streams, ordering where it matters (chosen) |
Guarantees:
- Events MUST be delivered in order within a stream
- Events across different streams have no ordering guarantee
- Clients reconnecting MUST receive missed events in order
Why this works for MUSHes: Players in the same location need to see “Alice says hi” before “Bob says yo” if Alice spoke first. Players in different locations don’t care about each other’s event ordering — and shouldn’t be blocked by it.
Stream Types
Section titled “Stream Types”Subjects use NATS dot-style notation: events.<game_id>.<domain>.<entity-id>[.<facet>...].
Producers emit domain-relative references (e.g. location.01ABC) and the host qualifies
them into fully-qualified JetStream subjects on the way in.
| Stream (domain-relative) | Full subject form | Content | Subscribers |
|---|---|---|---|
location.<id> | events.<gid>.location.<id> | Poses, says, arrivals, departures | Characters in location |
character.<id> | events.<gid>.character.<id> | Private messages, system notifications | That character |
scene.<id>.ic / scene.<id>.ooc | events.<gid>.scene.<id>.ic / .ooc | Scene IC / OOC roleplay | Scene participants |
channel.<name> | events.<gid>.channel.<name> | Channel messages (future) | Channel members |
Cross-Location Events
Section titled “Cross-Location Events”Movement between locations produces events in the destination stream. If departure visibility is needed in the origin location, that requires a separate event to the origin stream. Each stream maintains independent ordering.
Session Manager
Section titled “Session Manager”Handles connection lifecycle with reconnection support:
Sessions persist across disconnects, allowing players to resume where they left off.
World Engine
Section titled “World Engine”Manages the virtual world structure:
| Entity | Description |
|---|---|
| Location | A place in the world |
| Exit | Connection between locations (bidirectional) |
| Character | Player-controlled entity with location |
| Object | Items that can be in locations or inventories |
Plugin System
Section titled “Plugin System”Two-tier architecture for extensibility:
| Tier | Technology | Use Case |
|---|---|---|
| Lua Scripts | gopher-lua | Commands, simple behaviors |
| Go Plugins | go-plugin (gRPC) | Complex systems (combat, AI) |
Plugins declare ABAC policies and the engine enforces them:
# Plugin manifestname: combat-systempolicies: - name: "combat-access" dsl: | permit(principal is plugin, action in ["subscribe", "emit"], resource is stream) when { principal.plugin.name == "combat-system" && resource like "stream:location:*" }; - name: "world-read" dsl: | permit(principal is plugin, action in ["read"], resource is world_object) when { principal.plugin.name == "combat-system" && resource like "world:*" };Note: the stream:location:* form in the DSL is an ABAC resource ID using
<resource_type>:<id> convention — not a pub/sub stream subject. ABAC resource
IDs retain the colon separator; only pub/sub subjects use dot-style.
Plugin types, manifest, and registry
Section titled “Plugin types, manifest, and registry”Three plugin runtimes exist: lua (gopher-lua VM, lightweight event handlers),
binary (hashicorp/go-plugin subprocess, complex services with proto contracts),
and setting (bootstrap-only, configuration plugins with no runtime).
Manifest schema (plugin.yaml): Each plugin declares requires (proto
services it depends on), provides (proto services it implements), and
storage (database tables it needs). The plugin loader performs DAG dependency
resolution to determine load order and validates that all requires are
satisfied by another plugin’s provides.
Service registry: Maps proto service names to implementations. Binary plugins register services over gRPC; Lua plugins register via the Lua host.
Plugin admin commands:
plugin list # List loaded plugins with name, type, versionplugin info <name> # Detailed plugin info (requires, provides, storage, commands)Access Control
Section titled “Access Control”HoloMUSH uses phased access control:
| Phase | Model | Description |
|---|---|---|
| Core | Static roles | Admin/builder/player with fixed perms |
| Full ABAC | Attribute-based | Dynamic policies with attributes |
The core phase provides a simple Check(subject, action, resource) interface
that the full ABAC implementation extends. Default deny — explicit permission
is required for all operations.
Command Authorization
Section titled “Command Authorization”Commands use two-layer authorization at dispatch time:
- Layer 1 — Command Execution:
engine.Evaluate(subject, "execute", "command:<name>")— can this character run this command? - Layer 2 — Capability Pre-Flight:
engine.CanPerformAction(subject, action, resource, scope)per declared capability — does this character have the class of permissions this command needs?
Commands declare capabilities as structured objects:
Capabilities: []command.Capability{ {Action: "write", Resource: "location", Scope: command.ScopeLocal},}Scope: ScopeSelf (default, own character), ScopeLocal (current location), ScopeGlobal (server-wide).
Data Flow
Section titled “Data Flow”Command Execution
Section titled “Command Execution”1. Player types "say Hello"2. Telnet/WebSocket adapter receives input3. Session manager routes to command parser4. Parser identifies command + args5. Command handler validates permissions6. Handler emits "say" event to location stream7. Event bus delivers to: - Other sessions in location - Subscribed plugins8. Sessions render event to their clientsPlugin Event Handling
Section titled “Plugin Event Handling”1. Event emitted to stream2. Event bus notifies subscribed plugins3. Plugin receives event via runtime4. Plugin processes, may: - Emit new events - Query world state - Update plugin storage5. Response events flow back through busTechnology Stack
Section titled “Technology Stack”| Component | Technology | Rationale |
|---|---|---|
| Core | Go | Performance, concurrency, type safety |
| Database | PostgreSQL | Reliability, SQL power, JSONB |
| Events | Custom (ULID ordered) | Append-only audit/notification log |
| Telnet | Go stdlib | Standard protocol support |
| WebSocket | gorilla/websocket | Mature, widely used |
| Web Client | SvelteKit PWA | Modern, offline-capable |
| Lua Runtime | gopher-lua | Lightweight (~40KB per instance) |
| Go Plugins | go-plugin | Process isolation, gRPC transport |
| Observability | OpenTelemetry | Standard tracing/metrics |
Design Principles
Section titled “Design Principles”Event-Driven Architecture
Section titled “Event-Driven Architecture”- All game state changes emit immutable, ordered events
- World state is canonical in PostgreSQL; the event feed is an append-only audit and notification log
- Recovery reads state from the database — see the decided model in ADR
docs/adr/holomush-i4784-world-state-model-decision.md
Interface-First
Section titled “Interface-First”- Components define interfaces, not implementations
- Enables testing with mocks
- Supports future extensibility
Policy-Based Security (ABAC)
Section titled “Policy-Based Security (ABAC)”- Plugins declare ABAC policies in their manifest
- Access Policy Engine enforces policy boundaries
- Default deny — no seed policies for plugins
Gateway Layer (Phase 1.6+)
Section titled “Gateway Layer (Phase 1.6+)”The gateway process (cmd/holomush gateway) is a thin protocol translation
layer. After Phase 1.6 it holds no domain state and contains no verb registry.
What the gateway does
Section titled “What the gateway does”- Accepts incoming connections (telnet, web)
- Translates protocol formats: telnet/ConnectRPC ↔ core gRPC
- Reads
RenderingMetadataoffEventFrameand shapes it for the wire
Single enrichment site: RenderingPublisher
Section titled “Single enrichment site: RenderingPublisher”All rendering metadata is stamped at the RenderingPublisher layer inside the
core server — before events reach JetStream. The gateway reads the pre-stamped
EventFrame.Rendering field and passes it through without any domain knowledge.
This means:
- The gateway MUST NOT import
internal/world,internal/plugin,internal/eventbus, or other domain packages (enforced byINV-GW-1). - Verb labels, categories, and display targets are determined once, at emit time, by the publisher chain — not at delivery time by the gateway.
See Gateway Boundary for the full forbidden-import list
and the gateway_imports_test.go CI tripwire.
Implementation Patterns
Section titled “Implementation Patterns”HTTP Middleware
Section titled “HTTP Middleware”When wrapping http.ResponseWriter (e.g., cookie middleware), the wrapper
MUST implement http.Flusher and Unwrap() — ConnectRPC server-streaming
calls Flush() after each frame and will error if the interface is missing.
Web Client
Section titled “Web Client”See web/CLAUDE.md for SvelteKit-specific patterns including theme system
architecture, shadcn-svelte conventions, Tailwind v4 guidance, and Svelte 5
runes patterns.
Further Reading
Section titled “Further Reading”- Pull Request Guide - Contribution workflow
- Coding Standards - Code conventions
- Plugin Development - Building extensions
- Gateway Boundary - Gateway constraint enforcement and the forbidden-import list
- Event Store - JetStream + PostgreSQL event log design, publish/subscribe/history interfaces
- Event Emit Pipeline - Publisher chain and rendering