Project Chorus - Technical Architecture
June 5, 2026 · View on GitHub
Project Chorus - Technical Architecture
Version: 2.0 Updated: 2026-02-18
1. System Overview
1.1 Positioning
Chorus is a platform for AI Agent and human collaboration, implementing the AI-DLC (AI-Driven Development Lifecycle) methodology. The core philosophy is Reversed Conversation: AI proposes, humans verify.
1.2 Core Capabilities
| Capability | Description |
|---|---|
| Knowledge Base | Project context storage and querying |
| Task Management | Task CRUD, status transitions, Kanban, Task DAG dependencies |
| Assignment Mechanism | Flexible Idea/Task assignment, supporting human and Agent collaboration |
| Proposal Approval | PM Agent creates proposals, humans/Admin approve |
| MCP Server | 50+ tools, Agents connect via MCP protocol (Public/Session/Developer/PM/Admin) |
| Activity Stream | Real-time tracking of all participant actions (with Session attribution) |
| Notification System | In-app notifications with SSE push, preference controls, MCP tools for agents |
| Session Observability | Agent Session + Task Checkin, Kanban/Task Detail displays active Workers in real-time |
| Chorus Plugin | Claude Code plugin, automating Session lifecycle (create/heartbeat/close) |
| Task DAG | Task dependency modeling, cycle detection, @xyflow/react + dagre visualization |
| Global Search | Unified search across 6 entity types with scope filtering and Cmd+K UI (details) |
1.3 Participants
┌─────────────────────────────────────────────────────────────────────┐
│ Chorus Platform │
└─────────────────────────────────────────────────────────────────────┘
↑ ↑ ↑ ↑
│ │ │ │
┌────┴────┐ ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐
│ Human │ │ Agent w/ │ │ Agent w/ │ │ Agent w/ │
│ │ │ PM perms │ │ Dev perms │ │Admin perms│
└─────────┘ └───────────┘ └───────────┘ └───────────┘
Web UI access Claude Code Claude Code Claude Code
Approve proposals Propose tasks Execute tasks Proxy approval
Agent Permission Model:
Agents are no longer locked into three fixed roles. Each Agent carries a permission set built from 5 resources (idea, proposal, document, project, task$) \times 3 \text{actions} ($read, write, admin) = 15 bits. The UI offers three presets plus a Custom option:
- Developer preset (
developer_agent): all*:read+task:write— execute tasks, report work, submit for verification. - PM preset (
pm_agent): Developer preset +idea:write,proposal:write,document:write,project:write— requirements analysis, task breakdown, proposal creation. - Admin preset (
admin_agent): all 15 bits — proxy human actions such as approving Proposals, verifying Tasks, managing Projects. (Warning: dangerous permissions.) - Custom: any combination of the 15 bits (e.g. a read-only auditor, or a PM that can also verify its own tasks).
See §6.3 for the authoritative preset-to-permission table and the effective-permission computation — computeEffectivePermissions(roles, customPermissions) in src/lib/authz/permissions.ts, which returns the union of every preset expansion and any custom permission bits.
2. Tech Stack
2.1 Core Technology Choices
| Layer | Technology | Version | Rationale |
|---|---|---|---|
| Framework | Next.js | 15.x | Full-stack unified, App Router, RSC support |
| Language | TypeScript | 5.x | Type safety, frontend-backend consistency |
| ORM | Prisma | 7.x | Type safety, migration management, good DX, no foreign key constraint design |
| Database | PostgreSQL | 16 | Reliable, JSON support, future pgvector extensibility |
| UI Components | shadcn/ui | - | Based on Radix, customizable, elegant |
| Styling | Tailwind CSS | 4.x | Atomic CSS, rapid development |
| Auth | next-auth | 5.x | OIDC support, deep Next.js integration |
| MCP SDK | @modelcontextprotocol/sdk | latest | Official TypeScript SDK |
| Cache/Pub-Sub | Redis (ioredis) | 7.x | Cross-instance SSE event delivery via ElastiCache Serverless |
| Containerization | Docker Compose | - | One-click local dev setup |
2.2 Development Tools
| Tool | Purpose |
|---|---|
| pnpm | Package management |
| ESLint + Prettier | Code standards |
| Vitest | Unit testing |
| Playwright | E2E testing |
3. System Architecture
3.1 Overall Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Clients │
├──────────────────┬──────────────────┬───────────────────────────┤
│ Web Browser │ PM Agent │ Personal Agent │
│ (Human) │ (Claude Code) │ (Claude Code) │
└────────┬─────────┴────────┬─────────┴─────────┬─────────────────┘
│ │ │
│ HTTPS │ MCP/HTTP │ MCP/HTTP
│ │ │
┌────────▼──────────────────▼───────────────────▼─────────────────┐
│ Next.js Application │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Middleware Layer │ │
│ │ - OIDC Authentication (Human) │ │
│ │ - API Key Authentication (Agent) │ │
│ │ - Rate Limiting │ │
│ │ - Request Logging │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────┐ ┌─────────────────────────────────┐ │
│ │ Server Components │ │ API Routes │ │
│ │ + Server Actions │ │ (Agent-only) │ │
│ │ (Human frontend) │ │ │ │
│ │ │ │ /api/projects/* │ │
│ │ - Dashboard │ │ /api/ideas/* │ │
│ │ - Project Overview │ │ /api/documents/* │ │
│ │ - Ideas List │ │ /api/tasks/* │ │
│ │ - Documents List │ │ /api/proposals/* │ │
│ │ - Kanban Board │ │ /api/agents/* │ │
│ │ - Proposal Review │ │ /api/auth/* │ │
│ │ - Activity Feed │ │ /api/mcp <- MCP HTTP endpoint│ │
│ └─────────────────────┘ └─────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Service Layer │ │
│ │ - ProjectService - IdeaService │ │
│ │ - DocumentService - TaskService │ │
│ │ - ProposalService - CommentService │ │
│ │ - AgentService - ActivityService │ │
│ │ - AssignmentService │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Data Access Layer │ │
│ │ (Prisma Client) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────┬───────────────────────────────────┘
│
┌─────────▼─────────┐
│ PostgreSQL │
│ Database │
└───────────────────┘
3.2 Controller-Service-DAO Architecture
Chorus adopts the classic three-layer architecture pattern with clear separation of concerns:
┌─────────────────────────────────────────────────────────────────┐
│ Controller Layer │
│ (Next.js API Routes) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Responsibilities: │ │
│ │ - Request/response handling │ │
│ │ - Auth/authorization checks │ │
│ │ - Parameter validation │ │
│ │ - Calling the Service layer │ │
│ │ - Response formatting │ │
│ └──────────────────────────────────────────────────────────┘ │
│ Code location: src/app/api/**/*.ts │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Service Layer │
│ (Business Logic) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Responsibilities: │ │
│ │ - Business logic implementation │ │
│ │ - Data querying and transformation │ │
│ │ - Transaction management │ │
│ │ - Cross-entity operation coordination │ │
│ │ - State machine validation │ │
│ └──────────────────────────────────────────────────────────┘ │
│ Code location: src/services/*.service.ts │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ DAO Layer │
│ (Prisma Client) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Responsibilities: │ │
│ │ - Database operation encapsulation │ │
│ │ - ORM mapping │ │
│ │ - Connection pool management │ │
│ └──────────────────────────────────────────────────────────┘ │
│ Code location: src/lib/prisma.ts (singleton) │
│ src/generated/prisma/ (generated client) │
└─────────────────────────────────────────────────────────────────┘
Service Layer Modules
| Service | File | Responsibility |
|---|---|---|
| ProjectService | project.service.ts | Project CRUD |
| IdeaService | idea.service.ts | Idea CRUD + status transitions + assignment |
| TaskService | task.service.ts | Task CRUD + status transitions + assignment |
| DocumentService | document.service.ts | Document CRUD |
| ProposalService | proposal.service.ts | Proposal CRUD + approval workflow |
| AgentService | agent.service.ts | Agent + API Key management |
| CommentService | comment.service.ts | Polymorphic comments |
| ActivityService | activity.service.ts | Activity logging (including assignment/release records) |
| AssignmentService | assignment.service.ts | Agent self-service queries (my tasks, available, unblocked) |
| NotificationService | notification.service.ts | Notification CRUD, preferences, SSE event emission |
| NotificationListener | notification-listener.ts | Activity → Notification mapping, recipient resolution |
| SessionService | session.service.ts | Agent Session CRUD + Task Checkin/Checkout + heartbeat |
Code Examples
Controller (route.ts):
// src/app/api/projects/route.ts
import { withErrorHandler, parsePagination } from "@/lib/api-handler";
import { success, paginated, errors } from "@/lib/api-response";
import { getAuthContext, isUser } from "@/lib/auth";
import * as projectService from "@/services/project.service";
export const GET = withErrorHandler(async (request) => {
const auth = await getAuthContext(request);
if (!auth) return errors.unauthorized();
const { page, pageSize, skip, take } = parsePagination(request);
const { projects, total } = await projectService.listProjects({
companyUuid: auth.companyUuid, // UUID-based
skip,
take,
});
return paginated(projects, page, pageSize, total);
});
Service (*.service.ts):
// src/services/project.service.ts
import { prisma } from "@/lib/prisma";
export async function listProjects({ companyUuid, skip, take }) {
const [projects, total] = await Promise.all([
prisma.project.findMany({
where: { companyUuid }, // UUID-based query
skip,
take,
orderBy: { updatedAt: "desc" },
}),
prisma.project.count({ where: { companyUuid } }),
]);
return { projects, total };
}
3.3 Frontend Architecture: Server Components + Server Actions
Chorus adopts the Next.js 15 React Server Components (RSC) and Server Actions architecture to maximize server-side rendering and reduce client-side JavaScript.
┌─────────────────────────────────────────────────────────────────┐
│ Server Components (Page layer) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Responsibilities: │ │
│ │ - Server-side data fetching (directly calling Service) │ │
│ │ - Server-side auth checks (getServerAuthContext) │ │
│ │ - Server-side HTML rendering │ │
│ │ - Passing data to Client Components │ │
│ └──────────────────────────────────────────────────────────┘ │
│ Code location: src/app/(dashboard)/**/page.tsx │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────────┐
│ Client Components│ │ Server Actions │ │ Service Layer │
│ (Interactive) │ │ (Data mutation) │ │ (Direct calls) │
│ │ │ │ │ │
│ *-actions.tsx │ │ actions.ts │ │ *.service.ts │
│ *-form.tsx │ │ │ │ │
│ Uses useTransition│ │ "use server" │ │ │
└────────┬─────────┘ └────────┬────────┘ └─────────────────────────┘
│ │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Prisma Client │
└─────────────────────┘
Data Flow Patterns
Reading data (Server Components):
URL request -> Server Component -> Service Layer -> Prisma -> Render HTML
Writing data (Server Actions):
User action -> Client Component -> Server Action -> Service Layer -> Prisma -> revalidatePath
File Organization Pattern
Each feature page follows this file structure:
projects/[uuid]/tasks/[taskUuid]/
├── page.tsx # Server Component (data fetching + rendering)
├── actions.ts # Server Actions (data mutation)
├── task-actions.tsx # Client Component (interactive buttons)
└── task-form.tsx # Client Component (form)
Code Examples
Server Component (page.tsx):
// Server component: directly calls Service to fetch data
import { getServerAuthContext } from "@/lib/auth-server";
import { getTask } from "@/services/task.service";
import { TaskActions } from "./task-actions";
export default async function TaskPage({ params }: PageProps) {
const auth = await getServerAuthContext();
if (!auth) redirect("/login");
const { taskUuid } = await params;
const task = await getTask(auth.companyUuid, taskUuid);
return (
<div>
<h1>{task.title}</h1>
<TaskActions taskUuid={taskUuid} status={task.status} />
</div>
);
}
Server Action (actions.ts):
"use server";
import { revalidatePath } from "next/cache";
import { getServerAuthContext } from "@/lib/auth-server";
import { claimTask } from "@/services/task.service";
export async function claimTaskAction(taskUuid: string) {
const auth = await getServerAuthContext();
if (!auth) redirect("/login");
await claimTask({
taskUuid,
companyUuid: auth.companyUuid,
assigneeType: auth.type,
assigneeUuid: auth.actorUuid,
});
revalidatePath(`/projects`);
return { success: true };
}
Client Component (task-actions.tsx):
"use client";
import { useTransition } from "react";
import { claimTaskAction } from "./actions";
export function TaskActions({ taskUuid, status }: Props) {
const [isPending, startTransition] = useTransition();
const handleClaim = () => {
startTransition(async () => {
await claimTaskAction(taskUuid);
});
};
return (
<Button onClick={handleClaim} disabled={isPending}>
{isPending ? "Processing..." : "Claim Task"}
</Button>
);
}
Architecture Benefits
| Benefit | Description |
|---|---|
| Security | Auth and database operations are server-side, not exposed to the client |
| Performance | Reduced client-side JavaScript, faster initial page render |
| Simplified Code | No API route middle layer needed, Server Actions directly call Service |
| Type Safety | End-to-end TypeScript, compile-time parameter checking |
| Cache Control | revalidatePath for precise cache invalidation |
Scenarios Retaining Client-Side Auth
The following scenarios still use authFetch (client-side auth):
| File | Purpose |
|---|---|
layout.tsx | Dashboard layout session check |
auth-context.tsx | Global auth state Provider |
auth-client.ts | authFetch utility library |
These are auth infrastructure that need to maintain session state on the client side.
3.4 Directory Structure
chorus/
├── docker-compose.yml # Local development environment
├── Dockerfile # Production image
├── package.json
├── pnpm-lock.yaml
├── tsconfig.json
├── next.config.js
├── tailwind.config.js
├── .env.example
│
├── prisma/
│ ├── schema.prisma # Data model definitions
│ └── migrations/ # Database migrations
│
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── layout.tsx # Root layout
│ │ ├── page.tsx # Home/Dashboard
│ │ ├── globals.css
│ │ │
│ │ ├── (auth)/ # Auth-related pages
│ │ │ ├── login/page.tsx # Email input -> route dispatch
│ │ │ ├── login/password/page.tsx # Super admin password login
│ │ │ └── callback/page.tsx # OIDC callback
│ │ │
│ │ ├── admin/ # Super admin panel
│ │ │ ├── page.tsx # Super admin Dashboard
│ │ │ └── companies/
│ │ │ ├── page.tsx # Company list
│ │ │ └── [id]/page.tsx # Company details/OIDC config
│ │ │
│ │ ├── projects/
│ │ │ ├── page.tsx # Project list (Server Component)
│ │ │ ├── new/
│ │ │ │ ├── page.tsx # Create project form (Client Component)
│ │ │ │ └── actions.ts # Create project Server Actions
│ │ │ └── [uuid]/
│ │ │ ├── page.tsx # Project Overview (Server Component)
│ │ │ ├── ideas/
│ │ │ │ ├── page.tsx # Ideas list (Server Component)
│ │ │ │ └── [ideaUuid]/
│ │ │ │ ├── page.tsx # Idea details (Server Component)
│ │ │ │ ├── actions.ts # Idea Server Actions
│ │ │ │ └── idea-actions.tsx # Interactive buttons (Client Component)
│ │ │ ├── documents/
│ │ │ │ ├── page.tsx # Documents list (Server Component)
│ │ │ │ └── [documentUuid]/
│ │ │ │ ├── page.tsx # Document details (Server Component)
│ │ │ │ ├── actions.ts # Document Server Actions
│ │ │ │ ├── document-actions.tsx
│ │ │ │ └── document-content.tsx
│ │ │ ├── tasks/
│ │ │ │ ├── page.tsx # Kanban board (Server Component)
│ │ │ │ └── [taskUuid]/
│ │ │ │ ├── page.tsx # Task details (Server Component)
│ │ │ │ ├── actions.ts # Task Server Actions
│ │ │ │ ├── task-actions.tsx
│ │ │ │ └── task-status-progress.tsx
│ │ │ ├── proposals/
│ │ │ │ ├── page.tsx # Proposal list (Server Component)
│ │ │ │ └── [proposalUuid]/
│ │ │ │ ├── page.tsx # Proposal details (Server Component)
│ │ │ │ ├── actions.ts # Proposal Server Actions
│ │ │ │ └── proposal-actions.tsx
│ │ │ ├── knowledge/page.tsx # Knowledge base query
│ │ │ └── activity/page.tsx # Activity stream (Server Component)
│ │ │
│ │ ├── settings/
│ │ │ ├── page.tsx # Settings page (Client Component + Server Actions)
│ │ │ └── actions.ts # API Key management Server Actions
│ │ │
│ │ └── api/ # API Routes (for Agent access)
│ │ ├── auth/
│ │ │ ├── login/route.ts # Email-based login entry
│ │ │ ├── callback/route.ts # OIDC callback
│ │ │ └── [...nextauth]/route.ts
│ │ ├── admin/
│ │ │ ├── login/route.ts # Super admin password login
│ │ │ └── companies/
│ │ │ ├── route.ts # GET/POST Company
│ │ │ └── [id]/route.ts # GET/PATCH/DELETE Company
│ │ ├── projects/
│ │ │ ├── route.ts # GET (list), POST (create)
│ │ │ └── [id]/
│ │ │ ├── route.ts
│ │ │ ├── ideas/route.ts
│ │ │ ├── documents/route.ts
│ │ │ ├── tasks/route.ts
│ │ │ ├── proposals/route.ts
│ │ │ ├── knowledge/route.ts
│ │ │ └── activities/route.ts
│ │ ├── ideas/
│ │ │ └── [id]/route.ts
│ │ ├── documents/
│ │ │ └── [id]/route.ts
│ │ ├── tasks/
│ │ │ └── [id]/
│ │ │ ├── route.ts
│ │ │ └── comments/route.ts
│ │ ├── proposals/
│ │ │ └── [id]/
│ │ │ ├── route.ts
│ │ │ ├── approve/route.ts
│ │ │ └── reject/route.ts
│ │ ├── agents/
│ │ │ ├── route.ts
│ │ │ └── [id]/
│ │ │ ├── route.ts
│ │ │ └── keys/route.ts
│ │ ├── activities/
│ │ │ └── route.ts
│ │ └── mcp/
│ │ └── route.ts # MCP HTTP endpoint
│ │
│ ├── components/ # React components
│ │ ├── ui/ # shadcn/ui components
│ │ ├── layout/
│ │ │ ├── header.tsx
│ │ │ ├── sidebar.tsx
│ │ │ └── nav.tsx
│ │ ├── idea/
│ │ │ ├── idea-card.tsx
│ │ │ ├── idea-form.tsx
│ │ │ └── idea-list.tsx
│ │ ├── document/
│ │ │ ├── document-card.tsx
│ │ │ ├── document-viewer.tsx
│ │ │ └── document-list.tsx
│ │ ├── kanban/
│ │ │ ├── board.tsx
│ │ │ ├── column.tsx
│ │ │ └── card.tsx
│ │ ├── task/
│ │ │ ├── task-card.tsx
│ │ │ ├── task-detail.tsx
│ │ │ └── task-form.tsx
│ │ ├── proposal/
│ │ │ ├── proposal-card.tsx
│ │ │ ├── proposal-review.tsx
│ │ │ ├── proposal-timeline.tsx
│ │ │ └── approval-buttons.tsx
│ │ ├── knowledge/
│ │ │ ├── knowledge-search.tsx
│ │ │ └── knowledge-results.tsx
│ │ └── activity/
│ │ ├── activity-feed.tsx
│ │ └── activity-item.tsx
│ │
│ ├── lib/ # Core libraries
│ │ ├── prisma.ts # Prisma Client singleton
│ │ ├── auth.ts # NextAuth configuration
│ │ ├── api-key.ts # API Key validation
│ │ └── utils.ts # Utility functions
│ │
│ ├── services/ # Business logic layer
│ │ ├── project.service.ts
│ │ ├── idea.service.ts
│ │ ├── document.service.ts
│ │ ├── task.service.ts
│ │ ├── proposal.service.ts
│ │ ├── knowledge.service.ts
│ │ ├── agent.service.ts
│ │ ├── activity.service.ts
│ │ └── mcp.service.ts
│ │
│ ├── mcp/ # MCP Server
│ │ ├── server.ts # Per-auth MCP server factory
│ │ └── tools/
│ │ ├── public.ts # Public tools (all agents)
│ │ ├── session.ts # Session tools (all agents)
│ │ ├── developer.ts # Developer agent tools
│ │ ├── pm.ts # PM agent tools
│ │ └── admin.ts # Admin agent tools
│ │
│ └── types/ # TypeScript type definitions
│ ├── api.ts
│ ├── mcp.ts
│ └── index.ts
│
├── public/
│ ├── skill/ # Standalone Skill docs (served at /skill/)
│ │ ├── SKILL.md
│ │ ├── package.json
│ │ └── references/ # Role-specific workflow docs (7 files)
│ └── chorus-plugin/ # Chorus Plugin for Claude Code
│ ├── hooks/ # Claude Code hooks configuration
│ ├── bin/ # Hook scripts (on-subagent-start/stop/idle)
│ ├── skills/chorus/ # Plugin-embedded Skill (with session automation)
│ └── .mcp.json # MCP server config template
│
└── tests/
├── unit/
└── e2e/
4. Data Model
4.0 Database Design Principles: UUID-Based Architecture + No Foreign Key Constraints
Design Decisions:
- UUID-Based Foreign Key References: All inter-entity associations use UUIDs instead of numeric IDs
- Prisma Relation Mode: Uses
relationMode = "prisma", no database-level foreign key constraints
Configuration:
// prisma/schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
relationMode = "prisma" // Relations managed by Prisma, no database FK
}
UUID-Based Architecture Design Principles:
| Principle | Description |
|---|---|
| Foreign keys use UUID | All *Id fields renamed to *Uuid (e.g., companyUuid, projectUuid) |
| Relations reference UUID | Prisma relation definitions use references: [uuid] instead of references: [id] |
| No ID queries | All queries/operations are UUID-based, numeric id is not used |
| API consistency | UUIDs used uniformly internally and externally, no ID-UUID conversion needed |
Why UUID-Based Design:
| Benefit | Description |
|---|---|
| Security | Prevents numeric ID enumeration attacks |
| Simplified code | No ID-UUID conversion logic needed |
| API consistency | UUIDs used uniformly internally and externally |
| Distributed-friendly | UUIDs can be generated client-side without database sequences |
Relation Definition Example:
model Project {
id Int @id @default(autoincrement())
uuid String @unique @default(uuid())
companyUuid String
company Company @relation(fields: [companyUuid], references: [uuid])
tasks Task[]
@@index([companyUuid])
}
model Task {
id Int @id @default(autoincrement())
uuid String @unique @default(uuid())
companyUuid String
projectUuid String
project Project @relation(fields: [projectUuid], references: [uuid])
@@index([companyUuid])
@@index([projectUuid])
}
Notes:
- Numeric ID retained:
idstill serves as primary key for internal indexing, but is not used in business logic - UUID indexes: All UUID foreign key fields have indexes for query performance optimization
- Referential integrity: Managed by Prisma Client at the application layer
- Cascade operations:
onDelete: Cascadeis simulated by Prisma
4.1 ER Diagram
ID Design Principles: UUID-Based Architecture
id: Auto-incrementing numeric primary key (internal indexing only, not used in business logic)uuid: UUID string (used for all foreign key references and API exposure)- All association fields use
*Uuidnaming (e.g.,companyUuid,projectUuid)
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Company │───┬───│ User │ │ Agent │
│ │ │ │ │───────│ │
│ id (Int) │ │ │ id (Int) │ │ id (Int) │
│ uuid │ │ │ uuid │ │ uuid │
│ name │ │ │ companyUuid│ │ companyUuid│
│ emailDomains │ │ oidcSub │ │ name │
│ oidcIssuer │ │ │ email │ │ roles[] │
│ oidcClientId │ │ name │ │ ownerUuid │
│ oidcEnabled│ │ └─────────────┘ │ persona │
│ createdAt │ │ │ systemPrompt│
└─────────────┘ │ └─────────────┘
│ │ │
│ │ ┌─────────────┐ │
│ └───│ ApiKey │──────────────┘
│ │ │
│ │ id (Int) │
│ │ uuid │
│ │ companyUuid│
│ │ agentUuid │
│ │ keyHash │
│ │ lastUsed │
│ │ expiresAt │
│ │ revokedAt │
│ └─────────────┘
│
├──────────────────────────────────────────────────────┐
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Project │ │ Proposal │
│ │ │ │
│ id (Int) │ │ id (Int) │
│ uuid │ ┌─────────────┐ │ uuid │
│ companyUuid│───────│ Idea │ │ companyUuid│
│ name │ │ │ │ projectUuid│
│ description│ │ id (Int) │ │ title │
│ createdAt │ │ uuid │ │ inputType │
└─────────────┘ │ companyUuid│──── N:1 ─────────│ inputUuids │
│ │ projectUuid│ │ outputType │
│ │ content │ │ outputData │
│ │ attachments│ │ status │
│ │ assigneeType │ createdByUuid│
│ │ assigneeUuid │ reviewedByUuid│
│ │ createdByUuid └─────────────┘
│ └─────────────┘ │
│ ┌─────────────┐ │
│ │ Document │<────────────────────────┘
│ │ │ (outputType=document)
│ │ id (Int) │
│ │ uuid │
│ │ companyUuid│
│ │ projectUuid│
│ │ type │ (prd | tech_design | adr)
│ │ title │
│ │ content │
│ │ version │
│ │ proposalUuid│
│ │ createdByUuid│
│ └─────────────┘
│
│ ┌─────────────┐
├──────────────│ Task │<────────────────────────┐
│ │ │ (outputType=task) │
│ │ id (Int) │ │
│ │ uuid │ │
│ │ companyUuid│ │
│ │ projectUuid│ │
│ │ title │ │
│ │ description│ │
│ │ status │ │
│ │ assigneeType │
│ │ assigneeUuid │
│ │ proposalUuid│────────────────────────┘
│ │ createdByUuid│
│ │ storyPoints│
│ └─────────────┘
│ │
│ ┌──────▼──────┐
├──────────────│ Activity │
│ │ │
│ │ id (Int) │
│ │ uuid │
│ │ companyUuid│
│ │ projectUuid│
│ │ targetType │ (idea|task|proposal|document)
│ │ targetUuid │
│ │ actorType │ (user|agent)
│ │ actorUuid │
│ │ action │ (created|assigned|released|...)
│ │ value │ (JSON: operation result value)
│ └─────────────┘
│
│ ┌────────────────┐
├──────────────│ TaskDependency │
│ │ │
│ │ id (Int) │
│ │ taskUuid │
│ │ dependsOnUuid │
│ │ companyUuid │
│ └────────────────┘
│
│ ┌────────────────┐
├──────────────│ AgentSession │
│ │ │
│ │ id (Int) │
│ │ uuid │
│ │ agentUuid │
│ │ companyUuid │
│ │ name │
│ │ status │ (active|inactive|closed)
│ │ lastActiveAt │
│ └────────────────┘
│ │
│ ┌──────▼─────────────┐
└──────────────│ SessionTaskCheckin │
│ │
│ id (Int) │
│ sessionUuid │
│ taskUuid │
│ checkinAt │
│ checkoutAt │
└────────────────────┘
4.2 Core Entity Descriptions
Common Fields:
id: Auto-incrementing numeric primary key (Int @id @default(autoincrement())) - internal indexing onlyuuid: UUID string (String @unique @default(uuid())) - business identifier and foreign key reference- All foreign keys use UUID (e.g.,
companyUuid,projectUuid)
Company (Tenant)
- Root entity for multi-tenant isolation
- All data is associated via
companyUuid emailDomains: Email domain list, used to identify Company during loginoidcIssuer: OIDC Provider URLoidcClientId: OIDC Client ID (PKCE only, no Client Secret required)oidcEnabled: Whether OIDC login is enabled
User
- Human user, authenticated via OIDC
companyUuid: Parent company UUIDoidcSub: OIDC Provider subject
Agent
- AI Agent entity (Claude Code, etc.)
companyUuid: Parent company UUIDroles: Preset selector array — one or more ofdeveloper_agent/pm_agent/admin_agent(legacypm/developeraliases still resolve as the corresponding presets). Roles only select the preset; actual authorization is driven bypermissions.permissions: Custom permission bits layered on top of the preset(s). Effective set = union of expanded presets + custom. See §6.3.ownerUuid: Creator User UUIDpersona: Custom personality descriptionsystemPrompt: Full system prompt- One Agent can have multiple API Keys (all inherit the Agent's effective permissions)
ApiKey
- Independently managed, supports rotation and revocation
companyUuid: Parent company UUIDagentUuid: Associated Agent UUIDkeyHash: API key hash storageexpiresAt: Optional expiration timerevokedAt: Revocation time
Project
- Project container, parent of all business data
companyUuid: Parent company UUID- Contains Ideas, Documents, Tasks, Proposals, Activities
Idea
- Raw human input, can be assigned to a PM Agent for processing
companyUuid: Parent company UUIDprojectUuid: Parent project UUIDtitle: Titlecontent: Text contentattachments: Attachment list (images, files, etc.)status:open|assigned|in_progress|pending_review|completed|closedassigneeType:user|agent(polymorphic association)assigneeUuid: Assignee UUIDassignedAt: Assignment timeassignedByUuid: Assigner User UUID (recorded when assigned by human)createdByUuid: Creator User UUID- Serves as input source for Proposals (one Idea can only belong to one Proposal, multiple Ideas can be combined into the same Proposal, N:1 relationship via JSON array)
Assignment Methods:
- Assign to user: Visible and operable by all PM Agents under that user
- Assign to specific PM Agent: Visible and operable only by that Agent
- Humans can reassign at any time (regardless of current status)
Document
- Product of a Proposal (PRD, technical design, etc.)
companyUuid: Parent company UUIDprojectUuid: Parent project UUIDtype:prd|tech_design|adr| ...content: Markdown format contentversion: Version numberproposalUuid: Source Proposal UUID (traceable)createdByUuid: Creator UUID
Task
- Product of a Proposal or manually created, can be assigned to Agent/human for execution
companyUuid: Parent company UUIDprojectUuid: Parent project UUIDstatus:open|assigned|in_progress|to_verify|done|closedpriority:low|medium|highstoryPoints: Effort estimation (unit: Agent hours)assigneeType:user|agent(polymorphic association)assigneeUuid: Assignee UUIDassignedAt: Assignment timeassignedByUuid: Assigner User UUID (recorded when assigned by human)proposalUuid: Source Proposal UUID (traceable, optional)createdByUuid: Creator UUID
Assignment Methods:
- Assign to user: Visible and operable by all Developer Agents under that user
- Assign to specific Agent: Visible and operable only by that Agent
- Humans can reassign at any time (regardless of current status)
Proposal
- Created by PM Agent, approved by humans, connects inputs and outputs
companyUuid: Parent company UUIDprojectUuid: Parent project UUID- Input:
inputType:idea|documentinputUuids: Associated input UUID list (JSON array, supports multiple Ideas combined)
- Output:
outputType:document|taskoutputData: Proposed content (Document draft or Task list)
status:pending|approved|rejected|revisedcreatedByUuid: Creator Agent UUIDreviewedByUuid: Reviewer User UUID- Upon approval, automatically creates Documents or Tasks based on outputType
Activity
- Project-level activity log, generic design supporting all entity types
companyUuid: Parent company UUIDprojectUuid: Parent project UUIDtargetType: Target entity type (idea|task|proposal|document)targetUuid: Target entity UUIDactorType: Actor type (user|agent)actorUuid: Actor UUIDaction: Action type (see table below)value: Action result value (e.g., new status, assignment target, etc., JSON format)
Activity Field Design Principles:
targetType+targetUuid: Identifies the target entity of the operation (generic design)action: Describes what operation occurredvalue: Records the operation result/post-change value (concise, records result only)
Activity Action Types:
| Action | Description | Value Example |
|---|---|---|
created | Entity was created | - |
assigned | Entity was assigned | { type: "user", uuid: "...", name: "..." } |
released | Entity was released | - |
status_changed | Status changed | "in_progress" (new status) |
submitted | Submitted for approval/verification | - |
approved | Approved | - |
rejected | Rejected | "reason text" (optional) |
comment_added | Comment added | - |
Activity Maintenance Principles:
- Created in Service layer: All Activities are automatically created by the Service layer during business operations
- value records result only: Status changes only record the post-change value, not the pre-change value
- Assignment operations must be recorded: Any assignment/release operation must create an Activity
Comment
- Polymorphic comments, can comment on Idea, Proposal, Task, Document
companyUuid: Parent company UUIDtargetType:idea|proposal|task|documenttargetUuid: Target entity UUIDauthorType:user|agentauthorUuid: Author UUIDcontent: Comment content
TaskDependency
- DAG dependency relationship between tasks
taskUuid: Current task UUIDdependsOnUuid: Predecessor task UUID- Cycle detection implemented in the Service layer
AgentSession
- Session tracking Agent work status
agentUuid: Parent Agent UUIDname: Session name (e.g., "frontend-worker")status:active|inactive|closedlastActiveAt: Last heartbeat time- Automatically marked
inactiveafter 1 hour of inactivity
SessionTaskCheckin
- Records which task a Session is currently working on
sessionUuid: Session UUIDtaskUuid: Task UUIDcheckinAt/checkoutAt: Checkin/checkout time- UI uses this to display Kanban Worker badges and Task Detail active Workers
5. API Design
5.1 REST API
Authentication
- Human: OIDC + Session Cookie
- Agent:
Authorization: Bearer {api_key}
Endpoint Overview
UUID-Based API: All URL parameters and request/response data uniformly use UUIDs, consistent internally and externally.
| Method | Path | Description | Permissions |
|---|---|---|---|
| Projects | |||
| GET | /api/projects | Project list | User, Agent |
| POST | /api/projects | Create project | User |
| GET | /api/projects/:uuid | Project details | User, Agent |
| PATCH | /api/projects/:uuid | Update project | User |
| DELETE | /api/projects/:uuid | Delete project | User |
| Ideas | |||
| GET | /api/projects/:uuid/ideas | Project Ideas list | User, PM Agent |
| POST | /api/projects/:uuid/ideas | Create Idea | User |
| GET | /api/ideas/:uuid | Idea details | User, PM Agent |
| PATCH | /api/ideas/:uuid | Update Idea (including status) | User, PM Agent |
| POST | /api/ideas/:uuid/claim | Claim Idea | PM Agent |
| POST | /api/ideas/:uuid/release | Release claimed Idea | PM Agent |
| DELETE | /api/ideas/:uuid | Delete Idea | User |
| Documents | |||
| GET | /api/projects/:uuid/documents | Project Documents list | User, Agent |
| GET | /api/documents/:uuid | Document details | User, Agent |
| PATCH | /api/documents/:uuid | Update Document | User |
| Tasks | |||
| GET | /api/projects/:uuid/tasks | Project task list | User, Agent |
| POST | /api/projects/:uuid/tasks | Create task (manual) | User |
| GET | /api/tasks/:uuid | Task details | User, Agent |
| PATCH | /api/tasks/:uuid | Update task (including status) | User, Agent (assignee) |
| POST | /api/tasks/:uuid/claim | Claim Task | Developer Agent |
| POST | /api/tasks/:uuid/release | Release claimed Task | Developer Agent |
| POST | /api/tasks/:uuid/comments | Add comment | User, Agent |
| Proposals | |||
| GET | /api/projects/:uuid/proposals | Project proposal list | User, PM Agent |
| POST | /api/projects/:uuid/proposals | Create proposal | PM Agent |
| GET | /api/proposals/:uuid | Proposal details | User, PM Agent |
| POST | /api/proposals/:uuid/approve | Approve proposal | User |
| POST | /api/proposals/:uuid/reject | Reject proposal | User |
| Knowledge | |||
| GET | /api/projects/:uuid/knowledge | Unified knowledge base query | User, Agent |
| Agents | |||
| GET | /api/agents | Agent list | User |
| POST | /api/agents | Create Agent | User |
| GET | /api/agents/:uuid | Agent details | User |
| POST | /api/agents/:uuid/keys | Create API Key | User |
| DELETE | /api/agents/:uuid/keys/:keyUuid | Revoke API Key | User |
| Activities | |||
| GET | /api/projects/:uuid/activities | Project activity list | User, Agent |
| Agent Self-Service | |||
| GET | /api/me/assignments | Get my claimed Ideas + Tasks | Agent |
| GET | /api/projects/:uuid/available | Get claimable Ideas + Tasks | Agent |
| Super Admin (Super Admin Only) | |||
| POST | /api/auth/login | Email-based login entry | Public |
| POST | /api/admin/login | Super admin password login | Public |
| GET | /api/admin/companies | Company list | Super Admin |
| POST | /api/admin/companies | Create Company | Super Admin |
| GET | /api/admin/companies/:uuid | Company details | Super Admin |
| PATCH | /api/admin/companies/:uuid | Update Company (including OIDC config) | Super Admin |
| DELETE | /api/admin/companies/:uuid | Delete Company | Super Admin |
5.2 MCP API
Endpoint
POST /api/mcp
Transport
Streamable HTTP Transport (supports SSE)
Authentication
Header: Authorization: Bearer {api_key}
Based on the Agent's effective permission set (associated with the API Key), different tool sets are returned. Each gated MCP tool declares a single required permission (e.g. task:write, proposal:admin); only tools whose required permission is present in the caller's permission set are exposed. Public tools (discover, comment, session) carry no gate. See MCP_TOOLS.md for the complete tool → permission mapping.
Project Filtering (Optional)
Agents can filter results by project(s) using optional HTTP headers:
| Header | Format | Description |
|---|---|---|
X-Chorus-Project | Single UUID or comma-separated UUIDs | Filter by specific project(s) |
X-Chorus-Project-Group | Group UUID | Filter by project group |
Behavior:
- No header: Returns all projects (default, backward compatible)
X-Chorus-Project: Returns only specified project(s)X-Chorus-Project-Group: Returns all projects in the group- Priority:
X-Chorus-Project-Group>X-Chorus-Project
Affected tools: chorus_checkin, chorus_get_my_assignments
Example:
// .mcp.json
{
"mcpServers": {
"chorus": {
"type": "http",
"url": "http://localhost:8637/api/mcp",
"headers": {
"Authorization": "Bearer cho_xxx",
"X-Chorus-Project": "project-uuid-1,project-uuid-2"
}
}
}
}
Transport (Stateless MCP)
POST /api/mcp is stateless since 0.6.2. Each request authenticates via the Authorization: Bearer cho_… header, the server builds a fresh per-request McpServer instance gated by the agent's effective permission set, and the instance is torn down once the response is flushed.
Implications:
- No
initialize→ keep-alive → expire flow; noMcp-Session-Idexchange; no inactivity timeout. - Permission changes in the UI take effect on the next MCP call, with no reconnect needed.
- Any container can serve any request — the endpoint scales horizontally without sticky sessions. Cross-instance event propagation for SSE goes through Redis when
REDIS_URLis set (otherwise the in-memory EventBus is used in single-instance mode). - The
chorus_create_session/chorus_session_*tools operate on the AgentSession model (swarm-mode observability) — that is a database-backed Agent-level concept and is unrelated to MCP transport state.
Public Tools (All Agents)
| Tool | Description |
|---|---|
chorus_checkin | Check in: returns persona, resource-aggregated effective permissions, project-grouped ideaTracker, and an unread-notification summary |
chorus_get_project | Get project details |
chorus_get_ideas / chorus_get_idea | List/get Ideas |
chorus_get_documents / chorus_get_document | List/get Documents |
chorus_get_proposals / chorus_get_proposal | List/get Proposals (including drafts) |
chorus_list_tasks / chorus_get_task | List/get Tasks |
chorus_get_activity | Project activity stream |
chorus_get_my_assignments | Idea/task tracker grouped by project (same shape as checkin.ideaTracker) |
chorus_get_available_ideas | Claimable Ideas |
chorus_get_available_tasks | Claimable Tasks |
chorus_get_unblocked_tasks | Tasks with all dependencies completed (for scheduling) |
chorus_add_comment / chorus_get_comments | Comment CRUD |
Session Tools (All Agents)
| Tool | Description |
|---|---|
chorus_create_session | Create a named Session |
chorus_list_sessions | List Sessions |
chorus_close_session / chorus_reopen_session | Close/reopen Session |
chorus_session_checkin_task / chorus_session_checkout_task | Task Checkin/Checkout |
chorus_session_heartbeat | Session heartbeat |
Gated Tools by Required Permission
Gated tools are grouped below by the permission they require. An agent sees a tool if and only if that permission is in its effective set (preset-expanded + custom). The full tool → permission matrix is maintained in MCP_TOOLS.md and in source at src/mcp/tools/permission-map.ts.
| Required Permission | Representative Tools |
|---|---|
idea:write | chorus_claim_idea, chorus_release_idea, chorus_move_idea, chorus_pm_create_idea, chorus_pm_start_elaboration, chorus_pm_skip_elaboration |
proposal:write | chorus_pm_create_proposal, chorus_pm_submit_proposal, chorus_pm_validate_proposal, chorus_pm_{add,update,remove}_document_draft, chorus_pm_{add,update,remove}_task_draft, chorus_pm_assign_task, chorus_pm_{reject,revoke}_proposal |
document:write | chorus_pm_create_document, chorus_pm_update_document |
task:write | chorus_claim_task, chorus_release_task, chorus_submit_for_verify, chorus_report_work, chorus_report_criteria_self_check |
project:write | chorus_admin_create_project, chorus_admin_{create,update,delete}_project_group, chorus_admin_move_project_to_group |
proposal:admin | chorus_admin_approve_proposal, chorus_admin_close_proposal |
task:admin | chorus_admin_verify_task, chorus_admin_reopen_task, chorus_admin_close_task, chorus_mark_acceptance_criteria, chorus_admin_delete_task |
idea:admin | chorus_pm_validate_elaboration, chorus_admin_delete_idea |
document:admin | chorus_admin_delete_document |
The admin_agent preset grants all *:admin bits and so exposes every tool in the table above. Any custom combination (e.g. Developer preset + task:admin to self-verify) produces the corresponding tool set automatically.
Warning: *:admin permissions are human-level — they cover Proposal approval, Task verification, Idea/Document deletion. Grant them only to Agents that are intentionally automating human approval workflows.
Proposal Input/Output Description
| Scenario | inputType | inputUuids | outputType | outputData |
|---|---|---|---|---|
| Ideas -> PRD | idea | Idea UUIDs (supports multiple) | document | PRD draft |
| PRD -> Tasks | document | Document UUID | task | Task list |
| PRD -> Tech Design | document | Document UUID | document | Tech design draft |
6. Authentication & Authorization
6.0 Super Admin Authentication
Configuration (environment variables):
SUPER_ADMIN_EMAIL=admin@example.com
SUPER_ADMIN_PASSWORD_HASH=\$2b\$10$... # bcrypt hash
Login Flow:
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Browser │ │ Chorus │ │ Database │
│ │ │ Server │ │ │
└────┬─────┘ └────┬─────┘ └──────┬───────┘
│ │ │
│ 1. Enter email│ │
│ ──────────────>│ │
│ │ │
│ │ 2. Check if super admin
│ │ (compare with env var)
│ │ │
│ 3a. Is super │ │
│ admin, return │ │
│ password page │ │
│ <──────────────│ │
│ │ │
│ 4a. Enter │ │
│ password │ │
│ ──────────────>│ │
│ │ │
│ │ 5a. Verify password hash
│ │ │
│ 6a. Super │ │
│ admin panel │ │
│ <──────────────│ │
│ │ │
│ 3b. Not super │ │
│ admin │ Query email domain│
│ │ ─────────────────>│
│ │ │
│ │ Return Company │
│ │ OIDC config │
│ │ <─────────────────│
│ │ │
│ 4b. Redirect │ │
│ to Company │ │
│ OIDC │ │
│ <──────────────│ │
Super Admin Panel Routes:
/admin- Super admin panel entry/admin/companies- Company management/admin/companies/[id]- Company details/OIDC configuration
6.1 Human Authentication (OIDC + PKCE)
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Browser │ │ Chorus │ │ OIDC │
│ │ │ Server │ │ Provider │
└────┬─────┘ └────┬─────┘ └──────┬───────┘
│ │ │
│ 1. Login │ │
│ ──────────────>│ │
│ │ │
│ 2. Redirect │ │
│ <──────────────│ │
│ │ │
│ 3. Auth Request (PKCE) │
│ ──────────────────────────────────>│
│ │ │
│ 4. User Login │ │
│ <──────────────────────────────────│
│ │ │
│ 5. Callback with code │
│ ──────────────>│ │
│ │ │
│ │ 6. Exchange code │
│ │ ─────────────────>│
│ │ │
│ │ 7. Tokens │
│ │ <─────────────────│
│ │ │
│ 8. Set Session Cookie │
│ <──────────────│ │
6.2 Agent Authentication (API Key)
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Claude │ │ Chorus │ │ Database │
│ Code │ │ Server │ │ │
└────┬─────┘ └────┬─────┘ └──────┬───────┘
│ │ │
│ MCP Request │ │
│ + API Key │ │
│ ──────────────>│ │
│ │ │
│ │ Validate Key │
│ │ ─────────────────>│
│ │ │
│ │ Agent+Perms │
│ │ <─────────────────│
│ │ │
│ │ Filter tools by │
│ │ permission set │
│ │ │
│ MCP Response │ │
│ <──────────────│ │
6.3 Permission Model
Authorization for Agents is a 15-bit permission matrix — 5 resources × 3 actions. The UI exposes three presets plus a Custom option.
Resources × Actions:
| Resource | read | write | admin |
|---|---|---|---|
idea | view ideas | create/claim/release/update ideas, run elaboration | close/delete ideas |
proposal | view proposals and drafts | create/submit/reject/revoke proposals, manage drafts, batch-create tasks, manage task DAG, assign tasks | approve / close proposals |
document | view documents | create/update documents | delete documents |
project | view projects and groups | create/update/delete projects and project groups, move projects between groups | granted by admin_agent preset, but no tool or route currently checks this bit |
task | view tasks | claim/release/submit/report tasks, self-check acceptance criteria | verify/reopen/close/delete tasks, mark acceptance criteria |
Role Presets → Permission Set:
| Preset | Expanded permissions | Total |
|---|---|---|
developer_agent | *:read + task:write | 6 |
pm_agent | *:read + idea:write, proposal:write, document:write, task:write, project:write | 10 |
admin_agent | all 15 bits (*:read + *:write + *:admin) | 15 |
Effective permission computation: computeEffectivePermissions(roles, customPermissions) returns the union of every preset's expansion and any custom bits attached to the Agent (see src/lib/authz/permissions.ts). Both REST gating (requireAgentPermission) and MCP tool visibility (permission-map.ts + registerPermissionedTool) consult this effective set.
Human users do not go through the Agent permission matrix — REST routes gate them via the standard UserAuthContext path. SuperAdmin bypasses all checks.
Checkin output: chorus_checkin (and the /api/agents/me endpoint) surfaces permissions as a resource-aggregated object for token efficiency:
{
"permissions": {
"idea": ["read", "write"],
"proposal": ["read", "write"],
"document": ["read", "write"],
"project": ["read"],
"task": ["read", "write"]
}
}
Skills and plugin hooks read this aggregated shape directly instead of scanning a flat list.
7. Core Workflows
7.1 Reversed Conversation Workflow (Idea -> Proposal -> Document/Task)
┌─────────────────────────────────────────────────────────────────┐
│ 1. Human creates Ideas │
│ - Text: "I want to implement user auth with OAuth and │
│ email/password login" │
│ - Attachments: competitor screenshots, design sketches, etc.│
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. PM Agent creates PRD Proposal │
│ - Get Ideas (chorus_pm_get_ideas) │
│ - Read project knowledge base (chorus_query_knowledge) │
│ - Create proposal (chorus_pm_create_proposal) │
│ inputType: idea, inputIds: [idea1, idea2, ...] │
│ Supports selecting multiple Ideas combined as input │
│ outputType: document, outputData: { PRD draft } │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. Human reviews PRD Proposal (Web UI) │
│ - View PRD draft │
│ - Approve -> Create Document(PRD) │
│ - Request changes -> Return for revision │
│ - Reject -> Mark as rejected │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. PM Agent creates Task Breakdown Proposal │
│ - Read Document(PRD) │
│ - Create proposal (chorus_pm_create_proposal) │
│ inputType: document, inputIds: [prd_id] │
│ outputType: task, outputData: { Task list } │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. Human reviews Task Breakdown Proposal (Web UI) │
│ - View task list │
│ - Approve -> Create Tasks (status: todo) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 6. Personal Agent executes tasks │
│ - Get task (chorus_get_task) │
│ - Get related documents (chorus_get_document) │
│ - Execute development work │
│ - Report completion (chorus_report_work) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 7. PM Agent continuous tracking │
│ - Analyze progress (chorus_pm_analyze_progress) │
│ - Identify risks (chorus_pm_identify_risks) │
│ - Create new Proposals to adjust plans when needed │
└─────────────────────────────────────────────────────────────────┘
Complete Traceability Chain:
Ideas -> Proposal -> Document(PRD) -> Proposal -> Tasks
|
Proposal -> Document(Tech Design)
Every Task/Document can be traced back to its source Proposal and Ideas.
7.2 Task Status Transitions
┌──────────────┐
│ created │
│ (from UI │
│ or proposal)│
└──────┬───────┘
│
▼
┌──────────────┐
┌─────────│ open │<─────────────────┐
│ │ (unassigned) │ │
│ └──────┬───────┘ │
│ │ │
│ │ Assign to User/Agent │ Release
│ ▼ │
│ ┌──────────────┐ │
│ │ assigned │──────────────────┘
│ │ (assigned) │
│ └──────┬───────┘
│ │
│ │ Start work
│ ▼
│ ┌──────────────┐
│ │ in_progress │
│ │ (executing) │
│ └──────┬───────┘
│ │
│ │ Finish execution
│ ▼
│ ┌──────────────┐
│ │ to_verify │
│ │(awaiting │
│ │ human verify)│
│ └──────┬───────┘
│ │
│ │ Human verification passed
│ ▼
│ ┌──────────────┐
│ │ done │
│ │ (completed) │
│ └──────────────┘
│
│ ┌──────────────┐
└────────>│ closed │ (can be closed at any stage)
│ (closed) │
└──────────────┘
Assignment Rules:
- Only the current assignee can update the status
- Humans can reassign tasks at any status at any time
- Everyone can comment on tasks at any status
- Release operation clears the assignee, status returns to open
Assignment Flow (UI):
Click Assign button -> Open Assign modal
├── Assign to myself -> Visible to all my Developer Agents
├── Assign to specific Agent -> Visible only to that Agent
├── Assign to another user -> Visible to that user and their Agents
└── Release -> Clear assignee, status -> open
Activity Recording: Each assignment/release operation automatically creates an Activity:
task_assigned: Task was assigned, payload includes target infotask_released: Task was released
7.3 Idea Status Transitions
┌──────────────┐
│ created │
│(human created)│
└──────┬───────┘
│
▼
┌──────────────┐
┌─────────│ open │<─────────────────┐
│ │(awaiting claim)│ │
│ └──────┬───────┘ │
│ │ │
│ │ PM Agent claims │ Release claim
│ ▼ │
│ ┌──────────────┐ │
│ │ assigned │──────────────────┘
│ │ (claimed) │
│ └──────┬───────┘
│ │
│ │ Start processing
│ ▼
│ ┌──────────────┐
│ │ in_progress │
│ │(producing │
│ │ Proposal) │
│ └──────┬───────┘
│ │
│ │ Submit Proposal
│ ▼
│ ┌──────────────┐
│ │pending_review│
│ │(awaiting │
│ │human approval)│
│ └──────┬───────┘
│ │
│ │ Proposal approved
│ ▼
│ ┌──────────────┐
│ │ completed │
│ │ (completed) │
│ └──────────────┘
│
│ ┌──────────────┐
└────────>│ closed │ (can be closed at any stage)
│ (closed) │
└──────────────┘
Claim Rules:
- Only Ideas in
openstatus can be claimed - Only the claimant (assignee) can update the status
- Humans can force reassign Ideas at any status
- Everyone can comment on Ideas at any status
7.4 Proposal Approval Workflow
┌──────────────────────────────────────┐
│ Determined by outputType │
│ │
┌──────────────┐ ┌─────▼──────┐ ┌──────────────────────┐ │
│ pending │────>│ approved │────>│ outputType=document │──┼──> Create Document
└──────────────┘ └────────────┘ └──────────────────────┘ │
│ ┌──────────────────────┐ │
│ │ outputType=task │──┼──> Create Tasks
│ └──────────────────────┘ │
│ │
▼ │
┌──────────────┐ │
│ rejected │ │
└──────────────┘ │
│ │
▼ │
┌──────────────┐ │
│ revised │─────────────────────────────────────────────────>┘
└──────────────┘ (resubmit after revision)
Approval Results:
approved+outputType=document-> Create Document, record proposalIdapproved+outputType=task-> Batch create Tasks, record proposalIdrejected-> End, can re-proposerevised-> Re-approve after revision
8. Deployment Architecture
8.1 Local Development
# docker-compose.yml
version: '3.8'
services:
chorus:
build:
context: .
dockerfile: Dockerfile
ports:
- "8637:8637"
environment:
- DATABASE_URL=postgres://chorus:chorus@db:5432/chorus
- NEXTAUTH_URL=http://localhost:8637
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- OIDC_ISSUER=${OIDC_ISSUER}
- OIDC_CLIENT_ID=${OIDC_CLIENT_ID}
- OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
depends_on:
db:
condition: service_healthy
volumes:
- ./src:/app/src
- ./prisma:/app/prisma
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=chorus
- POSTGRES_PASSWORD=chorus
- POSTGRES_DB=chorus
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U chorus"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
8.2 Production Deployment (AWS CDK)
┌─────────────────────────────────────────────────────────────────┐
│ ALB (Application Load Balancer) │
│ HTTPS + ACM Certificate │
└─────────────────────────────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ ECS │ │ ECS │ │ ECS │
│ Fargate │ │ Fargate │ │ Fargate │
│ Task │ │ Task │ │ Task │
└──────────┘ └──────────┘ └──────────┘
│ │ │
└───────┬───────┴───────┬───────┘
│ │
┌─────────▼─────┐ ┌──────▼──────────────┐
│ Aurora │ │ ElastiCache │
│ Serverless v2 │ │ Serverless Redis 7 │
│ (PostgreSQL) │ │ (Pub/Sub + RBAC) │
└───────────────┘ └──────────────────────┘
CDK Infrastructure (packages/chorus-cdk/):
| Construct | File | Resources |
|---|---|---|
| Network | network.ts | VPC (2 AZs), public/private subnets, NAT Gateway, Security Groups |
| Database | database.ts | Aurora Serverless v2, Secrets Manager (DB creds + app config) |
| Cache | cache.ts | ElastiCache Serverless Redis 7, RBAC user + password in Secrets Manager |
| Service | service.ts | ECS Fargate cluster, ALB, Task Definition, ECR image build |
Redis Authentication: RBAC with password user (chorus), default user disabled. Password auto-generated and stored in Secrets Manager, injected into ECS container as REDIS_PASSWORD secret.
9. Security Considerations
9.1 API Key Security
- API Keys are stored as SHA-256 hashes
- Plaintext is only returned at creation time and cannot be recovered afterward
- Supports expiration time and manual revocation
- Records last usage time
9.2 Data Isolation
- All queries include
companyUuidfiltering (UUID-based multi-tenant isolation) - Service layer enforces tenant ownership checks
9.3 Input Validation
- Uses Zod for request body validation
- Prevents SQL injection (Prisma parameterized queries)
- Prevents XSS (React automatic escaping)
9.4 Rate Limiting
- API request throttling
- Prevents brute-force API Key attacks
10. Extensibility Considerations
10.1 Future Features
| Feature | Description | Status |
|---|---|---|
| Task DAG | Dependency modeling + cycle detection + visualization | Implemented |
| Session Observability | Agent Session + Checkin + Kanban integration | Implemented |
| Chorus Plugin | Claude Code plugin, automating Session lifecycle | Implemented |
| Task Auto-Scheduling Query | chorus_get_unblocked_tasks MCP tool | Implemented |
| Notification System | In-app notifications + SSE push + Redis Pub/Sub | Implemented |
| Global Search | Unified search across 6 entity types, Cmd+K UI, MCP tool | Implemented |
| Execution Metrics | Agent Hours, velocity statistics | To be developed (P1) |
| Git Integration | Associate commits and PRs | To be developed |
| Semantic Search | pgvector knowledge base search | To be developed |
10.2 Technical Reserves
- pgvector: PostgreSQL natively supports it, can be added seamlessly later
- Redis: ElastiCache Serverless for Pub/Sub event delivery; can be extended for caching or queues later
Appendix
A. Environment Variables
# Database
DATABASE_URL=postgres://chorus:chorus@localhost:5432/chorus
# NextAuth
NEXTAUTH_URL=http://localhost:8637
NEXTAUTH_SECRET=your-secret-key
# Super Admin (system startup config, manages Companies and global settings)
SUPER_ADMIN_EMAIL=admin@example.com
SUPER_ADMIN_PASSWORD_HASH=\$2b\$10$... # bcrypt hash
# Redis (optional — falls back to in-memory EventBus when unset)
# Local dev: redis://default:chorus-redis@localhost:6379
# CDK: assembled from REDIS_HOST + REDIS_PORT + REDIS_USERNAME + REDIS_PASSWORD
REDIS_URL=redis://default:chorus-redis@localhost:6379
# Note: OIDC configuration has been moved to the database (Company table),
# each Company is independently configured
# PKCE only, no Client Secret required