Internal Slack assistant for Sea Haven Industries, powered by AWS Bedrock. Employees can DM Alex or @mention Alex in any channel to ask about vendors, payments, work orders, purchase orders, Amazon sites, company policies, and more.
This bot reads the `purchase-orders` DynamoDB table **read-only** (via the `po-sync` and `wo-po-lookup` Lambdas, both granted `grantReadData`). The table is owned by the `procurement-ingest` repo (`po-ingest` stack), which is the sole authoritative writer. Any change to the `purchase-orders` schema must be coordinated with `procurement-ingest` (owner) and `payments-dashboard` (the other read-only consumer).
This bot does **not own any DynamoDB tables.** It imports five tables owned by other stacks via `Table.fromTableName(...)` (`lib/constructs/bedrock-agent.ts`, `workorder-sync.ts`, `po-sync.ts`) and reads them read-only. Because the tables are imported by name, this stack has no compile-time link to the owner: an owner-side change to a table's name, key schema, attribute names, GSIs, encryption key, or lifecycle policy will **silently break the Bedrock agent (Alex)** at runtime, not at deploy. The CMK grant-gap incident (INFRA-95 / M-3) is precedent: because `grantReadData` on an imported table does not carry KMS permissions, every read failed with `kms:Decrypt AccessDenied` until an explicit grant was added.
Owner-side changes to any of these tables must be coordinated with this repo before they ship. Treat them as cross-repo migrations, not local edits.
| Table | Owner repo / stack | Keys the bot depends on | Attributes the bot reads | Encryption |
**Known broken contract (verified-sites `by-state` GSI):** `lambda/wo-po-lookup/index.ts` still queries `IndexName: 'by-state'` for state-based site listings, but the owner removed that GSI on 2026-06-03 (procurement-ingest audit M-20, "0 reads in 30d"). State listings (`lookup_site` with a `state` arg) therefore fail at runtime today. Point-lookups by `siteCode` are unaffected. This needs either the GSI restored owner-side or the state-listing path removed from the bot.
The canonical map of Sea Haven's AWS infrastructure lives in Confluence. This project's `seahaven-slack-bot` stack is represented there as a Mermaid subgraph.
- **[AWS Architecture Map](https://seahaven.atlassian.net/wiki/spaces/IT/pages/1540098)** (Confluence, IT space, page 1540098)
These secrets must exist before deploying. The Slack, QBO, Maps, and app-level token secrets must be created manually before first deploy. The Notion secret is created automatically by CDK with a placeholder.
- **QBO OAuth** — the refresh token auto-rotates on every API call (persisted back to Secrets Manager). If the token ever expires (100 days of inactivity), reconnect via `https://bot.seahaven.com/qbo/connect`.
- **Notion sync** runs daily at 02:00 UTC automatically. Manual trigger: invoke `seahaven-notion-sync`.
- **PO sync** runs daily at 02:00 UTC. Scans `purchase-orders` DynamoDB table (from [po-ingest](https://github.com/Sea-Haven-Industries/po-ingest)). Manual trigger: invoke `seahaven-po-sync`.
- **Work order sync** runs daily at 02:00 UTC. Scans `WorkOrders` and `WorkOrderComments` (from [workorder-ingest](https://github.com/Sea-Haven-Industries/workorder-ingest)). Manual trigger: invoke `seahaven-workorder-sync`.
- **Site assignments** are auto-populated from the `verified-sites` DynamoDB table, maintained by the po-ingest DynamoDB Streams pipeline. No manual seeding required.
- **Unanswered questions** are logged to `seahaven-unanswered-questions` when Alex detects it couldn't answer a question. Review via DynamoDB console — records have `status: "pending"` and 180-day TTL.
- **Socket Mode** — the ECS Fargate task maintains a persistent WebSocket connection to Slack. Monitor via CloudWatch Logs (`socket-mode` log stream). The service auto-restarts on failure.