# Platform architecture

> Monorepo runtime areas, service boundaries, Valkey event decoupling, and nginx gateway routing between web, API, realtime, and storage.

- Repository: Paca-AI/paca
- GitHub: https://github.com/Paca-AI/paca
- Human docs: https://www.grok-wiki.com/public/docs/paca-ai-paca-f238b2ab3d25
- Complete Markdown: https://www.grok-wiki.com/public/docs/paca-ai-paca-f238b2ab3d25/llms-full.txt

## Source Files

- `docs/architecture/overview.md`
- `docs/architecture/service-boundaries.md`
- `docs/architecture/repository-structure.md`
- `deploy/nginx/gateway.conf`
- `services/api/internal/transport/http/router/router.go`
- `docs/architecture/database-schema.md`

---

---
title: "Platform architecture"
description: "Monorepo runtime areas, service boundaries, Valkey event decoupling, and nginx gateway routing between web, API, realtime, and storage."
---

Paca ships as a single monorepo whose production stack runs behind an nginx `gateway` container on port 80. The gateway terminates public HTTP, rate-limits API traffic, proxies `/api/` to `services/api` (Go + Gin on `:8080`), `/ws/` to `services/realtime` (Socket.IO on `:3001`), `/storage/` to MinIO or an external S3 endpoint, and `/` to `apps/web`. PostgreSQL holds durable product state; Valkey carries cache, pub/sub fan-out, and durable streams that decouple write paths from realtime delivery and agent orchestration.

## Monorepo layout

Runtime code, deploy assets, and documentation live in clearly separated top-level areas:

:::files
paca/
├── apps/
│   ├── web/          # React + TanStack Start SPA
│   ├── mcp/          # @paca-ai/paca-mcp npm package
│   └── e2e/          # Playwright stack verifier (not deployed)
├── services/
│   ├── api/          # Go + Gin system of record
│   ├── realtime/     # Bun + Socket.IO fan-out edge
│   └── ai-agent/     # Python + FastAPI + OpenHands (optional)
├── plugins/local/    # On-disk WASM + frontend + MCP bundles
├── deploy/           # Compose files + nginx/gateway.conf
├── docs/             # Architecture and guides
└── scripts/          # Install and plugin helpers
:::

| Area | Deployed | Stack | Primary role |
|------|----------|-------|--------------|
| `apps/web` | Yes (or CDN) | React, TanStack Start, shadcn/ui | Browser UI, session cookies, Socket.IO client |
| `apps/mcp` | External | Node MCP server | AI tool bridge to `/api/v1` over HTTP + API key |
| `apps/e2e` | No | Playwright | Cross-stack browser verification |
| `services/api` | Yes | Go, Gin, GORM | Business logic, auth, PostgreSQL, plugins, event publishing |
| `services/realtime` | Yes | Bun, Socket.IO, ioredis | WebSocket delivery; no business-rule mutations |
| `services/ai-agent` | Optional | Python, FastAPI, OpenHands | Agent conversation execution in Docker |

<Note>
`apps/e2e` exercises the full gateway-routed stack but is versioned alongside `apps/web` and never ships as a runtime container.
</Note>

## Production topology

`deploy/docker-compose.prod.yml` defines the default service graph. All user-facing HTTP enters through `gateway`; backend services communicate on the internal Docker network.

```mermaid
flowchart TB
  subgraph clients [Clients]
    Browser[Browser]
    MCPClient[MCP client]
  end

  subgraph gateway_layer [gateway nginx :80]
    GW[gateway.conf routes]
  end

  subgraph apps_layer [Application runtimes]
    Web[apps/web :3000]
    API[services/api :8080]
    RT[services/realtime :3001]
    Agent[services/ai-agent :8080]
    MCP[apps/mcp external]
  end

  subgraph data_layer [Infrastructure]
    PG[(PostgreSQL)]
    VK[(Valkey)]
    S3[(MinIO or S3)]
  end

  Browser --> GW
  MCPClient --> MCP
  MCP --> API

  GW -->|"/"| Web
  GW -->|"/api/"| API
  GW -->|"/ws/"| RT
  GW -->|"/storage/"| S3
  GW -->|"/plugins/"| PluginFE[frontend_plugins volume]
  GW -->|"/plugins-mcp/"| PluginMCP[mcp_plugins volume]

  Web --> API
  Web --> RT
  API --> PG
  API --> VK
  API --> S3
  RT --> VK
  RT --> API
  Agent --> VK
  Agent --> PG
  Agent --> API
  API -.->|agent triggers| Agent
```

Optional scaling flags from Compose:

| Flag | Effect |
|------|--------|
| `--scale postgres=0` | Use external `DATABASE_URL` |
| `--scale minio=0` | Use `STORAGE_PROVIDER=s3` with AWS credentials |
| `--scale ai-agent=0` | Disable agent orchestration |
| `--scale web=0` | Serve SPA from CDN; gateway still handles `/api/`, `/ws/`, `/storage/` |

## Gateway routing

`deploy/nginx/gateway.conf` is mounted read-only at `/etc/nginx/conf.d/default.conf`. It is the single public entrypoint.

| Public path | Upstream | Behavior |
|-------------|----------|----------|
| `/api/` | `api:8080` | Forwards prefix unchanged (`/api/v1/...` → `/api/v1/...`). Rate limit 100 req/s per IP, burst 50. |
| `/ws/` | `realtime:3001` | Strips `/ws/` prefix; WebSocket upgrade via `$connection_upgrade` map. Read timeout 3600s. |
| `/storage/` | `minio:9000` | Rewrites `Host` to `minio:9000` for SigV4 validation. `client_max_body_size 0` for large uploads. |
| `/plugins/` | `frontend_plugins` volume | Serves module-federation bundles only (no WASM). |
| `/plugins-mcp/` | `mcp_plugins` volume | Serves ESM MCP plugin bundles. |
| `/` | `web:3000` | SPA + dev HMR WebSocket upgrade. Default `client_max_body_size 10m`. |

Socket.IO clients connect through the gateway:

```ts
import { io } from "socket.io-client";

const socket = io("http://localhost", {
  path: "/ws/socket.io",
  withCredentials: true,
});
```

Presigned object URLs returned by the API rewrite internal `minio:9000` hosts to `STORAGE_PUBLIC_URL` (for example `http://localhost/storage`) so browsers hit the gateway `/storage/` location.

## Service boundaries

Ownership rules keep transactional logic centralized and edges stateless.

### `services/api` — system of record

- Owns business workflows: projects, tasks, sprints, views, documents, members, notifications, agents, plugins.
- Exposes versioned REST at `/api/v1/*` (see router groups in `services/api/internal/transport/http/router/router.go`).
- Authenticates JWT cookies, bearer tokens, and API keys; enforces global and project-scoped permissions.
- Runs embedded SQL migrations on every startup (`services/api/migrations/`).
- Loads WASM plugins (wazero), registers proxy routes at `/api/v1/plugins/:pluginId/*path`, and coordinates S3-compatible attachments.
- Publishes domain events to Valkey; runs in-process stream consumers for activity and notification side effects.

### `services/realtime` — delivery edge

- Accepts authenticated Socket.IO connections; verifies tokens by calling `GET /api/v1/users/me/global-permissions` (no local JWT validation).
- Subscribes to Valkey Pub/Sub channel `paca.events` and emits to namespace rooms.
- Does not mutate PostgreSQL or apply business rules.

### `services/ai-agent` — agent execution (optional)

- Consumes `paca:agent:triggers` with consumer group `ai-agent-workers`.
- Spawns one OpenHands Docker container per active conversation.
- Persists `agent_conversation_events` directly to PostgreSQL; publishes live updates to `paca.events` Pub/Sub and durable entries to `paca:agent:events` stream.
- Does not bypass the API for product state changes.

### `apps/mcp` — external integration surface

- Runs outside the Compose stack as `@paca-ai/paca-mcp`.
- Translates MCP tool calls into REST requests against `services/api`.
- Loads plugin-contributed MCP tools from gateway-served `/plugins-mcp/` bundles.

<Warning>
Keep state-changing business logic in `services/api`. `services/realtime` only delivers events; `services/ai-agent` executes conversations and reports results through API-owned tables and events.
</Warning>

## Valkey event decoupling

Valkey serves three roles: namespaced cache (`paca:` prefix), immediate Pub/Sub fan-out, and durable Streams for asynchronous workers.

### Pub/Sub — realtime path

| Channel | Publisher | Consumer | Purpose |
|---------|-----------|----------|---------|
| `paca.events` | `services/api`, `services/ai-agent` | `services/realtime` | Immediate Socket.IO fan-out |

Message shape:

```json
{
  "type": "task.comment.added",
  "payload": {
    "project_id": "...",
    "task_id": "...",
    "...": "..."
  }
}
```

`services/realtime` routes by type prefix:

| Event prefix | Socket.IO room | Required permission |
|--------------|----------------|---------------------|
| `task.*`, `agent.*`, `github.*` | `project:<projectId>:tasks` | `tasks.read` |
| `doc.*` | `project:<projectId>:docs` | `docs.read` |
| `notification.*` | `user:<userId>:notifications` | (user-scoped) |

Room membership is resolved once on `join`; the subscriber performs no per-message permission checks.

### Streams — durable async pipelines

| Stream key | Producer | Consumer | Purpose |
|------------|----------|----------|---------|
| `paca.task_activities` | `services/api` | API `ActivityConsumer` (`api.activity_writer`) | Persist system task activities to PostgreSQL |
| `paca.doc_activities` | `services/api` | API `DocActivityConsumer` (`api.doc_activity_writer`) | Persist doc activities |
| `paca.task_assignments` | `services/api` | API `NotificationConsumer` (`api.notification_writer`) | Create notifications + publish `notification.created` |
| `paca:agent:triggers` | `services/api` | `services/ai-agent` (`ai-agent-workers`) | Agent trigger and control messages |
| `paca:agent:events` | `services/ai-agent` | (durable log) | Ordered conversation event stream |
| `paca.analytics` | `services/api` | External / future | Analytics and audit |

Representative domain event types published through these paths include `task.created`, `task.updated`, `doc.updated`, `notification.created`, and agent topics such as `agent.task_assigned`, `agent.conversation.started`, and `agent.message`.

### Cache and session state

The API wraps Valkey with a `paca:` namespace for entity caches (projects, sprints, tasks, roles) and refresh-token storage. Cache TTLs are configurable per resource; see the configuration reference for env vars.

```mermaid
sequenceDiagram
  participant Web as apps/web
  participant API as services/api
  participant VK as Valkey
  participant RT as services/realtime
  participant PG as PostgreSQL

  Web->>API: PATCH /api/v1/projects/:id/tasks/:taskId
  API->>PG: Commit task update
  API->>VK: XADD paca.task_activities
  API->>VK: PUBLISH paca.events (task.updated)
  VK-->>RT: message on paca.events
  RT->>Web: emit event to project:uuid:tasks
  VK-->>API: ActivityConsumer reads stream
  API->>PG: INSERT task_activities row
```

## API surface through the gateway

All authenticated REST traffic uses the `/api` prefix. Public health check:

:::endpoint GET /api/healthz
Liveness probe used by API container health checks and operations scripts.
:::

Versioned resources mount under `/api/v1`:

| Route group | Examples | Auth |
|-------------|----------|------|
| `/api/v1/auth` | `login`, `refresh`, `logout` | Public except `logout` |
| `/api/v1/users` | `/me`, `/me/api-keys` | JWT or API key |
| `/api/v1/admin` | users, global roles, plugins | Global permissions |
| `/api/v1/projects` | CRUD, members, roles | Project-scoped permissions |
| `/api/v1/projects/:projectId/tasks` | tasks, activities, attachments | `tasks.read` / `tasks.write` |
| `/api/v1/projects/:projectId/views` | sprint, backlog, timeline views | `sprints.read` / `sprints.write` |
| `/api/v1/projects/:projectId/docs` | folders, documents, snapshots | `docs.read` / `docs.write` |
| `/api/v1/projects/:projectId/agents` | agent CRUD, MCP servers, skills | `agents.read` / `agents.write` |
| `/api/v1/plugins/:pluginId/*path` | WASM plugin proxy | Per-route manifest auth |

Plugin HTTP routes and MCP tools both resolve to API paths; the gateway never executes plugin WASM—only the API container mounts `backend_plugins`.

## Storage and plugins

**Object storage.** The API generates presigned upload/download URLs against MinIO (default) or AWS S3 (`STORAGE_PROVIDER=s3`). Files are tracked in PostgreSQL `files` with `storage_key` and `bucket`; task and document attachments reference those rows.

**Plugin artifacts.** Three volumes split plugin surfaces:

| Volume mount | Served by | Contents |
|--------------|-----------|----------|
| `backend_plugins` → API `/plugins` | API only | WASM binaries, SQL migrations |
| `frontend_plugins` → gateway `/var/www/plugins` | nginx | Module-federation JS/CSS |
| `mcp_plugins` → gateway `/var/www/plugins-mcp` | nginx + MCP server | ESM MCP bundles |

WASM and migrations are not exposed over HTTP; only frontend and MCP bundles are gateway-routable.

## Agent pipeline

Agent triggers originate in the API (task assignment, comment @mention, chat message, description-write). The API appends flat fields to `paca:agent:triggers`; `services/ai-agent` reads via consumer group `ai-agent-workers`, runs OpenHands in isolated Docker containers, writes conversation history to PostgreSQL, and pushes live `agent.*` events to `paca.events` for the web monitoring UI.

Control messages (`agent.stop`) use the same trigger stream. The API remains authoritative for agent configuration, conversation listing, and stop/send-message REST endpoints under `/api/v1/projects/:projectId/conversations`.

## Verification signals

| Check | Command / endpoint | Expected |
|-------|-------------------|----------|
| Gateway up | `curl -s -o /dev/null -w "%{http_code}" http://localhost/` | `200` |
| API healthy | `GET /api/healthz` | `200` |
| Realtime healthy | `GET /ws/healthz` (proxied) | `200` |
| Valkey reachable | API and realtime containers healthy | Compose `depends_on: valkey: service_healthy` |
| Socket.IO path | Browser devtools WS to `/ws/socket.io` | `101 Switching Protocols` |
| Migrations applied | API startup logs | `schema migrations applied` |

## Related pages

<CardGroup>
  <Card title="Overview" href="/overview">
    Runtime surfaces, MCP, and the shortest path from install to first API key.
  </Card>
  <Card title="Deploy production" href="/deploy-production">
    Production Compose topology, secret generation, and optional service scaling.
  </Card>
  <Card title="Local development" href="/local-development">
    Dev Compose stack, hot-reload commands, and port map through the gateway.
  </Card>
  <Card title="Realtime events" href="/realtime-events">
    Socket.IO auth, Valkey pub/sub fan-out, rooms, and event payloads.
  </Card>
  <Card title="AI agents" href="/ai-agents">
    Agent triggers, OpenHands lifecycle, Docker isolation, and Valkey streams.
  </Card>
  <Card title="Plugin system" href="/plugin-system">
    WASM sandbox, frontend extension points, and marketplace install lifecycle.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Environment variables across API, web gateway, realtime, and ai-agent.
  </Card>
</CardGroup>
