Operations CRM
Why I built it
The team was bouncing between Outlook, three marketplace dashboards (Amazon, eBay, Shopify), a spreadsheet for finances, and a separate ticket queue — each with its own login, schema, and notification noise. The original TicketSystem started as an Electron desktop app to consolidate Outlook + DynamoDB tickets, but distributing native installers and managing local credentials didn’t scale past a single workstation. Operations CRM is the rewrite: a static web app on Amplify that any teammate can open in a browser, backed by serverless Lambdas so there are no servers to babysit. The legacy Electron client has been retired.
Architecture
┌────────────────────────────────────────────────────────────┐
│ Static Next.js (S3 + CloudFront via AWS Amplify) │
│ - output: 'export', no SSR │
│ - apps/OperationsCRM (Next.js 14) │
└──────┬──────────────────────────────────────────┬──────────┘
│ fetch() with Cognito-issued JWT │ WSS (MQTT-over-WebSocket,
▼ │ SigV4-signed via Cognito)
┌──────────────────────────────┐ ▼
│ Amazon API Gateway │ ┌────────────────────────────┐
└──────────────┬───────────────┘ │ AWS IoT Core │
│ │ (WebSocket pub/sub) │
▼ └──────────────┬─────────────┘
┌────────────────────────────────────────────────────┴───────┐
│ AWS Lambda (infra/*-lambda) │
│ ├─ list-users-lambda identity directory │
│ ├─ outlook-oauth-lambda OAuth flow + token refresh │
│ ├─ sales-lambda Amazon / eBay / Shopify orders │
│ ├─ finance-lambda revenue, P&L, ledger queries │
│ ├─ sla-checker-lambda ticket SLA + metrics aggregate │
│ └─ assistant-lambda AI assistant + RAG → Bedrock │
└──────────────┬─────────────────────────────────┬───────────┘
│ │
▼ ▼
┌──────────────────────────────┐ ┌────────────────────────────┐
│ Amazon DynamoDB │ │ Amazon Bedrock │
│ (tickets, sales, finance, │ │ (Claude models for chat, │
│ auth tokens, RAG vectors) │ │ embeddings for RAG) │
└──────────────────────────────┘ └────────────────────────────┘
Auth: Amazon Cognito (client-side, JWT for HTTPS, SigV4 for IoT WSS)
- No SSR.
output: 'export'produces a fully staticdist/artifact — Amplify just serves it from S3 + CloudFront. - No backend in the Next.js app. All data calls go through API Gateway → Lambda.
- Auth is client-side. Amazon Cognito hosts users and issues JWTs that the frontend attaches to every request.
- Real-time updates over WebSockets. AWS IoT Core fans out ticket, mailbox, and sales events to connected browsers as MQTT-over-WebSocket messages — no polling, and no API Gateway WebSocket connection table to manage.
- Generative AI on Bedrock. The assistant, reply rewriter, and RAG embeddings all run through Amazon Bedrock so model access stays inside the same AWS account as the data.
Real-time updates (WebSockets via AWS IoT Core)
The frontend opens a single WSS connection to AWS IoT Core, signed with SigV4 using the Cognito identity, and subscribes to per-workspace MQTT topics:
ops/{workspace}/tickets— new tickets, status changes, assignment changesops/{workspace}/mail/{mailbox}— incoming Outlook messages routed byoutlook-oauth-lambdaops/{workspace}/sales— new marketplace orders picked up bysales-lambdaops/{workspace}/assistant/{sessionId}— token-by-token streaming from the AI assistant
Lambdas publish to these topics as a side effect of the work they’re already doing, so the UI updates the moment data lands in DynamoDB instead of waiting for the next poll. IoT Core handles connection state, reconnection, and fan-out, which is why there is no custom WebSocket service in the Lambda list.
Repository layout
| Path | Purpose |
|---|---|
apps/OperationsCRM/ |
Next.js 14 static web client (the only frontend) |
infra/ |
DynamoDB scripts, Cognito role setup, Lambda source |
docs/ |
Product gap analysis and roadmap notes |
amplify.yml |
Amplify build spec — produces dist/ from apps/OperationsCRM |
Outlook integration
Microsoft Outlook is the team’s primary inbox, and it’s where tickets actually start. The outlook-oauth-lambda handles:
- PKCE-based OAuth2 authorization code flow against Microsoft Entra (Azure AD)
- Per-user token storage in DynamoDB with automatic refresh before expiry
- Microsoft Graph scopes:
Mail.ReadWrite,Mail.Send,User.Read - Multi-mailbox support — a user can connect several inboxes (e.g. sales@, support@, vendor@) and label each one
- Auto-routing — incoming threads are moved to a “Tickets” folder and recorded as tickets in DynamoDB
In the UI an agent sees their open conversations as a unified ticket queue, replies inline, and the reply goes back out via Graph. There is no separate webmail tab to manage.
Marketplace integrations — Amazon, eBay, Shopify
The sales-lambda is the single entry point for marketplace data. The frontend calls it for orders, revenue, and customer lookups, and it fans out to:
- Amazon — order, fulfillment, and customer data via Amazon’s seller APIs
- eBay — order and listing data via eBay’s REST APIs
- Shopify — orders, customers, and storefront data via Shopify’s Admin API
Marketplace data lands in DynamoDB on a normalized schema, so a ticket about an Amazon order and a ticket about a Shopify order render in the same view with the same fields. Order numbers detected in ticket bodies become deep links back to the originating marketplace dashboard.
AI assistant
A floating assistant available from any screen, with three things that make it useful instead of a toy:
- Persistent memory. The assistant keeps a memory file per workspace — facts about the team, preferred phrasing, repeat customers, brand-specific quirks. It carries between sessions instead of starting fresh every time.
- Agentic actions. The assistant can take real actions on request: pull up a customer record, look up an order across marketplaces, draft a reply with brand voice applied, escalate or close a ticket. Multi-step workflows are delegated in plain language.
- Custom RAG. Internal technical documents (vehicle specs, fitment data, policy docs, vendor sheets) are chunked, embedded with Bedrock embedding models, and stored as a vector index. When the assistant or a reply draft needs domain knowledge, the relevant chunks are pulled in and cited in the response.
Inference runs through Amazon Bedrock — Claude models for chat, drafting, and rewrites, and Bedrock embedding models for the RAG index. Tokens stream back to the browser over the IoT Core WebSocket so the assistant feels live instead of request/response.
The reply composer reuses the same stack: an agent writes a draft, hits the AI button, and gets a rewrite shaped by the brand voice and the cited RAG context. Word-level diffs let the agent accept or reject before sending.
Metrics dashboard
A reporting view backed by the sla-checker-lambda and the same DynamoDB-backed query layer the rest of the app uses. It surfaces the operational numbers a small ops team actually looks at:
- Ticket volume by inbox, marketplace, and time window
- First-response and resolution time, with SLA breach flagging
- Per-agent activity (tickets touched, replies sent, average handling time)
- Sales totals and order counts by marketplace
- Trend lines so week-over-week and month-over-month moves are obvious
The dashboard is a read view — no destructive actions — so it is safe to share with stakeholders who should not be editing tickets.
Tech stack
- Next.js 14 (
output: 'export', static-only, no SSR) - AWS Amplify (build pipeline + CloudFront + S3 hosting)
- Amazon API Gateway + AWS Lambda (Node.js, one Lambda per concern)
- AWS IoT Core (MQTT-over-WebSocket for real-time ticket, mail, sales, and assistant streams)
- Amazon Bedrock (Claude models for chat and rewrites, embedding models for RAG)
- Amazon DynamoDB (tickets, sales, finance, settings, auth tokens, RAG vectors)
- Amazon Cognito (user pool, JWT for HTTPS, SigV4 credentials for IoT WSS)
- Microsoft Graph API + OAuth2 PKCE (email send/receive)
- Amazon / eBay / Shopify seller and admin APIs