Skip to content

Architecture

Overview

coqu is a TypeScript monorepo (yarn workspaces) consisting of three packages:

packages/
├── shared/   — shared types and utilities
├── api/      — REST API (Express + Prisma)
└── web/      — web interface (React + Vite)

Packages

@coqu/shared

Shared TypeScript types used by both API and Web. No runtime dependencies.

Exports: - ApiResponse<T> — wrapper for all API responses - User — user entity interface (safe — no passwordHash) - UserRole — user role type ("admin" | "user") - HealthStatus — health check response interface - OAuthAccount — OAuth account entity (provider, providerAccountId, userId) - AppSettings — application settings (Google OAuth config) - AppSettingsUpdate — partial settings update payload - AuthStatus — whether initial setup is needed and whether Google OAuth is enabled - LoginRequest — login payload - SetupRequest — initial admin setup payload - AuthResponse — token + user returned on login/setup - ApiToken — API token entity (safe — no hash, no userId) - CreateTokenRequest — create-token payload ({ name }) - CreateTokenResponse — raw token + token metadata (token shown once) - PingResponse — ping endpoint response - QueryRequest — query submission payload ({ query, projectId, agentId, modeId }) - QueryResponse — query result ({ query, result, timestamp, provider?, durationMs?, costUsd? }) - ProjectStatus — project lifecycle status ("pending" | "cloning" | "ready" | "error") - Project — project entity interface (includes hasGitToken boolean instead of the raw token) - CreateProjectRequest — project creation payload (name, optional gitUrl/branch/gitToken) - BranchListResponse — current branch + list of all branches - CommitInfoResponse — latest commit hash + message - AgentType — agent type ("claude-code") - AgentStatus — agent lifecycle status ("pending" | "installing" | "installed" | "error") - Agent — agent entity interface - CreateAgentRequest — agent creation payload ({ name, type }) - UpdateAgentRequest — agent update payload ({ name?, envVars? }) - MODE_ORDER — ordered list of mode names (["tiny", "compact", "default", "extended", "deep"]) - ModeName — union type of mode names (derived from MODE_ORDER) - Mode — global mode entity (id, name, createdAt, updatedAt) - AgentModeConfig — per-agent mode configuration (id, agentId, modeId, modeName, systemPrompt, maxTokens, createdAt, updatedAt) - UpdateAgentModeConfigRequest — mode config update payload (optional systemPrompt, maxTokens) - AgentEnv — environment file content ({ content }) - AgentAuthStatus — agent OAuth/API key authentication status (authenticated, account, authMethod) - AgentAuthLoginResponse — OAuth login URL response ({ url, needsCode? }) - LogLevel — log severity level ("info" | "warn" | "error") - LogCategory — log source category ("auth" | "projects" | "agents" | "git" | "system") - LogEntry — single log entry (level, time, msg, category, optional userEmail/projectId/agentId) - LogsResponse — paginated log entries ({ entries, total, limit, offset }) - LogDatesResponse — available log file dates ({ dates })

@coqu/api

REST API built with Express.js. Default port: 4000.

Stack: - Express — HTTP server - Prisma — PostgreSQL ORM - Pino — structured JSON logging (with pino-roll for daily file rotation) - Helmet — security headers - CORS — cross-origin request handling - jsonwebtoken — JWT-based authentication - bcryptjs — password hashing - google-auth-library — Google OAuth 2.0 authentication - @anthropic-ai/claude-agent-sdk — Claude Code agent integration for query execution - @modelcontextprotocol/sdk — MCP server for remote tool access via Streamable HTTP - zod — schema validation (used for MCP tool parameter schemas)

Authentication supports three schemes:

  1. JWT — issued on login/setup/OAuth, expires after 7 days. Used by the web SPA. Sent via Authorization: Bearer <token>.
  2. API tokens — personal tokens for programmatic access. Format: coqu_ prefix + 64 hex chars. Stored as SHA-256 hashes in the database. lastUsedAt is updated on each use. Sent via Authorization: Bearer <token>.
  3. Google OAuth — users can sign in via Google when enabled in admin settings. On first OAuth login, if a user with the same email exists, the Google account is linked to the existing user (preserving their role). Otherwise, a new user is created with the user role.

Both JWT_SECRET and GIT_TOKEN_SECRET environment variables are required — the server will refuse to start without them.

The requireAuth middleware tries JWT verification first (no DB hit); on failure it hashes the bearer value and looks up an ApiToken record.

User roles:

  • admin — full access to all endpoints and UI features. The initial account created via setup is always admin.
  • user — read-only access. Can view projects and agents, and submit queries. Cannot create/modify/delete projects, agents, tokens, environment, or settings. New users created via Google OAuth default to the user role.

The requireRole(...roles) middleware enforces role-based access on protected endpoints.

On first launch (no users in the database), the app enters a setup flow where the initial admin account is created via POST /api/auth/setup.

Routes (role annotations: all = any authenticated user, admin = admin only): - GET /health — service health check (public) - GET /api/auth/status — returns { needsSetup, googleOAuthEnabled } (public) - POST /api/auth/setup — create initial admin account (only works when no users exist) - POST /api/auth/login — authenticate with email + password, returns JWT - GET /api/auth/me — get current user (all) - GET /api/auth/google — redirect to Google OAuth consent screen (public, requires OAuth enabled) - GET /api/auth/google/callback — Google OAuth callback, exchanges code for token, creates/links user, redirects to /?token=<jwt> - GET /api/settings — read app settings with masked secrets (admin) - PUT /api/settings — update app settings (admin) - GET /api/users — list users (admin) - POST /api/users — create user (admin) - GET /api/tokens — list current user's API tokens (admin) - POST /api/tokens — create a new API token, returns raw value once (admin) - DELETE /api/tokens/:id — delete an API token with ownership check (admin) - GET /api/ping — returns { message: "pong", timestamp, userId } (all) - POST /api/query — submit a query to an agent for a project with a specific mode (all) - GET /api/projects — list all projects (all) - POST /api/projects — create a project (admin) - GET /api/projects/:id — get a single project (all) - PATCH /api/projects/:id — update project fields (admin) - DELETE /api/projects/:id — delete project and its workspace directory (admin) - POST /api/projects/:id/clone — start async git clone, returns 202 (admin) - GET /api/projects/:id/branches — list local + remote branches (all, project must be ready) - POST /api/projects/:id/checkout — switch branch via git checkout (admin) - GET /api/projects/:id/commit — get latest commit hash and message (all) - POST /api/projects/:id/pull — pull latest changes from origin (admin) - GET /api/agents — list all agents (all) - POST /api/agents — create agent, triggers async SDK installation (admin) - GET /api/agents/:id — get a single agent (all) - PATCH /api/agents/:id — update agent name (admin) - DELETE /api/agents/:id — delete agent, uninstall SDK globally (admin) - POST /api/agents/:id/install — reinstall agent SDK, returns 202 (admin) - GET /api/modes — list all global modes (all) - GET /api/agents/:id/modes — list mode configs for an agent (all) - PATCH /api/agents/:agentId/modes/:modeId — update an agent's mode config (admin) - GET /api/agents/:id/auth/status — check agent OAuth/API key auth status (all) - POST /api/agents/:id/auth/login — initiate OAuth login, returns URL and needsCode hint (admin) - POST /api/agents/:id/auth/code — submit authentication code for headless OAuth flow (admin) - POST /api/agents/:id/auth/logout — logout from OAuth session (admin) - GET /api/agents/:id/env — read agent-specific environment variables (admin) - PUT /api/agents/:id/env — update agent-specific environment variables (admin) - GET /api/env — read global environment file (admin) - PUT /api/env — write global environment file (admin) - GET /api/logs — list log entries with filtering by date/level/category and pagination (admin) - GET /api/logs/dates — list available log file dates, newest first (admin) - POST /mcp — MCP Streamable HTTP endpoint (all, see MCP section below)

Agent management: Agents represent AI agents (currently only Claude Code). When created, the API runs npm install -g @anthropic-ai/claude-code asynchronously via execFile and auto-creates 5 predefined modes (tiny, compact, default, extended, deep). Status transitions: pendinginstallinginstalled or error. A 5-minute timeout kills stalled installs. On API startup, a health check verifies installed agents still have their binary (which claude) and triggers reinstallation if missing.

Modes: Modes are a global resource (not per-agent). The 5 predefined modes are seeded on startup: tiny (1024 tokens), compact (2048), default (4096), extended (8192), deep (16384). Each agent has an AgentModeConfig for each mode, which defines the system prompt and max token limit for that agent-mode pair. Configs are auto-seeded when an agent is created or on first fetch. Users can edit per-agent mode settings (system prompt, max tokens) but cannot create or delete modes themselves.

Agent authentication: Agents can authenticate via OAuth (Anthropic account) or API key (set in the global .env file). The API provides endpoints to check auth status, initiate OAuth login, submit an authentication code (for headless servers), and logout. The GET /api/agents/:id/auth/status endpoint runs claude auth status and parses the output (supports both JSON and text formats). The POST /api/agents/:id/auth/login endpoint runs claude auth login and captures the OAuth URL from CLI output — the process stays alive in the background for up to 5 minutes while the user completes OAuth in their browser. On headless servers (e.g. Docker in production), the CLI cannot start a local callback server, so the OAuth flow falls back to showing an authentication code that the user must paste back. The login response includes needsCode: true when the redirect_uri points to platform.claude.com/oauth/code/callback. The POST /api/agents/:id/auth/code endpoint accepts the code and writes it to the active login process's stdin. Login processes are tracked per-agent and cleaned up automatically on timeout or when a new login is initiated. The authMethod field in AgentAuthStatus indicates how the agent is authenticated (oauth, api_key, third_party, etc.).

Agent query abstraction: Queries are dispatched through a provider abstraction layer (packages/api/src/agents/). The AgentProvider interface defines a query(prompt, options) method with options for env, cwd, systemPrompt, maxTokens, and abortSignal. ClaudeCodeProvider implements this using @anthropic-ai/claude-agent-sdk with permissionMode: "dontAsk" and a restricted tool set (Read, Glob, Grep, Task) — the agent can only read and search code, not modify it. Providers are registered in a global registry at startup. When POST /api/query is called, the API validates the project (must be ready with existing path), agent (must be installed), and mode, loads environment variables from ~/.coqu/.env, merges them with the process environment, then merges the agent's per-agent env vars on top (priority: process.env < global .env < agent envVars), resolves the agent's provider from the registry, and executes the query with the project path as cwd and mode settings as system prompt and token limit (5-minute timeout). If a branch is specified, git checkout is performed before querying; if pull is requested, git pull runs first. The response includes the result text, provider name, duration, and cost.

Global environment file: A single .env file at $HOME/.coqu/.env provides shared environment variables for all agents. The GET/PUT /api/env endpoints read and write this file. The directory is created automatically if it doesn't exist. The ANTHROPIC_API_KEY must be set here (or in an agent's per-agent env vars) for agent queries to work.

Per-agent environment variables: Each agent has an envVars field (stored in the database) that holds KEY=VALUE pairs in the same format as the global .env file. These are managed via the GET/PUT /api/agents/:id/env endpoints and edited in the "Environment Variables" section of the agent detail page. During query execution, per-agent env vars are merged on top of the global env, so they override global settings for that specific agent. This allows configuring different API keys, models, or other settings per agent.

Structured logging: All server events are logged via Pino (packages/api/src/logger.ts). Logs are written as NDJSON to daily-rotated files in LOG_DIR (default ./logs/), named app.{YYYY-MM-DD}.{N}.log by pino-roll. Each entry includes a numeric level (30=info, 40=warn, 50=error), a category (auth, projects, agents, git, system), and optional context fields (userEmail, projectId, agentId). Log files older than LOG_RETENTION_DAYS (default 30) are deleted on startup and every 24 hours.

MCP (Model Context Protocol) server: The API exposes a remote MCP server at POST /mcp using the Streamable HTTP transport (@modelcontextprotocol/sdk). Authentication reuses the existing requireAuth middleware — clients must send a Bearer token (JWT or API token) in the Authorization header. The server operates statelessly (no session persistence — each request creates a fresh server and transport instance). MCP tools are defined in packages/api/src/mcp/:

  • list_projects — returns all projects (id, name, status, description)
  • list_agents — returns all agents (id, name, type, status)
  • query — executes a code query. Parameters: query (string, required), project (name, required), agent (name, required), mode (name, optional — defaults to "default"), branch (string, optional — checks out branch before querying), pull (boolean, optional — runs git pull before querying). Names are resolved to database entities internally; ambiguous names return an error listing matches. Available project, agent, and mode names are embedded directly in tool descriptions so MCP clients can discover them without a separate listing tool.

The query tool and the POST /api/query REST endpoint share the same core execution logic (packages/api/src/query.ts), ensuring identical validation (project readiness, agent installation, mode resolution, environment loading, provider dispatch, 5-minute timeout).

Git tokens (PATs) are encrypted at rest using AES-256-GCM with a key derived from GIT_TOKEN_SECRET. The token is injected into the clone URL at clone time and scrubbed from any error messages. Cloned repositories are stored under WORKSPACE_PATH (default /workspace), each in a directory named by the project's UUID.

@coqu/web

React SPA built with Vite. In dev mode runs on port 3000 and proxies /api/* requests to the API server.

Uses react-router-dom for client-side routing with sixteen pages: - SetupPage — shown when no admin account exists yet - LoginPage — email/password sign-in, with optional "Sign in with Google" button (shown when Google OAuth is enabled), and links to About and Setup Guide pages - HomePage — chat interface with project/agent/mode dropdowns (persisted via localStorage), scrollable message history, bottom input bar (disabled until all selections made), and API health status footer - TokensPage — API token management (create, list, delete) - ProjectsPage — project list with status badges, branch, and path info - NewProjectPage — create a project (name, description, git URL, branch, git token) - ProjectDetailPage — project info, clone/pull/delete actions, branch switcher, commit info - AgentsPage — agent list with status badges (pending/installing/installed/error) - NewAgentPage — create agent form (name + type dropdown) - AgentDetailPage — agent info, status, version, per-agent environment variables editor (textarea + save, overrides global env), modes management (edit system prompt and max tokens), OAuth authentication status with login/logout buttons (includes code input for headless OAuth flow on production servers), reinstall and delete actions, installation polling - EnvPage — global environment variable editor (textarea + save) - SettingsPage — admin-only settings page for configuring Google OAuth (enable/disable toggle, Client ID, Client Secret, display of Authorized JavaScript origins and Authorized redirect URIs) - LogsPage — server log viewer with date/level/category filters and "Load more" pagination - AboutPage — public page describing what coqu is, its purpose, how it works, the tech stack, and a link to the GitHub repository. Accessible without authentication. - McpInfoPage — authenticated page with MCP integration guide: endpoint URL (dynamically derived from window.location.origin), available tools, authentication (with link to Menu → API Tokens), and client setup instructions for Claude Desktop, Cursor, and Claude Code. - SetupGuidePage — public page covering local development with Docker, production deployment via Docker Compose with Cloudflare Tunnel, environment configuration, and Google OAuth setup.

Pages restricted to admin role: SettingsPage, EnvPage, LogsPage, TokensPage, NewProjectPage, NewAgentPage. Non-admin users are redirected to the home page. On ProjectDetailPage and AgentDetailPage, non-admin users see only the Info section (no Actions, Branch, Git Token, Mode Configs, or Authentication sections).

A shared Header component (Header.tsx) renders the navigation bar on all authenticated pages, including the logo/favicon with a "Code Query — ask your codebase" slogan, and a dropdown menu containing the user's name, navigation links (Projects, Agents — visible to all; Environment, Logs, API Tokens, Settings — visible to admin only; About, MCP, Setup Guide — visible to all; with active-state highlighting for the current page), and a logout button. A MinimalHeader component (MinimalHeader.tsx) renders a lightweight header for public pages (About, Setup Guide) when the user is not authenticated, showing only the logo and a Login link. When authenticated, these pages show the full Header instead.

Auth state is managed via AuthContext (React context). JWT tokens are stored in localStorage. On mount, the context checks for a token query parameter in the URL (set by OAuth callback redirect) and stores it before cleaning the URL. An apiFetch helper in api.ts automatically attaches the Bearer token to requests.

In production it is built into static files and served by nginx (stable-alpine-slim), which also handles API proxying.

Package dependencies

web ──→ shared
api ──→ shared

TypeScript project references (composite: true in shared) ensure correct cross-package type checking.

Database

PostgreSQL 16. ORM — Prisma.

Schema is defined in packages/api/prisma/schema.prisma. Migrations are managed via Prisma Migrate and committed to the repository (packages/api/prisma/migrations/). In production, prisma migrate deploy runs automatically on container start.

Models: - User — user accounts (email, name, passwordHash (nullable for OAuth-only users), role: admin or user) - OAuthAccount — linked OAuth accounts (provider, providerAccountId, FK to User with cascade delete, unique constraint on provider+providerAccountId) - AppSetting — key-value application settings (e.g. Google OAuth configuration). Sensitive values are encrypted at rest. - ApiToken — personal API tokens (hashed, linked to User) - Project — git-backed projects (name, gitUrl, branch, status, encrypted gitToken, workspace path) - Agent — AI agents (name, type, status, statusMessage, version, envVars) - Mode — global modes (name, unique). Seeded on startup with 5 predefined modes. - AgentModeConfig — per-agent mode settings (systemPrompt, maxTokens, FK to Agent and Mode with cascade delete, unique constraint on agentId+modeId)

Network architecture (Docker)

Internet
cloudflared (Cloudflare Tunnel)
web (nginx :80)
  ├── /           → React static files
  ├── /api/*      → proxy → api:4000
  ├── /mcp        → proxy → api:4000
  └── /health     → proxy → api:4000
                          postgres:5432

Single domain, single entry point. Nginx routes traffic between static files and API based on URL path. The API proxy has extended timeouts (proxy_read_timeout 360s) to support long-running agent queries. Nginx forwards X-Forwarded-Proto from the upstream proxy (Cloudflare) or falls back to $scheme, and the API uses trust proxy to read these headers for correct protocol detection.

Environment variables

Defined in .env (template — .env.example):

  • POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB — PostgreSQL credentials
  • POSTGRES_PORT — PostgreSQL port (default: 5432)
  • DATABASE_URL — Prisma connection string
  • API_PORT — API server port
  • JWT_SECRET — secret key for signing JWT auth tokens (required, no default)
  • GIT_TOKEN_SECRET — secret key for encrypting git PATs at rest (required, no default)
  • WORKSPACE_PATH — root directory for cloned project repos (default: /workspace)
  • APP_URL — public URL of the app (e.g. https://coqu.aimost.pl). Required in production for correct OAuth redirect URIs. In dev, the origin is derived from the request automatically.
  • VITE_API_URL — API URL for Vite dev proxy
  • LOG_DIR — directory for log files (default: ./logs)
  • LOG_RETENTION_DAYS — days to keep log files before deletion (default: 30)
  • CLOUDFLARE_TUNNEL_TOKEN — Cloudflare Tunnel token