Architecture / Architecture Overview
Source on GitHub

Architecture Overview

MangoStudio follows a modular DDD-inspired architecture across three workspaces. This document explains the design decisions, layer responsibilities, and data flow.

Workspace Map

Workspace Role Stack
apps/api Backend API hub Elysia, Better Auth, Kysely + SQLite
apps/frontend Browser SPA React 19, TanStack Router/Query, Tailwind CSS v4, Bun.build() bundle
apps/shared Contracts, and the code both machines run TypeScript types, TypeBox schemas, i18n dictionaries; host-only code behind its own export subpath

Published alongside the application

The repository also hosts the Mango Protocol — the wire contract the hub and the runtime speak — on its own version line and its own protocol-v* release train. It is not one of the three application workspaces and does not follow their lifecycle.

Path Ships as Stack
spec/ The normative spec, schemas and fixtures Markdown, JSON Schema 2020-12
packages/protocol/ @mangostudio/protocol on npm TypeScript, TypeBox
crates/mango-protocol/ mango-protocol on crates.io Rust, serde, tokio
crates/mangostudio-launcher/ mangostudio on crates.io (the launcher) Rust, application version and shared workspace MSRV

Both stable Rust crates belong to the root Cargo workspace and share its lockfile, edition, development toolchain, and lint policy. The protocol and application versions remain independent. crates/mango-protocol/fuzz is an excluded nightly-only workspace with its own lockfile.

packages/protocol/AGENTS.md is the contributor guide for that tree, including the contract-change procedure every wire change follows. docs/protocol/ holds the adoption guides.

API Module Architecture

The API is organized into domain modules under apps/api/src/modules/. Each module follows a DDD-inspired structure:

modules/<domain>/
  application/     → Use-case services (orchestration logic)
  domain/          → Domain rules and entities (optional — omitted for simple modules)
  http/            → Elysia route definitions
  infrastructure/  → Repository/persistence implementations

Layer Responsibilities

application/ — Orchestration services that implement business use cases. They coordinate between repositories, services, and domain logic. Examples:

  • stream-text-turn.ts — the main generation loop orchestrating providers, tools, and continuation.
  • create-chat.ts — creates a new chat with ownership validation.
  • prompt-composer.ts — assembles system prompts with rule composition.

domain/ — Pure functions and types describing domain invariants. Present only when business rules need isolation from infrastructure. Examples:

  • chat-ownership.ts — ownership assertion rules.
  • connector.ts — connector entity validation.

http/ — Elysia route plugins that handle HTTP concerns (request parsing, auth guards, response serialization). They delegate to application services.

infrastructure/ — Kysely-based repository implementations that abstract SQL queries behind typed interfaces. One repository per aggregate root.

Modules

Module Layers Responsibilities
chats A/D/H/I Chat CRUD, ownership, context compaction
messages A/D/H/I Message persistence, gallery queries
generation A/D/H/I Streaming text turns, image generation, model resolution
connectors A/D/H/I API key management, model enable/disable, secret persistence
app-settings A/H/I Global user settings (theme, language, generation defaults)
provider-settings A/H/I Per-provider runtime configuration (caching, reasoning, tokens)
tool-settings A/H/I Per-tool enable/disable and parameter overrides
prompt-rules A/H System prompt composition, rule file resolution
attachments A/I File upload, validation, storage, provider delivery
realtime H Cookie-authenticated, user-scoped cache invalidation
activity A/D/H/I Account-wide “what changed” log; many modules write, one reads

A = Application, D = Domain, H = HTTP, I = Infrastructure

When to Skip Layers

Simple modules skip layers that would add ceremony without value:

  • No domain/ — when business rules are self-evident (e.g., app-settings is a simple CRUD wrapper with normalization).
  • No http/ — when the module is purely infrastructural (e.g., generated-images/ only has a repository).
  • No application/ — only for bridge modules whose policy and data work is delegated to existing services (for example, realtime delegates fan-out to the realtime bus and ownership checks to chats).

Data Flow

Typical API Request

Browser (Eden Treaty)
  │
  ▼
Elysia Route (http/)
  ├─ requireAuth middleware
  ├─ TypeBox body validation
  │
  ▼
Application Service (application/)
  ├─ Ownership check (domain/)
  ├─ Business logic
  ├─ Repository calls (infrastructure/)
  │
  ▼
Response (typed via shared contracts)

Streaming Request (Chat)

Browser (fetch + ReadableStream)
  │
  ▼
respond-stream-routes.ts
  ├─ Pre-flight validation (ownership, content, model)
  │
  ▼
stream-text-turn.ts (orchestrator)
  ├─ Resolve model + provider
  ├─ Compose prompt rules
  ├─ Decide continuation strategy
  ├─ Call provider.generateAgentTurnStream()
  ├─ Execute tools (if model calls them)
  ├─ Enforce subagent delegation response contract (delegate_to_agent)
  ├─ Feed tool results back to provider
  └─ Persist turn → yield SSE events

Hub/Runtime Execution

Filesystem tools, shell commands, freshness tracking, and checkpoint effects execute through the versioned runtime protocol. The API remains the hub for workdir policy, orchestration, and durable checkpoint state. See hub-runtime.md for the ownership table, protocol lifecycle, and transport roadmap.

Every runtime, Local included, is the cargo-built mangostudio-runtime from crates/mangostudio-runtime. The hub spawns Local on its own machine over stdio: the sibling binary beside a standalone hub, or in a source checkout the newest target/debug or target/release build, which bun run dev builds first. MANGOSTUDIO_RUNTIME_BINARY overrides both, and a missing binary fails the connect with cargo build -p mangostudio-runtime rather than falling back to anything.

Realtime Invalidation

/api/ws is an invalidation-only bridge from the user-scoped in-process bus to authenticated browser tabs. Entity data continues to travel over HTTP. See realtime.md for the protocol, security boundaries, limits, degradation model, and topic-extension workflow.

Subagent Delegation Enforcement

Subagent delegation is executed through the delegate_to_agent tool. The orchestrator enforces a response contract so the parent agent always receives a usable final result, even when a provider ends a subagent run after tool calls without emitting assistant text.

  • Response contract: the delegation tool result must be a non-empty SubagentRunResult with a non-empty summary, trace.lastMessage, and at least one assistant message in trace.messages.
  • Tool-only runs: if a subagent executes tools but produces no assistant text, the runtime synthesizes a summary (including the tool names executed) and emits a subagent_text event for UI consistency.
  • Main-agent verification: the orchestrator validates that the delegation result satisfies the response contract; if it is missing or invalid, it retries up to 3 times with exponential backoff and stricter output requirements, then falls back to a standardized failure result.
  • Response persistence: subagent text deltas and final results are stored in an in-memory cache keyed by tool call id so the orchestrator can recover a usable result if the final payload is missing or invalid.
  • Debug logging: structured lifecycle logs are emitted with [subagent] and delegation enforcement logs with [subagent-delegation], including call ids, tool counts, and summary lengths (without logging tool result contents).

Cross-Cutting Concerns

Auth

Better Auth handles sessions via cookie-based authentication. The auth-middleware.ts plugin decorates routes with requireAuth, which validates the session and injects the user context. External automation uses scoped API keys: callers send x-api-key, apiKeyGuard enforces the per-user toggle and scope, and Better Auth resolves the key into the same session path so requireAuth stays unchanged. See reference/external-api.md.

Rate Limiting

In-memory rate limiter (rate-limit.ts) that counts requests per (bucket, client id). A classify function (rate-limit-policy.ts) sorts each request into a named bucket — health, auth, general (cookie traffic), and api-key (requests with x-api-key) use independent counters so automation cannot starve browser sessions on the same IP. general is the widest of them: the narrower buckets each bound one expensive operation, while general bounds ordinary page loads, and one page load is many requests. Client id is the caller IP for every bucket today (per-key ids need a verified key id after apiKeyGuard; see issue #737). Limited requests return a 429 ApiErrorResponse (code: RATE_LIMITED) with a Retry-After header. Expired counters are cleaned up lazily. Proxy headers affect the client id only when trustProxy is explicitly enabled behind a trusted reverse proxy.

Error Handling

  • API errors use ApiErrorResponse from @mangostudio/shared/errors: { error: string, code?: string, details?: Record<string, string> }. The HTTP status is carried by the response, not the body.
  • Streaming errors use SSEErrorEvent from @mangostudio/shared/streaming: { type: 'error', error, done: true }.
  • Domain errors extend Error with typed codes (e.g., ChatNotFoundError, ToolParameterError).
  • The centralized error-handler.ts plugin maps thrown errors to HTTP responses.

Validation

  • Request bodies validated via TypeBox schemas in shared contracts.
  • Response shapes validated in integration tests via Value.Check.
  • Attachment uploads validated for MIME type, magic bytes, size, and UTF-8 correctness.

Environment Installs

Environment installation is a guarded local-only execution surface built from code-owned recipes, bounded downloads and output, user-scoped audit records, and raw SSE logs. See environment-installs.md for its threat model and opt-in controls.

Provider Architecture

Providers implement the AIProvider interface. The provider registry maps provider types to implementations. The bundled providers are Gemini, OpenAI, OpenAI-compatible, Anthropic, DeepSeek and ChatGPT.

Cursor is registered but deprecated. It advertises no models and refuses execution; the supported path is the external-agent adapter, where Cursor’s own CLI owns the model, the tools and the approvals. resolve-model.ts is where that refusal is raised, before provider resolution, so every path to a provider passes through it. See ../providers/cursor.md.

Key abstraction layers:

stream-text-turn.ts (orchestrator)
  │
  ├─ resolve-model.ts → selects provider + model, refuses deprecated ones
  ├─ continuation-runtime.ts → decides strategy
  │
  ▼
Provider Implementation (e.g., gemini/interactions-stream.ts)
  ├─ generateAgentTurnStream() → yields AgentEvent[]
  ├─ continuation-envelope.ts → serializes cursor state
  │
  ▼
Provider Wire Format (Gemini / OpenAI / Anthropic / DeepSeek)

See continuation.md for the full continuation architecture and ../providers/development.md for the provider integration guide.

Frontend Architecture

routes/                → TanStack Router file-based pages
  _authenticated/      → Auth-protected routes
    index.tsx           → Chat (default)
    gallery.tsx         → Image gallery
    settings.tsx        → Settings layout
    studio.tsx          → Image workspace
    environments.tsx    → Environments umbrella layout
    environments/       → Toolchains, agents, health
      library.tsx       → Library section layout (second-level tabs)
      library/          → Coverage matrix, backups, resource detail
    library/            → Redirect stubs for pre-move /library/* bookmarks

features/              → Domain feature modules
  chat/                → Chat UI, components, hooks, services
  environments/        → Environment cards, health, install actions
  gallery/             → Gallery page + queries
  generation/          → Text/image generation hooks + types
  library/             → Coverage matrix, divergence, propagation wizards
  settings/            → Settings sub-modules (app, connectors, providers, tools, prompts)
  sidebar/             → Sidebar + context ring

components/            → Shared UI components
  ui/                  → Design system primitives (Button, Input, Card, etc.)
  layout/              → Header, Layout, ModelSelector, ThinkingToggle
  MarkdownContent.tsx  → Markdown rendering with syntax highlighting

Frontend Data Flow

TanStack Query (cache + invalidation)
  │
  ▼
Eden Treaty Client (typed API client)
  │
  ▼
Elysia API (server)

Shared Package

The shared package is framework-agnostic and importable by both API and frontend:

shared/src/
  contracts/           → Barrel export of all contract types
  <module>/            → Per-module contracts + TypeBox schemas
  realtime/            → WebSocket invalidation messages, topics, and close codes
  runtime-contract/    → Hub/runtime methods, capabilities, events, manifest, errors
    methods/           → One schema module per method family (fs, mcp, library, …)
    generated/         → Emitted catalog + schemas a non-TypeScript runtime is built from
  streaming/           → SSE event types + schemas
  types/               → Domain types (provider, agent-events, gallery)
  i18n/                → Portuguese/English dictionaries + type system
  test-utils/          → Shared mock factories

Cross-workspace imports use package names (@mangostudio/shared), never relative paths.

Database

SQLite via Kysely with 18 migrations covering:

  • Core tables: chats, messages, generated_images, chat_attachments
  • Auth tables: Better Auth standard schema (5 tables)
  • Settings tables: user_app_settings, user_provider_settings, user_tool_settings

Columns use camelCase, tables use snake_case. Kysely type aliases: <Entity>Select, <Entity>Insert, <Entity>Update.

Standalone Build

bun run build --binary compiles the API into platform-specific binaries via bun build --compile, with the frontend embedded, and stages the cargo-built mangostudio-runtime beside each. Distribution builds all 8 platforms (linux/windows/darwin × x64/arm64 + glibc/musl) from per-target runtimes (--runtime-dir); a local build covers the host’s own platform (--platform <host>) — see releasing.md. Database defaults to ~/.mango/database.sqlite.

Each runtime is a native binary built with fat LTO and panic = "unwind" (handler panic isolation depends on it): gnu Linux targets link at the glibc 2.17 floor, musl targets are static, Windows links the MSVC C runtime statically, and every binary reports the release version it was stamped with. Release assets, runtime binaries included, are listed in an attested SHA256SUMS; runtime-metrics.md records their sizes.