|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +> Project context for AI coding assistants. See [agents.md](https://agents.md) for the open standard. |
| 4 | +
|
| 5 | +## Architecture Overview |
| 6 | + |
| 7 | +Event-driven notification system for DAO governance, built as a **pnpm monorepo** with 4 microservices connected via RabbitMQ: |
| 8 | + |
| 9 | +1. **Logic System** (`apps/logic-system/`) - Polls AntiCapture GraphQL API every Xs, detects governance events, publishes trigger events |
| 10 | +2. **Dispatcher** (`apps/dispatcher/`) - Consumes trigger events, fetches subscribers with temporal filtering, routes notifications |
| 11 | +3. **Subscription Server** (`apps/subscription-server/`) - Fastify REST API for user preferences, PostgreSQL persistence, Slack OAuth |
| 12 | +4. **Consumer** (`apps/consumers/`) - Delivers notifications via Telegram (telegraf) and Slack (@slack/bolt) bots |
| 13 | + |
| 14 | +Supporting packages: `anticapture-client` (GraphQL), `messages` (templates), `rabbitmq-client` (AMQP wrapper). |
| 15 | + |
| 16 | +Dashboard (`apps/dashboard/`) provides read-only metrics via Next.js. |
| 17 | + |
| 18 | +## Essential Commands |
| 19 | + |
| 20 | +```bash |
| 21 | +pnpm install # Install all dependencies |
| 22 | +pnpm dev # Start all services with Docker Compose |
| 23 | +pnpm build # Build all services (via Turbo) |
| 24 | +pnpm test # Run all tests (via Turbo) |
| 25 | + |
| 26 | +# Service-specific (filter shortcuts) |
| 27 | +pnpm logic-system <cmd> # e.g., pnpm logic-system test |
| 28 | +pnpm dispatcher <cmd> |
| 29 | +pnpm subscription-server <cmd> |
| 30 | +pnpm consumer <cmd> |
| 31 | + |
| 32 | +# Testing specific services/patterns |
| 33 | +pnpm --filter @notification-system/logic-system test |
| 34 | +pnpm --filter @notification-system/integrated-tests test -- --testNamePattern="voting" |
| 35 | +NODE_ENV=test pnpm --filter @notification-system/integrated-tests test |
| 36 | + |
| 37 | +# Type checking and linting |
| 38 | +pnpm consumer check-types |
| 39 | +pnpm logic-system lint |
| 40 | + |
| 41 | +# GraphQL code generation (requires ANTICAPTURE_GRAPHQL_ENDPOINT) |
| 42 | +pnpm client codegen |
| 43 | +``` |
| 44 | + |
| 45 | +## Notification Pipeline |
| 46 | + |
| 47 | +``` |
| 48 | +Logic System (polls API every 30s) |
| 49 | + -> publishes TriggerEvent to dispatcher-queue |
| 50 | + -> Dispatcher consumes, fetches subscribers with temporal filtering |
| 51 | + -> publishes NotificationPayload to notifications.exchange (topic) |
| 52 | + -> Consumer binds notifications.<channel>.* per platform |
| 53 | + -> delivers via Telegram/Slack -> marks as sent in Subscription Server |
| 54 | +``` |
| 55 | + |
| 56 | +## Adding New Trigger Types |
| 57 | + |
| 58 | +Step-by-step guide for agents and developers. Use the same **trigger id** (e.g. `my-trigger`) in Logic System and Dispatcher so routing works. |
| 59 | + |
| 60 | +### 1. Logic System – detect event and publish |
| 61 | + |
| 62 | +- **Create trigger** in `apps/logic-system/src/triggers/`: extend `Trigger<T>` (see `base-trigger.ts`), implement `fetchData()` and `process(data[], lastTimestamp?)`. |
| 63 | +- **Optional:** If the trigger needs a dedicated data layer, add a repository in `apps/logic-system/src/repositories/` and use it from the trigger. |
| 64 | +- **Register** the trigger in `App.setupTriggers()` in `apps/logic-system/src/app.ts`. |
| 65 | + |
| 66 | +### 2. Dispatcher – handle event and route notifications |
| 67 | + |
| 68 | +- **Create handler** in `apps/dispatcher/src/services/triggers/`: extend `BaseTriggerHandler`, use the same trigger id as in Logic System. |
| 69 | +- **Register** the handler in `TriggerProcessorService` via `addHandler()` in `apps/dispatcher/src/app.ts`. |
| 70 | + |
| 71 | +### 3. Messages – templates and buttons |
| 72 | + |
| 73 | +- **Add message templates** in `packages/messages/src/triggers/` (e.g. `my-trigger.ts`) with `{{placeholder}}` syntax; export from `packages/messages/src/index.ts`. |
| 74 | +- **Add buttons config** in `packages/messages/src/triggers/buttons.ts` if the notification needs CTAs (e.g. explorer links). |
| 75 | + |
| 76 | +### 4. Unit tests |
| 77 | + |
| 78 | +- **Logic System:** Add tests in `apps/logic-system/tests/` (e.g. next to or mirroring the trigger). Use **stubs or fakes** for the RabbitMQ dispatcher and repositories (preferred over mocks); assert `fetchData()`/`process()` behavior. |
| 79 | +- **Dispatcher:** Add or extend co-located tests (e.g. `my-trigger.service.test.ts` in `apps/dispatcher/src/services/triggers/`). Use **stubs or fakes** for `ISubscriptionClient`, `INotificationClient`, and dependencies (preferred over mocks); assert handler logic and payload shape. |
| 80 | +- **Messages:** Add tests in `packages/messages` for new templates (placeholder replacement, edge cases) if non-trivial. |
| 81 | + |
| 82 | +### 5. Integration tests |
| 83 | + |
| 84 | +- **Add or extend** tests in `apps/integrated-tests/`: cover the new trigger flow (Logic System → Dispatcher → Consumer) using real RabbitMQ (testcontainers), in-memory DB, and stubs/fakes (or mocks during transition) for Telegram/Slack. Run with `NODE_ENV=test` and optionally filter: `pnpm --filter @notification-system/integrated-tests test -- --testNamePattern="my-trigger"`. |
| 85 | + |
| 86 | +### Checklist |
| 87 | + |
| 88 | +| Step | Location | Action | |
| 89 | +|------|----------|--------| |
| 90 | +| 1a | Logic System `src/triggers/` | New class extending `Trigger<T>`, implement `fetchData()` and `process()` | |
| 91 | +| 1b | Logic System `src/repositories/` | (Optional) New repository if trigger needs dedicated data access | |
| 92 | +| 1c | Logic System `src/app.ts` | Register trigger in `App.setupTriggers()` | |
| 93 | +| 2a | Dispatcher `src/services/triggers/` | New handler extending `BaseTriggerHandler` | |
| 94 | +| 2b | Dispatcher `src/app.ts` | Register handler with `TriggerProcessorService.addHandler()` | |
| 95 | +| 3a | Messages `src/triggers/` | New template file; export from `index.ts` | |
| 96 | +| 3b | Messages `src/triggers/buttons.ts` | Add button config if trigger has CTAs | |
| 97 | +| 4 | Logic System, Dispatcher, Messages | Add/update unit tests | |
| 98 | +| 5 | `apps/integrated-tests/` | Add or extend integration test for the new trigger flow | |
| 99 | + |
| 100 | +## Database Schema (Subscription Server) |
| 101 | + |
| 102 | +Key tables: |
| 103 | +- `users` - User profiles with `channel`, `channel_user_id` |
| 104 | +- `user_preferences` - DAO subscriptions with `is_active`, `created_at` (temporal filtering) |
| 105 | +- `user_notifications` - Delivery tracking for deduplication |
| 106 | +- `user_addresses` - Wallet addresses for personalized notifications |
| 107 | +- `channel_workspaces` - Slack workspace metadata |
| 108 | +- `slack_workspaces` - Encrypted OAuth tokens (AES-256-CBC) |
| 109 | + |
| 110 | +Migrations in `apps/subscription-server/db/migrations/` (Knex.js). |
| 111 | + |
| 112 | +## Testing Strategies |
| 113 | + |
| 114 | +**Unit tests:** Prefer **stubs and fakes** over mocks. The codebase still has many mocks; new tests and refactors should use stubs/fakes where possible (e.g. in-memory or fake implementations of interfaces) to improve maintainability and avoid over-coupling to implementation details. |
| 115 | + |
| 116 | +**Integration tests** (`apps/integrated-tests/`): Uses `@testcontainers/rabbitmq` for real RabbitMQ, SQLite in-memory DB, and stubs/fakes (or temporary mocks) for Telegram/Slack. Run with `NODE_ENV=test`. Prefer stubs and fakes for external service boundaries as we migrate away from mocks. |
| 117 | + |
| 118 | +**Dashboard tests:** Node.js built-in `test` module (`tsx --test`). |
| 119 | + |
| 120 | +## Environment Configuration |
| 121 | + |
| 122 | +Required `.env` variables: |
| 123 | +``` |
| 124 | +DATABASE_URL=postgresql://user:pass@localhost/dbname |
| 125 | +RABBITMQ_URL=amqp://localhost |
| 126 | +ANTICAPTURE_GRAPHQL_ENDPOINT=https://... |
| 127 | +TELEGRAM_BOT_TOKEN=... |
| 128 | +SLACK_SIGNING_SECRET=... |
| 129 | +TOKEN_ENCRYPTION_KEY=... # 64-char hex for AES-256-CBC |
| 130 | +``` |
| 131 | + |
| 132 | +## Code Conventions |
| 133 | + |
| 134 | +- **Language:** TypeScript (strict mode) across all services |
| 135 | +- **Validation:** Zod schemas for environment variables and API inputs |
| 136 | +- **Monorepo:** pnpm workspaces + Turbo for builds |
| 137 | +- **Testing:** Jest with ts-jest (most services), Node.js test runner (dashboard) |
| 138 | +- **Package manager:** pnpm 10.x, Node.js >= 18 |
| 139 | + |
| 140 | +## Deployment |
| 141 | + |
| 142 | +- GitHub Actions deploys to Railway on push to `dev` or `main` |
| 143 | +- Path-based triggers for selective service deployment |
| 144 | +- Docker Compose in `docker-compose.yml` for local development |
| 145 | +- Each service has its own `Dockerfile` |
| 146 | + |
| 147 | +## Manual Notification Testing (Database Inserts) |
| 148 | + |
| 149 | +To test without real blockchain events, insert mock data into the AntiCapture API database. The Logic System polls this data and triggers notifications. |
| 150 | + |
| 151 | +### Prerequisites |
| 152 | +1. Identify the correct schema (check `information_schema.schemata`) |
| 153 | +2. Find an active user in subscription-server with `is_active = true` |
| 154 | +3. Get the user's wallet address from `user_addresses` table |
| 155 | +4. Run the indexer locally (`pnpm serve`) or just use the onde deployed on dev. |
| 156 | + |
| 157 | +### Critical Notes |
| 158 | +- **Always disable triggers before INSERT**: Tables have `live_query` triggers that fail on manual inserts |
| 159 | +- **Use real `proposal_id`** for votes: Fake IDs cause API 500 errors |
| 160 | +- **Prefix mock tx_hash with `0xmock`**: Makes cleanup easy |
| 161 | +- **Use current timestamp**: `extract(epoch from now())::bigint` in SQL |
| 162 | + |
| 163 | +### Vote Confirmation Insert |
| 164 | +```sql |
| 165 | +SET search_path TO "<schema_uuid>"; |
| 166 | +ALTER TABLE votes_onchain DISABLE TRIGGER ALL; |
| 167 | + |
| 168 | +INSERT INTO votes_onchain (tx_hash, dao_id, voter_account_id, proposal_id, support, voting_power, reason, timestamp) |
| 169 | +VALUES ( |
| 170 | + '0xmock_vote_' || extract(epoch from now())::bigint, |
| 171 | + 'ENS', |
| 172 | + '<user_wallet_address>', |
| 173 | + '<real_proposal_id_from_proposals_onchain>', |
| 174 | + '1', -- 0=Against, 1=For, 2=Abstain |
| 175 | + 1000000000000000000, |
| 176 | + 'Mock vote for testing', |
| 177 | + extract(epoch from now())::bigint |
| 178 | +); |
| 179 | + |
| 180 | +ALTER TABLE votes_onchain ENABLE TRIGGER ALL; |
| 181 | +``` |
| 182 | + |
| 183 | +### Voting Power Change Insert (Delegation Received) |
| 184 | +```sql |
| 185 | +SET search_path TO "<schema_uuid>"; |
| 186 | +ALTER TABLE voting_power_history DISABLE TRIGGER ALL; |
| 187 | +ALTER TABLE delegations DISABLE TRIGGER ALL; |
| 188 | + |
| 189 | +INSERT INTO voting_power_history (transaction_hash, dao_id, account_id, voting_power, delta, delta_mod, timestamp, log_index) |
| 190 | +VALUES ( |
| 191 | + '0xmock_vp_' || extract(epoch from now())::bigint, |
| 192 | + 'ENS', |
| 193 | + '<user_wallet_address>', |
| 194 | + 5000000000000000000, |
| 195 | + 1000000000000000000, |
| 196 | + 1000000000000000000, |
| 197 | + extract(epoch from now())::bigint, |
| 198 | + 1 |
| 199 | +); |
| 200 | + |
| 201 | +INSERT INTO delegations (transaction_hash, dao_id, delegate_account_id, delegator_account_id, delegated_value, previous_delegate, timestamp, log_index) |
| 202 | +VALUES ( |
| 203 | + '0xmock_vp_' || extract(epoch from now())::bigint, |
| 204 | + 'ENS', |
| 205 | + '<user_wallet_address>', |
| 206 | + '0x1111111111111111111111111111111111111111', |
| 207 | + 1000000000000000000, |
| 208 | + '0x0000000000000000000000000000000000000000', |
| 209 | + extract(epoch from now())::bigint, |
| 210 | + 1 |
| 211 | +); |
| 212 | + |
| 213 | +ALTER TABLE voting_power_history ENABLE TRIGGER ALL; |
| 214 | +ALTER TABLE delegations ENABLE TRIGGER ALL; |
| 215 | +``` |
| 216 | + |
| 217 | +### Cleanup Mock Data |
| 218 | +```sql |
| 219 | +SET search_path TO "<schema_uuid>"; |
| 220 | +ALTER TABLE votes_onchain DISABLE TRIGGER ALL; |
| 221 | +ALTER TABLE voting_power_history DISABLE TRIGGER ALL; |
| 222 | +ALTER TABLE delegations DISABLE TRIGGER ALL; |
| 223 | + |
| 224 | +DELETE FROM votes_onchain WHERE tx_hash LIKE '0xmock%'; |
| 225 | +DELETE FROM voting_power_history WHERE transaction_hash LIKE '0xmock%'; |
| 226 | +DELETE FROM delegations WHERE transaction_hash LIKE '0xmock%'; |
| 227 | + |
| 228 | +ALTER TABLE votes_onchain ENABLE TRIGGER ALL; |
| 229 | +ALTER TABLE voting_power_history ENABLE TRIGGER ALL; |
| 230 | +ALTER TABLE delegations ENABLE TRIGGER ALL; |
| 231 | +``` |
| 232 | + |
| 233 | +### Finding Test Data |
| 234 | +```sql |
| 235 | +-- Get real proposal IDs |
| 236 | +SELECT id, LEFT(description, 50), status FROM proposals_onchain ORDER BY timestamp DESC LIMIT 5; |
| 237 | + |
| 238 | +-- Check for orphan votes (will cause API 500) |
| 239 | +SELECT v.* FROM votes_onchain v LEFT JOIN proposals_onchain p ON v.proposal_id = p.id WHERE p.id IS NULL; |
| 240 | + |
| 241 | +-- List mock records |
| 242 | +SELECT 'VOTE' as type, tx_hash, timestamp FROM votes_onchain WHERE tx_hash LIKE '0xmock%' |
| 243 | +UNION ALL SELECT 'VP', transaction_hash, timestamp FROM voting_power_history WHERE transaction_hash LIKE '0xmock%' |
| 244 | +UNION ALL SELECT 'DELEG', transaction_hash, timestamp FROM delegations WHERE transaction_hash LIKE '0xmock%'; |
| 245 | +``` |
0 commit comments