Guided walkthrough of adding a complete feature to a real Node.js/Express project: planning tasks, running agents, reviewing diffs, and shipping the change.
Go from feature idea to committed code using the full Paperclip dev-assistant workflow. We'll add a "recent activity" feed to a Node.js/Express + PostgreSQL API — a realistic feature touching migrations, routes, tests, and a small service layer.
"Recent Activity Feed" — an endpoint that returns the last 20 actions a user has taken (logins, profile updates, password changes), pulled from an activity_log table.
We'll do it in five agent tasks.
# Start on a clean branch
cd ~/projects/my-api
git checkout main
git pull
git checkout -b paperclip/activity-feed
# Confirm Paperclip is running
curl -s http://localhost:3100/api/health | python3 -m json.tool
# Set shell vars
export BASE=http://localhost:3100/api
export COMPANY_ID=<id>
export AGENT_ID=<claude-code-agent-id>
curl -X POST $BASE/companies/$COMPANY_ID/issues \
-H "Content-Type: application/json" \
-d '{
"title": "Add activity_log table migration",
"description": "## Task\nCreate a new Drizzle ORM migration that adds an `activity_log` table.\n\n## Schema\n```\nactivity_log\n id uuid primary key default gen_random_uuid()\n user_id uuid not null references users(id) on delete cascade\n action text not null -- e.g. \"login\", \"profile_update\", \"password_change\"\n metadata jsonb -- optional context\n created_at timestamptz not null default now()\n```\nAdd an index on `(user_id, created_at DESC)` for the feed query.\n\n## Files\n- Create migration in `db/migrations/` following the existing numbering pattern\n- Update `src/db/schema.ts` to export the new table definition\n\n## Acceptance criteria\n- [ ] Migration file created with correct SQL\n- [ ] Schema exported from `src/db/schema.ts`\n- [ ] `npm run db:migrate` runs without errors\n- [ ] `npm test` passes\n\n## Do not change\n- Do not modify any existing tables",
"assigned_agent_id": "'$AGENT_ID'",
"priority": "high"
}'
Review: Check the migration SQL is correct, the schema export is clean, npm run db:migrate ran. Accept and commit.
git add -A
git commit -m "feat: add activity_log table migration
Paperclip agent task 1/5 — activity feed feature"
curl -X POST $BASE/companies/$COMPANY_ID/issues \
-H "Content-Type: application/json" \
-d '{
"title": "Add ActivityService with logActivity and getRecentActivity methods",
"description": "## Context\nMigration added `activity_log` table (id, user_id, action, metadata, created_at). Schema is exported from `src/db/schema.ts` as `activityLog`.\n\n## Task\nCreate `src/services/activity.ts` with two exported functions:\n\n### logActivity(userId: string, action: string, metadata?: Record<string, unknown>): Promise<void>\nInserts a row into activity_log.\n\n### getRecentActivity(userId: string, limit = 20): Promise<ActivityEntry[]>\nReturns the last `limit` entries for the user, ordered by created_at DESC.\nReturn type: `{ id: string, action: string, metadata: unknown, createdAt: Date }[]`\n\n## Patterns\n- Use Drizzle ORM (`db.insert`, `db.select`) — see `src/services/user.ts` for examples\n- No raw SQL\n\n## Acceptance criteria\n- [ ] `src/services/activity.ts` created and exported\n- [ ] Both functions typed correctly with no `any`\n- [ ] Unit tests in `tests/activity.service.test.ts`\n- [ ] `npm test` passes",
"assigned_agent_id": "'$AGENT_ID'",
"priority": "high"
}'
Review: Check Drizzle usage, types, tests. Accept and commit.
curl -X POST $BASE/companies/$COMPANY_ID/issues \
-H "Content-Type: application/json" \
-d '{
"title": "Add GET /api/users/:id/activity endpoint",
"description": "## Context\n`src/services/activity.ts` now exports `getRecentActivity(userId, limit)`.\n\n## Task\nAdd a new route to `src/routes/users.ts`:\n- Method: GET\n- Path: /api/users/:id/activity\n- Auth: requireAuth middleware (already used in this file)\n- Authorization: users can only fetch their own activity (req.user.id must equal params.id)\n- Query param: `?limit=20` (max 50, default 20)\n- Response: `{ activity: ActivityEntry[] }`\n- 403 if user tries to access another user'\''s activity\n- 404 if user not found\n\n## Acceptance criteria\n- [ ] Route added to `src/routes/users.ts`\n- [ ] Registered in `src/app.ts`\n- [ ] Integration test in `tests/users.test.ts`\n- [ ] `npm test` passes",
"assigned_agent_id": "'$AGENT_ID'",
"priority": "high"
}'
Review: Check auth, 403 enforcement, test coverage. Accept and commit.
curl -X POST $BASE/companies/$COMPANY_ID/issues \
-H "Content-Type: application/json" \
-d '{
"title": "Log activity events on login, profile update, and password change",
"description": "## Context\n`src/services/activity.ts` exports `logActivity(userId, action, metadata?)`.\n\n## Task\nCall `logActivity` at three points (do not await — fire and forget to avoid slowing the response):\n1. `src/routes/auth.ts` — after successful login: `logActivity(user.id, \"login\", { ip: req.ip })`\n2. `src/routes/users.ts` PATCH handler — after profile update: `logActivity(user.id, \"profile_update\")`\n3. `src/routes/auth.ts` — after password change: `logActivity(user.id, \"password_change\")`\n\n## Acceptance criteria\n- [ ] All three call sites added\n- [ ] Calls are fire-and-forget (void, no await)\n- [ ] `npm test` passes\n\n## Do not change\n- Do not modify the response shape of any existing endpoint",
"assigned_agent_id": "'$AGENT_ID'",
"priority": "medium"
}'
Review: Confirm fire-and-forget pattern (no awaiting), no response changes. Accept and commit.
curl -X POST $BASE/companies/$COMPANY_ID/issues \
-H "Content-Type: application/json" \
-d '{
"title": "Add end-to-end test: login generates an activity entry visible in the feed",
"description": "## Task\nAdd a test in `tests/activity.integration.test.ts` that:\n1. Creates a test user\n2. Logs in (POST /api/auth/login)\n3. Fetches the activity feed (GET /api/users/:id/activity)\n4. Asserts that exactly one `login` entry exists in the response\n\nUse the existing test setup helpers in `tests/helpers.ts`.\n\n## Acceptance criteria\n- [ ] Test exists and covers the full flow\n- [ ] `npm test` passes (all tests, not just the new one)",
"assigned_agent_id": "'$AGENT_ID'",
"priority": "medium"
}'
Review: Run the test yourself, confirm the integration test works. Accept and commit.
# Review the full feature diff
git diff main
# Run all tests one last time
npm test
# Merge or raise a PR
git checkout main
git merge --no-ff paperclip/activity-feed -m "feat: add recent activity feed
5 tasks completed via Paperclip dev assistant:
- activity_log table migration
- ActivityService (logActivity + getRecentActivity)
- GET /api/users/:id/activity endpoint
- Activity logging on login, profile update, password change
- End-to-end integration test"
You built a complete, tested feature across migrations, service layer, routes, and integration tests — using Paperclip as your dev assistant — without writing a single line of code yourself. You wrote task descriptions instead of code, and reviewed diffs instead of debugging from scratch.
This is the Paperclip dev-assistant workflow at full speed.
CLAUDE.md reduce the amount you needed to write in each issue description?