---
title: "Architecture"
description: "How the Chickpea source tree is laid out, how one codebase builds for Cloudflare and Node, and which module owns what."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.chickpea.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture

**Chickpea's source tree** is one TypeScript project that builds for two targets, Cloudflare Workers and Node, out of the same modules. Where Flue, the open agent framework from the Astro team, now part of Cloudflare, owns the agent loop, durable sessions, and provider plumbing, Chickpea owns Slack, the authority model, Admin, and the state stores under them. This page maps `src/`, explains how one tree serves two targets, names the three Flue agents and the work ledger, and says where tests live.

## The source map

`CONTRIBUTING.md` has the short version under "Where things live". Directory by directory:

| Directory | What it owns |
|---|---|
| `src/slack/` | Slack transport, event admission, routing, delivery, attachments, presentation, streaming, handles. The largest directory, holding `transport/`, `gateway/`, and `agent-presence/`. |
| `src/agents/` | The three Flue agent modules and `runtime-plan.ts`, which turns one Agent's resolved configuration into the plan a turn executes. |
| `src/config/` | Configuration and settings stores, connector presets, model policy, seeding, and per-target store selection (`state-backend.ts`). |
| `src/state/` | The `StateDb` mini interface, its Node backend, the async facade, and cross-domain link DDL. |
| `src/work/` | The work ledger: admission, run lifecycle, leases, executor, retention, migrations. |
| `src/management/` | The workspace management service, its schemas and policy, the management MCP server, the Slack tools. |
| `src/admin/` | Admin pages and their HTTP routes. |
| `src/auth/`, `src/identity/` | Slack sign-in, roles, setup capability links, invitations, workspace identity. |
| `src/connections/` | Connections, the managed Composio lane, and credential resolution at egress. |
| `src/routines/` | Schedules: parsing, admission, scheduler, execution, delivery. `routine` is the code word for a schedule. |
| `src/memory/`, `src/sandbox/` | One memory per Agent with its validation and tool policy; the coding sandbox lifecycle, egress handler, and session caps. |
| `src/model-catalog/`, `src/model-compat/` | The pinned model catalog and the compatibility layer over Pi. |
| `src/usage/`, `src/telemetry/`, `src/audit/`, `src/activity/` | Usage and pricing, the content-free telemetry contract, audit events, live Slack activity status. |
| `src/security/`, `src/http/` | Shared primitives: constant-time compare, digests, content validation, bounded response bodies, request origin. |

Four files sit at the root of `src/`. `src/app.ts` builds the Hono application, registers the runtime interceptors, and default-exports it; both targets serve it. `src/cloudflare.ts` is Worker only: the `TagStateStore` and `SlackGatewaySession` Durable Objects, the `Sandbox` class, and the scheduled handler. `src/db.node.ts` selects file-backed Flue persistence for the Node build. `src/runtime-bootstrap.ts` installs the app-owned Pi providers once per module graph.

## Two targets, one tree

There are two Vite configs. `vite.config.ts` runs the Flue plugin first, then `@cloudflare/vite-plugin`, emits `assets/` as Static Assets, and builds into `dist-cf`. `vite.node.config.ts` runs the Flue plugin alone and passes `db: 'src/db.node.ts'`, which is how Node gets file-backed persistence the Cloudflare target rejects. `npm run build` builds the Cloudflare profile then validates the artifact with `scripts/flue-build-cf.mjs`; `npm run dev` uses the Node config.

`flue.config.ts` keeps the shared provider graph to `anthropic`, `openai`, and `openrouter`, and sets `tracing: false`. The Cloudflare config adds the `cloudflare` provider, since only that target has a Workers AI binding.

The target is a runtime check, not a build flag. `isCloudflareTarget()` in `src/config/runtime-target.ts` compares `globalThis.navigator?.userAgent` to `Cloudflare-Workers`. The same modules are bundled for both targets and pick their backend on first use, so a new store, provider, or capability needs a decision on both sides of that branch.

## On Cloudflare

`wrangler.jsonc` declares the whole surface: a `chickpea` Worker with Static Assets (`ASSETS`), the Workers AI binding (`AI`), `CF_VERSION_METADATA`, two app-owned Durable Objects (`TAG_STATE` and `SLACK_GATEWAY_SESSION`), and one D1 database, `AUTH_DB`, with migrations in `migrations/better-auth`. Flue's own agent Durable Objects are added by the build. There is no KV and no R2.

State runs inside `TagStateStore`. `DoSqlStateDb` in `src/cloudflare.ts` wraps `ctx.storage.sql`, the store logic classes run in the Durable Object, and the Worker reaches them through the `Cf*Store` proxies in `src/config/cf-state-proxies.ts`. A cron trigger fires every minute (`* * * * *`), driving the scheduler and the turn relay. The Durable Object migration list is history and is append only: `v6` is the Flue 2 reset that created the three v2 agent classes.

## On Node

`src/db.node.ts` points Flue at `./tmp/flue.db`, overridable with `TAG_DB_PATH`. The application stores use a sibling SQLite file, resolved by `resolveStateDbPath()` as `<TAG_DB_PATH>.state` unless `SLACK_STATE_DB_PATH` overrides it; a `:memory:` transcript database gets a `:memory:` state store, so test runs stay ephemeral. The `Sqlite*Store` classes run in process against those files rather than proxying to a Durable Object.

Node runs no scheduler and no coding sandbox. `RoutineCapability` in `src/routines/scheduler-adapter.ts` reports the reason `unsupported_target` there.

## The state layer

`StateDb` in `src/state/state-db.ts` is a five-method SQL interface: `run`, `get`, `all`, `exec`, and a synchronous `transaction`. It stays small because everything in it must be implementable over Durable Object SQLite, which has no prepared statements, rejects multi-statement strings, and cannot span an `await` inside a transaction. Write DDL one statement per `exec` call, or the code passes on Node and fails on Cloudflare.

Each store is written twice. A synchronous `*Logic` class owns the SQL and runs unchanged in both backends. A `Sqlite*Store` class satisfies the async store interface, and `promisify()` in `src/state/async-facade.ts` generates that forwarding with a Proxy, keeping `close()` synchronous. Columns that point across domains, such as `routines.work_id` and `usage_operations.run_id`, are installed by the idempotent `installLedgerLinks()` in `src/state/schema-links.ts`, which every owner calls.

## The three Flue agents

Chickpea registers exactly three Flue agents, named in `src/agents/names.ts`:

| Constant | Identifier | Module |
|---|---|---|
| `CHICKPEA_SLACK_AGENT_NAME` | `chickpea-slack-v2` | `src/agents/slack-thread.ts` |
| `CHICKPEA_ROUTINE_INTENT_AGENT_NAME` | `chickpea-routine-intent-v2` | `src/agents/routine-intent.ts` |
| `CHICKPEA_ROUTINE_EXECUTION_AGENT_NAME` | `chickpea-routine-execution-v2` | `src/agents/routine-execution.ts` |

Each module starts with `'use agent'` and assigns its name as a string literal, because the Flue build reads `<Agent>.agentName = '<literal>'` statically to derive Durable Object class and binding names. The constants exist separately because runtime policy keys off the same strings, and `tests/agent-names.test.ts` asserts the two agree. A Chickpea Agent is configuration these agents load, not a class of its own: `runtime-plan.ts` compiles one Agent's instructions, skills, connections, and repositories into the plan a turn runs. `UNATTENDED_AGENT_NAMES` covers the two routine agents, which have no human in the loop and so need authority written into the saved task text.

## The work ledger

`src/work/` is the canonical record of what ran. Four nouns carry it, defined in `src/work/types.ts` and created in `src/work/migrations.ts`:

- **Work** is a unit of ongoing activity, of kind `conversation`, `routine`, or `web_admin`, and lifecycle `open`, `closed`, or `expired`.
- **A binding** attaches one work item to one external conversation through an adapter (`slack`, `routine`, `web_admin`, `conformance`) and says whether configuration freezes when the binding opens or resolves on each run.
- **A run** is one admitted attempt, carrying its dedupe key, actor trust tier, configuration revision, capability digest, lease and fencing token, status, terminal disposition, and delivery status.
- **A run execution** is one model-invoking attempt inside a run.

Two supporting tables round it out. `effective_config_revisions` stores canonical configuration by digest, so a run records which authority it ran under. `ledger_content` holds request and response bodies under a 262,144 byte ceiling, with an expiry; a purged row keeps its metadata and drops its body.

## Management and its adapters

`WorkspaceManagementService` in `src/management/service.ts` holds every workspace mutation. `src/management/tool-adapter.ts` is the transport-neutral seam above it and lists the tool names in `WORKSPACE_MANAGEMENT_TOOL_NAMES`. Three adapters sit on that seam: the management MCP server (`src/management/mcp.ts`, Zod schemas), the Flue tools the Slack Agent calls (`src/management/slack-tools.ts`, Valibot schemas), and Admin's HTTP routes (`src/admin/routes.ts`). Adding a capability to one door and not the other two is how this seam drifts.

## Where tests live

Tests are Node's test runner over TypeScript: `npm test` typechecks, then runs `tests/*.test.ts` and `tests/usage/*.test.ts` with `tsx`. There are 271 top-level test files, plus `tests/helpers/` for fake services, `tests/fixtures/` for recorded payloads, and `tests/parity/fake-slack.ts`. Anything needing a build, a workerd run, or a fake provider is a script under `scripts/`, invoked through a `verify:*` npm script. Verification is local by design; the repository runs no GitHub Actions workflows.

## Why it works this way

Two targets share one tree so behaviour cannot fork. The alternative, a Worker build and a server build with their own stores, doubles every authority check and hides the divergence until a live workspace finds it. The cost is the narrow `StateDb` waist and the doubled store classes, which is why that constraint sits in the interface's own comments rather than with reviewers.

The work ledger exists for the same reason. A turn crosses Slack, a durable Flue session, a model provider, and a delivery attempt, and each can retry on its own. Recording admission, authority, and delivery as rows makes a duplicate reply a constraint violation, not a judgement call.

## What is not covered

- **Flue internals.** Durable sessions, subagents, tool wiring, and provider streaming belong to `@flue/runtime` and Pi, not this repository.
- **Generated files.** The model catalog, avatar pool, and brand assets have builder scripts; never edit their output by hand.

## Next steps

- [Development](/contribute/development): the checkout, the local gate, and the pull request rules.
- [How Chickpea works](/start/how-it-works): the same system described for admins, from mention to reply.
- [Authority and confirmation](/security/authority-and-confirmation): the authority model these modules enforce.
- [Reference](/reference): environment variables, the connector catalog, and the management MCP tools.

Source: https://docs.chickpea.co/contribute/architecture/index.mdx
