---
title: "Data and storage"
description: "Where a Chickpea deployment keeps its state on each target, how credentials are protected, how long each kind of record lives, and what backups can and cannot recover."
---

> 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.

# Data and storage

**Deployment state** is everything Chickpea keeps between turns: Agents and their channel grants, members, sessions, connections, memory, schedules, the work ledger, usage, and audit events. Where the security model says what may be read and written, this page says where those bytes land and how long they stay. It covers the stores on each target, the credential keyring, retention, what is deliberately content-free, and what a backup recovers.

## What is stored

| Data | On Cloudflare | On Node |
|---|---|---|
| Conversation transcripts | One Flue agent Durable Object per conversation | The transcript SQLite file |
| Agents, channel grants, thread routes, connection accounts | `TAG_STATE` Durable Object | The state SQLite file |
| Members, roles, encrypted Slack credential revisions | `TAG_STATE` | The state SQLite file |
| Memory, schedules, the work ledger, usage, management proposals, audit events | `TAG_STATE` | The state SQLite file |
| Browser sessions and the Slack provider account | D1 `AUTH_DB` | A separate auth SQLite file |
| Slack credential encryption keys | Worker secrets | A keyring JSON file |
| Shared gateway delivery inbox | `TAG_STATE` | Not available |
| Shared gateway connection session | `SLACK_GATEWAY_SESSION` Durable Object | Not available |

There is no KV binding and no R2 bucket. The only other Cloudflare binding that holds bytes is `ASSETS`, which serves the public product images.

## On Cloudflare

`TAG_STATE` is one Durable Object class, `TagStateStore`, addressed as a single instance named `singleton`. Inside it, one SQLite database carries the identity, configuration, snapshot, Slack, settings, turn job, gateway inbox, memory, schedule, usage, work, and management tables. Every store on this target is a proxy that calls that object.

Transcripts live apart from it. Flue, the open agent framework from the Astro team, now part of Cloudflare, registers three agent classes, `chickpea-slack-v2` for Slack turns plus `chickpea-routine-intent-v2` and `chickpea-routine-execution-v2` for schedules, and gives each conversation its own instance keyed by workspace, channel, and thread. Chickpea's retention pass never deletes a Flue transcript.

D1 `AUTH_DB` holds only what Better Auth owns: the opaque browser session and the Slack provider account. It is bound as `chickpea-auth-db` with migrations in `migrations/better-auth`, and a deploy stops before upload if the artifact omits the binding, changes its database ID, or points at a second database. Backing up or restoring `AUTH_DB` alone does not back up or restore the application. Chickpea provides no cross-store snapshot or restore tool today, so a Cloudflare recovery plan means keeping credential secrets recoverable and following the release's own migration procedure.

## On Node

Every store is a SQLite file, and the defaults derive from one path.

| Data | Default path | Variable |
|---|---|---|
| Conversation transcripts | `./tmp/flue.db` | `TAG_DB_PATH` |
| App state, identity, configuration, claims | `<TAG_DB_PATH>.state` | `SLACK_STATE_DB_PATH` |
| Better Auth sessions and accounts | `<SLACK_STATE_DB_PATH>.auth` | `CHICKPEA_AUTH_DB_PATH` |
| Slack credential encryption keys | `<SLACK_STATE_DB_PATH>.credential-keyring.json` | `CHICKPEA_CREDENTIAL_KEYRING_PATH` |

Those are development defaults. A production host sets all four explicitly, inside a private state directory owned by a dedicated account at mode 0700, never inside a checkout that an upgrade will replace. Never use `:memory:` for a production database.

Each state database runs in WAL mode, so `-wal` and `-shm` files sit beside the main file and belong to the same snapshot. One process serves one state directory: several processes against these files, or a network filesystem standing in for shared state, is not supported. A consistent backup therefore means stopping the service and snapshotting the whole state directory, including the WAL files, the keyring, and the runtime environment file. Copying live SQLite files is not a backup, and a backup is not proven until its restore has been tested on an isolated host.

## The credential keyring

Slack app credentials, connected Slack credentials, the gateway deployment key, and the Composio project key are sealed with AES-GCM-256 before they are written, as encrypted revisions rather than ordinary settings rows. Each envelope binds its deployment, identity, app, workspace, purpose, and revision into the ciphertext as associated data, so a revision cannot be replayed into another slot.

- **The keys live outside the databases.** On Cloudflare they are Worker secrets: `CHICKPEA_CREDENTIAL_KEY_CURRENT_ID` names the active key ID and a `CHICKPEA_CREDENTIAL_KEY_<ID>` slot holds it. The deploy wrapper creates `key_v1` on a fresh Worker and refuses to deploy over an existing one whose current ID or matching slot is missing.
- **The Node keyring is a file, not a database row.** It is created on first use at mode 0600 in a directory created at 0700, beside but never inside the state database. Chickpea refuses to start if its permissions allow group or other access.
- **The keyring is independent of the auth secret.** `CHICKPEA_AUTH_SECRET` signs Better Auth sessions and is never read as credential key material. Losing the keyring makes stored credentials unusable, so back it up with every database, not instead of them.
- **Connector credentials are stored by reference, outside that encrypted realm.** An API or MCP connection's credential is written to app state under its own secret reference, separate from the connection's policy, resolved per use with no cache, and overridden by a matching environment variable when one is set. Protecting those values is the state store's job.

## Retention

| Data | Retention | Where it is set |
|---|---|---|
| Run bodies in the work ledger | 30 days | `TAG_RUN_BODY_RETENTION_DAYS`, an integer from 1 to 365 |
| Raw usage operations | 90 days, rolled into daily aggregates first | Fixed in code |
| Usage aggregates | 13 months | Fixed in code |
| Gateway delivery deduplication record | 48 hours after the row is terminal | Fixed in code |
| Gateway inbox envelope | Scrubbed on completion, retry exhaustion, or recovery; at most 7 days if abandoned | Fixed in code |
| Transcripts, memory, audit events | No automatic expiry | Not configurable |

A run body is one ledger record, capped at 262,144 bytes, holding a trigger, a prepared input, a model output, or a rendered payload. Its expiry is stamped when it is written, so changing the retention variable moves new records only. The purge nulls the body and keeps the row with a `purged_at` timestamp, preserving the ledger's shape while removing its content. That pass runs from the Cloudflare deployment's one-minute cron and clears up to 100 records per tick; the Node target runs no scheduler, so nothing drives it there.

Memory is not on a clock. One Agent has one memory, capped at 8 KiB of body and 512 bytes of description, and it stays until somebody changes it.

## What is content-free

Some records exist to prove that something happened without keeping what it said.

- **Audit events carry hashes, not values.** Each row holds a before hash and an after hash of the subject, a bounded reason code, actor and subject identifiers, and a metadata object capped at 4,096 bytes whose keys are allowlisted per event type.
- **The deduplication record holds delivery identity only.** Delivery, binding, and workspace IDs, event kind, status, attempt count, timestamps, and a bounded reason code. No event or message body. When its one million protected rows are full, Chickpea rejects new admission rather than deleting an unexpired record.
- **The shared gateway does not durably store Slack message or event bodies.** It holds encrypted installation credentials and sanitized health metadata, and its delivery logs carry bounded reason codes such as `session_missing`.

## Why it works this way

Splitting the stores follows the blast radius. Transcripts are the largest and most sensitive body of text, so they sit in per-conversation objects that nothing else reads. Everything that answers an authority question, grants and members and bindings, sits in one place so a single consistent read can decide a turn.

Encryption keys live outside the databases on purpose. A snapshot of state without the keyring is not a usable copy of the credentials in it, which is what makes an off-host backup safe to keep.

## What is not covered

- **There is no cross-store snapshot on Cloudflare.** Durable Object state and D1 are backed up and restored by separate means, and Chickpea ships no tool that captures them together.
- **Node durability is single-host SQLite.** Multi-instance Node needs a shared state service.
- **A restore rolls state back in time.** It may lose recent work or replay events, and the old and restored copies must never answer Slack at once.
- **Retention is not deletion on request.** There is no per-person or per-channel erase operation; the knobs above are age-based.
- **Slack keeps its own copy.** Messages, files, and waiting event retries follow the workspace's Slack plan and Slack's policies, not anything on this page.

## Next steps

- [Security model](/security/security-model): the guarantees these stores hold up, and the mechanism behind each.
- [Deploy and operate](/operate): the Node backup procedure and the upgrade policy.
- [How Chickpea works](/start/how-it-works): the layers above these stores.
- [Environment variables](/reference/environment-variables): the exact values, including every environment variable named here.

Source: https://docs.chickpea.co/security/data-and-storage/index.mdx
