mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 21:05:21 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The Decisions desk shows work that needs a human decision > - The queue foundation can group and rank decision work > - Operators also need a focused daily view and a safe way to handle old work > - This pull request adds the desk controls, the aging shelf, and reversible retention > - It also binds bulk archive decisions to the exact reviewed item set > - The benefit is a smaller daily queue without lost or orphaned work ## Linked Issues or Issue Description **Subsystem affected** Cross-cutting: server, UI, database, and shared contracts. **Problem or motivation** Decision work can grow into one large company list. Operators need quick queue and date controls. Old items also need a safe retention path that does not delete work. **Proposed solution** Add queue and date controls to the Decisions desk. Compute the aging shelf on the server. Archive idle items after 90 days unless an operator keeps them. Keep archived items searchable and revivable. Notify origin agents in one batch per sweep. Bind bulk archive proposals to a signed, exact item manifest. **Alternatives considered** Client-only aging can drift across browsers and source kinds. Deleting old rows removes audit and recovery paths. An unsigned dynamic bulk query can archive items that the reviewer did not inspect. **Roadmap alignment** This work supports the Work Queues and decision-memory directions in `ROADMAP.md`. It extends the Decisions and attention-feed foundation from #10651. Related earlier work includes #9380, #10010, and #10474. ## What Changed - Added the queue rail, date chips, decide split, triage strip, queue page, and aging shelf UI. - Added server-owned shelf state with per-queue retention overrides. - Added reversible retention state, archive history, and an idempotent notification outbox. - Added the 90-day archive sweeper, Keep exemption, archived feed query, and revive actions. - Added one origin-agent notification per agent and sweep. - Added signed bulk archive proposals with exact-set and version checks. - Persisted queue-exclusion reasons atomically and kept cross-domain source resolution per-item until it has an exact-set transaction contract. - Added API contracts, OpenAPI entries, migration coverage, focused tests, and Storybook screens. ## Verification - `pnpm -r typecheck` - `pnpm test:run` (server: 329 files and 3,453 tests passed; UI: 408 files and 3,362 tests passed) - `env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY pnpm exec vitest run cli/src/__tests__/secrets.test.ts` - `pnpm build` - `pnpm check:token-gates` - Focused retention, attention, decisions, migration replay, startup, and UI API tests. - Complete queue snapshot regression with 51 items across the normal 50-item page boundary. The unmodified CLI test reports one warning assertion in this runtime because the harness injects static AWS credential variables. The isolated test passes when those two variables are removed. ## Risks - The migration adds retention and notification outbox tables. It uses idempotent table, index, and foreign-key creation. - Retention runs on the heartbeat scheduler interval. Compare-and-set version checks prevent stale archive writes. - Bulk archive acceptance fails closed when authority, activity, version, or the reviewed set changes. - Archive is reversible and does not delete source records. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used OpenAI Codex with `gpt-5.6-sol`. The model used tool calls, code execution, database migration generation, and test execution. The context-window size is not exposed in this runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Paperclip <noreply@paperclip.ing>
266 lines
12 KiB
Markdown
266 lines
12 KiB
Markdown
# Database
|
|
|
|
Paperclip uses PostgreSQL via [Drizzle ORM](https://orm.drizzle.team/). There are three ways to run the database, from simplest to most production-ready.
|
|
|
|
## 1. Embedded PostgreSQL — zero config
|
|
|
|
If you don't set `DATABASE_URL`, the server automatically starts an embedded PostgreSQL instance and manages a local data directory.
|
|
|
|
```sh
|
|
pnpm dev
|
|
```
|
|
|
|
That's it. On first start the server:
|
|
|
|
1. Creates a `~/.paperclip/instances/default/db/` directory for storage
|
|
2. Ensures the `paperclip` database exists
|
|
3. Runs migrations automatically for empty databases
|
|
4. Starts serving requests
|
|
|
|
Data persists across restarts in `~/.paperclip/instances/default/db/`. To reset local dev data, delete that directory.
|
|
|
|
If you need to apply pending migrations manually, run:
|
|
|
|
```sh
|
|
pnpm db:migrate
|
|
```
|
|
|
|
When `DATABASE_URL` is unset, this command targets the current embedded PostgreSQL instance for your active Paperclip config/instance.
|
|
|
|
Issue reference mentions follow the normal migration path: the schema migration creates the tracking table, but it does not backfill historical issue titles, descriptions, comments, or documents automatically.
|
|
|
|
To backfill existing content manually after migrating, run:
|
|
|
|
```sh
|
|
pnpm issue-references:backfill
|
|
# optional: limit to one company
|
|
pnpm issue-references:backfill -- --company <company-id>
|
|
```
|
|
|
|
Future issue, comment, and document writes sync references automatically without running the backfill command.
|
|
|
|
This mode is ideal for local development and one-command installs.
|
|
|
|
Docker note: the Docker quickstart image also uses embedded PostgreSQL by default. Persist `/paperclip` to keep DB state across container restarts (see `doc/DOCKER.md`).
|
|
|
|
## 2. Local PostgreSQL (Docker)
|
|
|
|
For a full PostgreSQL server locally, use the included Docker Compose setup:
|
|
|
|
```sh
|
|
docker compose up -d
|
|
```
|
|
|
|
This starts PostgreSQL 17 on `localhost:5432`. Then set the connection string:
|
|
|
|
```sh
|
|
cp .env.example .env
|
|
# .env already contains:
|
|
# DATABASE_URL=postgres://paperclip:paperclip@localhost:5432/paperclip
|
|
```
|
|
|
|
Run migrations:
|
|
|
|
```sh
|
|
DATABASE_URL=postgres://paperclip:paperclip@localhost:5432/paperclip \
|
|
pnpm db:migrate
|
|
```
|
|
|
|
Start the server:
|
|
|
|
```sh
|
|
pnpm dev
|
|
```
|
|
|
|
## 3. Hosted PostgreSQL (Supabase)
|
|
|
|
For production, use a hosted PostgreSQL provider. [Supabase](https://supabase.com/) is a good option with a free tier.
|
|
|
|
### Setup
|
|
|
|
1. Create a project at [database.new](https://database.new)
|
|
2. Go to **Project Settings > Database > Connection string**
|
|
3. Copy the URI and replace the password placeholder with your database password
|
|
|
|
### Connection string
|
|
|
|
Supabase offers two connection modes:
|
|
|
|
**Direct connection** (port 5432) — use for migrations and one-off scripts:
|
|
|
|
```
|
|
postgres://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres
|
|
```
|
|
|
|
**Connection pooling via Supavisor** (port 6543) — use for the application:
|
|
|
|
```
|
|
postgres://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres
|
|
```
|
|
|
|
### Configure
|
|
|
|
For the application runtime, use a direct PostgreSQL connection unless the database client has explicit prepared-statement configuration for your pooling mode:
|
|
|
|
```sh
|
|
DATABASE_URL=postgres://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres
|
|
```
|
|
|
|
If you later run the app with a pooled runtime URL, set `DATABASE_MIGRATION_URL` to the direct connection URL. Paperclip uses it for startup schema checks/migrations and plugin namespace migrations, while the app continues to use `DATABASE_URL` for runtime queries:
|
|
|
|
```sh
|
|
DATABASE_URL=postgres://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres
|
|
DATABASE_MIGRATION_URL=postgres://postgres.[PROJECT-REF]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:5432/postgres
|
|
```
|
|
|
|
If your hosted database requires transaction-pooling-only connections, use a direct or session-pooled connection for Paperclip until runtime pooling support is documented in this guide. Do not edit database client source files as part of deployment setup.
|
|
|
|
### Push the schema
|
|
|
|
```sh
|
|
# Use the direct connection (port 5432) for schema changes
|
|
DATABASE_URL=postgres://postgres.[PROJECT-REF]:[PASSWORD]@...5432/postgres \
|
|
pnpm db:migrate
|
|
```
|
|
|
|
### Free tier limits
|
|
|
|
- 500 MB database storage
|
|
- 200 concurrent connections
|
|
- Projects pause after 1 week of inactivity
|
|
|
|
See [Supabase pricing](https://supabase.com/pricing) for current details.
|
|
|
|
## Switching between modes
|
|
|
|
The database mode is controlled by `DATABASE_URL`:
|
|
|
|
| `DATABASE_URL` | Mode |
|
|
|---|---|
|
|
| Not set | Embedded PostgreSQL (`~/.paperclip/instances/default/db/`) |
|
|
| `postgres://...localhost...` | Local Docker PostgreSQL |
|
|
| `postgres://...supabase.com...` | Hosted Supabase |
|
|
|
|
Your Drizzle schema (`packages/db/src/schema/`) stays the same regardless of mode.
|
|
|
|
## Migration authoring checklist
|
|
|
|
The 0126 issue comment attribution backfill showed the failure mode this checklist is meant to prevent: each batch looked for the next rows with an unindexed predicate, so PostgreSQL repeatedly scanned the same table and the migration became O(n²) as the table grew.
|
|
|
|
When authoring migrations or one-time backfills:
|
|
|
|
- Create the supporting index for the batch predicate before the backfill loop runs.
|
|
- Bound batches by an indexed key, such as an id range or keyset pagination cursor. Do not use `OFFSET` pagination or a query shape that re-scans already-visited rows each batch.
|
|
- Avoid unbounded full-table `UPDATE` or `DELETE` statements. Add a selective predicate and process rows in bounded batches when table size can be large.
|
|
- Use `CREATE INDEX CONCURRENTLY` for large existing tables when the migration can run outside a transaction and must avoid long write locks.
|
|
- Split schema changes, index creation, and data backfill into separate phases so each step has clear locking and rollback behavior.
|
|
- Treat the `check:migrations` CI gate as the enforcement backstop for these rules. If it flags a migration, rewrite the migration or add a suppression comment with the indexed predicate, batch bound, and reason the remaining scan is safe.
|
|
|
|
## Resource membership tables
|
|
|
|
Paperclip stores current-user sidebar membership state in:
|
|
|
|
- `project_memberships`
|
|
- `agent_memberships`
|
|
|
|
These rows are company-scoped and user-scoped. A missing row means the user is joined, so existing users keep seeing projects and agents in the sidebar until they explicitly leave them. Rows only control sidebar visibility; they do not affect project/agent detail access, all-pages, selectors, assignment flows, or existing company permissions.
|
|
|
|
Both tables use a unique key on `(company_id, user_id, resource_id)` and keep `state` as `joined` or `left`. Join/leave mutations are idempotent board-user `/me` operations and write activity entries when the effective state changes.
|
|
|
|
## Decision training snapshot retention
|
|
|
|
`decision_training_examples` stores a point-in-time copy of an issue, its comments, relevant runs, and the selected decision. Each row carries the `scrub_deleted_comments_v1` retention policy marker, and JSONL exports include that marker alongside the snapshot.
|
|
|
|
- Deleting a captured source comment transactionally replaces that comment in every affected snapshot with a content-free redaction tombstone. The original body, presentation, and metadata are not retained in the training record.
|
|
- Deleting an issue deletes its decision-training examples through the `issue_id` foreign-key cascade.
|
|
- Deleting a training example deletes only that example and does not mutate the source issue.
|
|
|
|
This policy makes training exports self-describing while keeping the decision record usable after a comment deletion without retaining content the author removed.
|
|
|
|
## Decision queues and triage provenance
|
|
|
|
The decisions desk stores queue membership, decide-by/snooze state, and retention state in `decision_queues`, `decision_queue_items`, `decision_triage`, and `decision_retention`. These sidecars use the stable attention identity `(source_kind, source_id)` so all attention source kinds can participate without copying source titles, bodies, projects, or other visibility-sensitive data.
|
|
|
|
`decision_triage_events` is append-only history for queue and triage changes. Current rows and history both carry server-derived user/agent, heartbeat run, API-key, and responsible-user attribution where applicable. Queue reads must resolve and authorize their source rows at read time; a sidecar row is never a visibility grant.
|
|
|
|
Triage writes serialize on the company and attention-source identity so concurrent partial updates preserve both fields and produce monotonic history versions.
|
|
|
|
`decision_retention` tracks the last observed source `activityAt`, Keep, reversible archive provenance, and monotonic source/archive versions. `decision_archive_notification_outbox` has a unique key over company, source identity, archive version, and immutable origin agent so repeated sweeps cannot enqueue duplicate notifications; delivery claims are retryable and coalesced per agent.
|
|
|
|
## Plugin database namespaces
|
|
|
|
The plugin runtime tracks plugin-owned database namespaces and migrations in `plugin_database_namespaces` and `plugin_migrations`. Hosted deployments that separate runtime and migration connections should set `DATABASE_MIGRATION_URL`; plugin namespace migration work uses the migration connection when present.
|
|
|
|
## Backups
|
|
|
|
Paperclip supports automatic and manual logical database backups. These dumps include
|
|
non-system database schemas such as `public`, the Drizzle migration journal, and
|
|
plugin-owned database schemas. See `doc/DEVELOPING.md` for the current
|
|
`paperclipai db:backup` / `pnpm db:backup` commands and backup retention
|
|
configuration.
|
|
|
|
Database backups do not include non-database instance files such as local-disk
|
|
uploads, workspace files, or the local encrypted secrets master key. Back those paths
|
|
up separately when you need full instance disaster recovery.
|
|
|
|
## Secret storage
|
|
|
|
Paperclip stores secret metadata and versions in:
|
|
|
|
- `user_secret_definitions`
|
|
- `user_secret_declarations`
|
|
- `company_secrets`
|
|
- `company_secret_versions`
|
|
- `company_secret_bindings`
|
|
- `secret_access_events`
|
|
|
|
Company secrets use `company_secrets.scope = 'company'` and are bound directly
|
|
through `company_secret_bindings`. User-specific secrets reuse the same provider
|
|
and version storage, but each value is a `company_secrets.scope = 'user'` row
|
|
with `owner_user_id` and `user_secret_definition_id` set. Definitions describe
|
|
the reusable company-level slot, declarations record where `user_secret_ref`
|
|
bindings are required, and the concrete value is selected later for the
|
|
responsible user.
|
|
|
|
Secret-aware env bindings are supported by agents, projects, and routines. Routine env lives in `routines.env`, is captured in `routine_revisions.snapshot`, and routine dispatches store `routine_runs.routine_revision_id` so runtime secret resolution uses the env snapshot that existed when the run was created. Routine secret refs bind with `target_type = 'routine'`, `target_id = routines.id`, and `config_path` values under `env.*`.
|
|
|
|
For local/default installs, the active provider is `local_encrypted`:
|
|
|
|
- Secret material is encrypted at rest with a local master key.
|
|
- Default key file: `~/.paperclip/instances/default/secrets/master.key` (auto-created if missing).
|
|
- CLI config location: `~/.paperclip/instances/default/config.json` under `secrets.localEncrypted.keyFilePath`.
|
|
- Backup/restore requires both the database metadata and the local master key file; either artifact alone is insufficient.
|
|
- The server best-effort enforces `0600` key file permissions and provider health reports permission warnings.
|
|
- User-scoped values use the same local encrypted provider path. Database
|
|
backups preserve definitions, declarations, owner metadata, version metadata,
|
|
and access events, but restored user-scoped values are decryptable only when
|
|
the matching local master key is restored with the database.
|
|
|
|
Optional overrides:
|
|
|
|
- `PAPERCLIP_SECRETS_MASTER_KEY` (32-byte key as base64, hex, or raw 32-char string)
|
|
- `PAPERCLIP_SECRETS_MASTER_KEY_FILE` (custom key file path)
|
|
|
|
Strict mode to block new inline sensitive env values:
|
|
|
|
```sh
|
|
PAPERCLIP_SECRETS_STRICT_MODE=true
|
|
```
|
|
|
|
You can set strict mode and provider defaults via:
|
|
|
|
```sh
|
|
pnpm paperclipai configure --section secrets
|
|
```
|
|
|
|
Inline secret migration command:
|
|
|
|
```sh
|
|
pnpm paperclipai secrets migrate-inline-env --company-id <company-id> --apply
|
|
|
|
# direct database maintenance fallback
|
|
pnpm secrets:migrate-inline-env --apply
|
|
```
|
|
|
|
Hosted AWS provider notes live in [SECRETS-AWS-PROVIDER.md](./SECRETS-AWS-PROVIDER.md).
|