AI-powered revenue intelligence for sales and revenue teams. Early warning for pipeline risk—predictions, recommendations, and real-time visibility so you never lose a deal to silent decay.
Live demo: https://www.sentinel.parbhat.dev · API Reference · Documentation
Tech stack: Next.js, TypeScript, PostgreSQL, Prisma, Clerk, OpenRouter (AI), Upstash Redis, Sentry.
Revenue teams lose an estimated $1.3 trillion annually due to poor pipeline visibility. Deals stall silently: prospects stop replying, meetings slip, proposals go unread. By the time traditional CRMs surface the problem, relationships have cooled and opportunities are lost.
Sentinel adds an AI intelligence layer on top of your pipeline. It computes predictive risk scores from temporal decay, stage velocity, and engagement patterns; detects at-risk deals before they fail; and surfaces actionable recommendations. Natural-language queries, webhooks, and team workspaces integrate insights into your existing workflow.
- Temporal decay: Weighted risk from time since last activity; configurable inactivity thresholds.
- Stage velocity: Time-in-stage vs. historical norms; bottlenecks and stalled stages flagged.
- Engagement scoring: Human touchpoints (emails, meetings, calls) tracked; drop-off triggers alerts.
- Competitive signals: High-value and negotiation-stage deals weighted for priority.
- Composite risk score: Single 0–1 score with Low / Medium / High bands and reason strings.
Natural-language queries over your deals and pipeline. Example prompts:
- "Which deals need my attention today?"
- "Tell me about the Acme Corp deal."
- "Why is my pipeline health declining?"
- "Compare my performance this month vs. last month."
The AI router maps query intent to specialized models:
| Query type | Model | Use case |
|---|---|---|
| Semantic search | OpenAI GPT-4 Turbo | Find, similar, match, embedding |
| Financial / deal | Anthropic Claude 3.5 | Pipeline, revenue, risk, forecast |
| Deal-specific | Anthropic Claude 3.5 | Single-deal detail, follow-ups |
| Code / SQL | OpenAI GPT-4o | Queries, scripts, database |
| Planning / docs | Google Gemini Pro | Strategy, roadmap, multimodal |
| General | OpenAI GPT-4 Turbo | Everything else |
- RBAC: Owner, admin, member, viewer roles; team-scoped deal access.
- Team workspaces: Create teams, invite by email, assign deals to members.
- Activity timeline: Immutable audit trail per deal; stage changes and events.
- Real-time notifications: In-app plus optional email (deal at risk, action overdue, stage change).
- Salesforce sync: Import opportunities as deals with automatic stage mapping.
- HubSpot sync: Sync deals and contacts from HubSpot CRM.
- Google Calendar: Sync meetings, auto-link to deals, schedule meetings for deals.
- Slack notifications: Real-time deal alerts in Slack channels.
- Auto-sync: Configurable periodic sync (every 6 hours) or manual trigger.
- Activity logging: Complete audit trail of all integration actions.
- Error handling: Detailed error messages and retry logic for failed syncs.
Configure endpoints to receive JSON payloads on deal and team events. Example payload:
{
"id": "evt_abc123",
"event": "deal.stage_changed",
"timestamp": "2025-01-25T12:00:00Z",
"data": {
"id": "clx123",
"name": "Acme Corp",
"oldStage": "proposal",
"newStage": "negotiation",
"value": 50000
}
}Supported events: deal.created, deal.updated, deal.stage_changed, deal.at_risk, deal.closed_won, deal.closed_lost, team.member_added, team.member_removed. Slack incoming webhooks supported for deal notifications.
Sentinel integrates with popular CRM and calendar platforms to sync deals, contacts, and meetings automatically.
Sync opportunities and contacts from Salesforce into Sentinel.
Features:
- Bidirectional sync: Import Salesforce opportunities as deals
- Stage mapping: Automatic mapping of Salesforce stages to Sentinel stages
- Auto-sync: Configurable periodic sync (every 6 hours via cron)
- Manual sync: Trigger sync on-demand from the Settings page
- Activity logging: All sync actions are logged for audit trails
Setup:
- Navigate to Settings → Integrations
- Click "Connect" on the Salesforce card
- Enter your Salesforce Instance URL (e.g.,
https://yourcompany.salesforce.com) - Enter your Salesforce API Key / Access Token
- Credentials are validated before saving
Stage Mapping:
Prospecting,Qualification,Needs Analysis→DiscoveryValue Proposition,Proposal/Price Quote→ProposalNegotiation/Review→NegotiationClosed Won→Closed WonClosed Lost→Closed Lost
API Endpoints:
POST /api/integrations/salesforce/sync- Manual sync trigger
Import deals and contacts from HubSpot CRM.
Features:
- Deal sync: Import HubSpot deals with automatic stage mapping
- Portal detection: Automatically detects and stores HubSpot Portal ID
- Pagination support: Handles large deal lists (up to 500 deals per sync)
- Auto-sync: Configurable periodic sync
- Manual sync: On-demand sync from Settings
Setup:
- Create a Private App in HubSpot (Settings → Integrations → Private Apps)
- Generate an Access Token with CRM read permissions
- In Sentinel Settings → Integrations, click "Connect" on HubSpot
- Paste your Private App Access Token
Stage Mapping:
appointmentscheduled,qualifiedtobuy→Discoverypresentationscheduled,decisionmakerboughtin→Proposalcontractsent→Negotiationclosedwon→Closed Wonclosedlost→Closed Lost
API Endpoints:
POST /api/integrations/hubspot/sync- Manual sync trigger
Sync meetings and calendar events, automatically link them to deals.
Features:
- Event sync: Import calendar events as meetings (next 30 days)
- Auto-linking: Intelligently matches meetings to deals based on:
- Meeting title containing deal name
- Attendee email domains matching deal company
- Meeting description mentioning deal name
- Meeting management: Create meetings for deals, link/unlink meetings
- Dashboard widget: View upcoming meetings on the dashboard
- Deal integration: See meetings directly on deal detail pages
Setup:
- Get a Google Calendar API key from Google Cloud Console
- Enable Calendar API for your project
- In Sentinel Settings → Integrations, click "Connect" on Google Calendar
- Enter your API Key and Calendar ID (use
primaryfor main calendar)
API Endpoints:
POST /api/integrations/google-calendar/sync- Manual sync triggerGET /api/integrations/google-calendar/events?dealId=xxx- Get meetings for a dealPOST /api/integrations/google-calendar/events- Create a new meeting
Sync Behavior:
- Deal deduplication: Deals are matched by
externalIdto prevent duplicates. If a deal with the sameexternalIdexists, it's updated; otherwise, a new deal is created. - Source tracking: Synced deals are tagged with
source: "salesforce"orsource: "hubspot"and display a badge on the deal page. - Stage mapping: CRM stages are automatically mapped to Sentinel's stage system.
- Meeting auto-linking: Google Calendar meetings are automatically linked to deals when:
- Meeting title contains the deal name
- Attendee email domains match the deal's company domain
- Meeting description mentions the deal name
- Error handling: Failed syncs are logged with detailed error messages. Check the Recent Activity section in Settings to view sync history.
Receive real-time notifications in Slack channels.
Features:
- Multiple channels: Connect multiple Slack webhooks
- Event filtering: Choose which events to receive (deal at risk, stage changes, etc.)
- Rich formatting: Formatted messages with deal details and links
- Test messages: Verify webhook configuration before saving
Setup:
- Create a Slack Incoming Webhook (Slack App → Incoming Webhooks)
- Navigate to Settings → Integrations → Slack
- Add webhook URL and configure notification preferences
Sentinel includes a cron job endpoint that automatically syncs all active integrations. You can run it on a schedule (Vercel Cron or external scheduler).
Endpoint: GET /api/cron/sync-integrations
Security: Protected by CRON_SECRET environment variable. Set in Vercel Cron or your scheduler:
CRON_SECRET=your-secret-key-hereVercel Cron (plan limits): On Hobby, Vercel allows only one run per day with hourly precision (no every-5-min or every-15-min). On Pro, you can use per-minute schedules. For more frequent runs on Hobby, use an external scheduler (e.g. cron-job.org, GitHub Actions) and call the URL with Authorization: Bearer <CRON_SECRET>.
Example – Hobby (once per day): Add to vercel.json:
{
"crons": [
{
"path": "/api/cron/sync-integrations",
"schedule": "0 0 * * *"
}
]
}Runs at midnight UTC once per day. For Pro, you can use e.g. "schedule": "0 */6 * * *" to run every 6 hours.
What it does:
- Syncs all Salesforce integrations with
syncEnabled: true - Syncs all HubSpot integrations with
syncEnabled: true - Syncs all Google Calendar integrations with
syncEnabled: true - Logs results and errors for monitoring
Manual Trigger:
curl -X GET "https://your-domain.com/api/cron/sync-integrations" \
-H "Authorization: Bearer YOUR_CRON_SECRET"-
Get your Salesforce credentials:
- Instance URL: Your Salesforce org URL (e.g.,
https://yourcompany.salesforce.com) - API Key: Create a Connected App in Salesforce and generate an access token
- Instance URL: Your Salesforce org URL (e.g.,
-
Connect in Sentinel:
- Go to Settings → Integrations
- Click "Connect" on the Salesforce card
- Enter your Instance URL and API Key
- Click "Connect" (credentials are validated automatically)
-
Sync deals:
- Click "Sync Now" to manually import opportunities
- Or enable "Auto-sync" for automatic periodic syncing
-
Create a Private App:
- Go to HubSpot Settings → Integrations → Private Apps
- Create a new app with CRM read permissions
- Copy the Access Token
-
Connect in Sentinel:
- Go to Settings → Integrations
- Click "Connect" on the HubSpot card
- Paste your Access Token
- Click "Connect"
-
Sync deals:
- Click "Sync Now" or enable auto-sync
-
Get API credentials:
- Go to Google Cloud Console
- Enable Calendar API
- Create an API Key (or use OAuth for write access)
-
Connect in Sentinel:
- Go to Settings → Integrations
- Click "Connect" on the Google Calendar card
- Enter your API Key and Calendar ID (
primaryfor main calendar) - Click "Connect"
-
View meetings:
- Meetings automatically sync and appear on deal pages
- View upcoming meetings on the dashboard
- Create meetings for deals directly from the deal page
All integrations show:
- Connection status (Connected/Not Connected)
- Last sync time and status
- Total items synced
- Recent activity log
+------------------------------------------------------------------+
| PRESENTATION |
| Next.js 16 · React 19 · Tailwind CSS · Server / Client Components|
+------------------------------------------------------------------+
|
+------------------------------------------------------------------+
| APPLICATION |
| Server Actions · API Routes · Middleware · Clerk Auth |
+------------------------------------------------------------------+
|
+------------------------------------------------------------------+
| SERVICES |
| +------------------+ +------------------+ +------------------+ |
| | AI (OpenRouter) | | Data Layer | | External | |
| | Claude, GPT-4o, | | PostgreSQL | | Redis (Upstash) | |
| | Gemini | | Prisma ORM | | Resend · Slack | |
| +------------------+ +------------------+ +------------------+ |
+------------------------------------------------------------------+
Core models (Prisma):
model Deal {
id String @id @default(cuid())
userId String
teamId String?
assignedToId String?
name String
stage String
value Int
location String?
isDemo Boolean @default(false)
source String? // "salesforce", "hubspot", or null
externalId String? // External CRM deal ID
createdAt DateTime @default(now())
user User @relation("CreatedDeals", ...)
team Team? @relation(...)
assignedTo User? @relation("AssignedDeals", ...)
events DealEvent[]
timeline DealTimeline[]
notifications Notification[]
meetings Meeting[]
@@index([userId, createdAt])
@@index([userId, stage])
@@index([teamId])
@@index([assignedToId])
@@index([externalId])
}model SalesforceIntegration {
id String @id @default(cuid())
userId String @unique
apiKey String
instanceUrl String
isActive Boolean @default(true)
syncEnabled Boolean @default(true)
lastSyncAt DateTime?
lastSyncStatus String?
syncErrors String?
totalSynced Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(...)
}
model HubSpotIntegration {
id String @id @default(cuid())
userId String @unique
apiKey String
portalId String?
isActive Boolean @default(true)
syncEnabled Boolean @default(true)
lastSyncAt DateTime?
lastSyncStatus String?
syncErrors String?
totalSynced Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(...)
}
model GoogleCalendarIntegration {
id String @id @default(cuid())
userId String @unique
apiKey String
calendarId String
refreshToken String?
isActive Boolean @default(true)
syncEnabled Boolean @default(true)
lastSyncAt DateTime?
lastSyncStatus String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(...)
}
model Meeting {
id String @id @default(cuid())
userId String
dealId String?
externalId String?
title String
description String?
startTime DateTime
endTime DateTime
attendees String[]
location String?
meetingLink String?
source String // "google_calendar", "manual"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(...)
deal Deal? @relation(...)
@@index([userId])
@@index([dealId])
@@index([startTime])
@@index([externalId])
}
model IntegrationLog {
id String @id @default(cuid())
userId String
integration String // "salesforce", "hubspot", "google_calendar"
action String // "connect", "disconnect", "sync", etc.
status String // "success", "failed"
message String?
metadata Json?
createdAt DateTime @default(now())
@@index([userId, integration])
@@index([createdAt])
}| Component | Choice | Rationale |
|---|---|---|
| Framework | Next.js 16 | App Router, RSC, Server Actions, Vercel-ready |
| Language | TypeScript 5 | Type safety, Prisma alignment, editor support |
| Database | PostgreSQL | ACID, JSON, scaling; Supabase/Railway-friendly |
| ORM | Prisma | Type-safe queries, migrations, generated client |
| Authentication | Clerk | MFA, sessions, OAuth; minimal backend code |
| AI | OpenRouter | Multi-model routing; Claude, GPT, Gemini via one API |
| Queue | Upstash Redis | Optional email/webhook queue; rate limit state; serverless-friendly |
| Resend | Transactional email; simple API, deliverability | |
| Monitoring | Sentry | Error tracking, performance; optional, fail-safe |
| Rate limiting | Redis + in-app | Per-user/IP tiers; graceful degradation when Redis null |
| Testing | Vitest, Playwright | Unit + E2E; fast feedback, CI integration |
The system is built around a few core choices.
Risk scoring over raw metrics. Pipeline data alone is noisy; stage and value tell you what exists, not what is at risk. We compute a composite risk score from temporal decay, stage velocity, and engagement so the UI can prioritize. The model is deterministic and auditable — no black box. Recommendations (e.g. “schedule a follow-up”) are derived from the same signals so reasoning is traceable.
PostgreSQL + Prisma, no separate vector store. Deals, teams, and notifications live in one database. The AI assistant uses the same relational data via structured queries and controlled context, not a separate embedding index. That keeps the stack simple, avoids sync and consistency issues, and fits Vercel/serverless. If we later need semantic search over large corpora, we’d add it explicitly rather than preemptively.
OpenRouter for multi-model routing. Different tasks map to different models: summarization and strategy to Gemini, deal and revenue reasoning to Claude, code/SQL to GPT-4o. A single API and key simplifies operations and lets us swap models per route without changing client code. Fallback and rate handling live in one layer.
Real-time via SSE, not WebSockets. Notifications and deal updates are pushed over a single GET stream (/api/events) with Redis-backed delivery. SSE works with serverless, keeps auth as standard HTTP cookies, and avoids a long-lived WebSocket server. We only need server→client push; bidirectional chat is request/response.
Server Actions + API routes. Mutations and auth-heavy flows use Server Actions; external callbacks (webhooks, cron, third-party) use API routes. Clear boundary: internal UI and server logic in actions, external contracts in API. Middleware handles auth so both paths share the same guarantees.
Clerk for auth. We don’t store passwords or build session logic. Clerk gives us MFA, OAuth, and session handling so we can focus on domain logic and RBAC (team roles, deal access).
Redis optional. Queues (email, real-time events), rate limiting, and caching use Redis when available. Without it, the app still runs: rate limits bypass gracefully, notifications work via API, real-time degrades to polling. Optional infra keeps the default path simple for small deployments.
Rate limiting and circuit breakers. API routes use Redis-backed rate limits with per-user and per-IP tiers. External calls (CRM syncs, AI) use retry with backoff and circuit breakers so a failing provider doesn’t cascade. When Redis is null, we skip rate limiting rather than block requests.
Developer cognition. The codebase is split by domain (deals, integrations, notifications, real-time) rather than by layer. You open actions/deals.ts or lib/realtime.ts and see one slice of the system. That reduces context-switching and makes ownership obvious.
Risk score accuracy vs latency. The risk engine runs on every deal list and detail load. We use a single pass over timeline events and config (inactivity threshold, competitive signals) so responses stay fast. More sophisticated models (e.g. per-deal ML) would require async jobs and caching; we traded that for predictable latency and simplicity.
AI context vs cost and limits. The chat endpoint builds context from the current user’s deals and metadata. We cap context size and summarize where needed so we stay within model limits and cost. More context improves answers but increases tokens and latency; we tune the balance per use case.
Chunk size and retrieval. When we send deal data to the model, we structure it (e.g. key fields, stage, value, last activity) rather than dumping raw records. That keeps prompts small and reduces hallucination risk. Tradeoff: very detailed, narrative-style answers would need more context and cost.
Sync frequency vs API limits. CRM and calendar syncs run on a schedule (e.g. cron every 6 hours) plus manual trigger. Higher frequency would improve freshness but hit provider rate limits and increase failure surface. We log sync state and errors so operators can adjust or retry.
Real-time vs serverless. The SSE route polls Redis on an interval and sends heartbeats so the connection stays active within typical function timeouts. True long-lived connections would need a dedicated runtime or edge; for our scale, polling + heartbeat is a deliberate tradeoff for deployment simplicity.
Database performance. Deal and notification queries are scoped by userId or team; we rely on indexes on userId, teamId, dealId, and createdAt. Heavy analytics (e.g. cross-tenant reporting) would need separate read paths or read replicas; the current design optimizes for single-tenant, per-user views.
API rate limiting. We enforce Redis-backed rate limits on chat, search, export, and analytics. When Redis is unavailable, limits bypass so the app stays usable. Chat and sync get stricter tiers; analytics gets per-IP limits. Edge or CDN rate limiting remains recommended for abuse protection. We don’t enforce rate limits in-app by default; we assume Vercel or a reverse proxy handles abuse. For stricter control, we’d add a rate-limit layer in front of chat and sync endpoints.
Model unpredictability. LLM outputs vary. We treat the AI assistant as an aid, not the source of truth; critical actions (e.g. deal stage changes) stay in the main app with explicit user steps. We avoid letting the model drive irreversible state changes without confirmation.
Quick start:
git clone https://github.com/parbhatkapila4/Sentinel.git
cd Sentinel
npm install
cp .env.example .env.localEdit .env.local with required variables (see Required Environment Variables below). Then:
npx prisma generate
npx prisma migrate dev
npm run devApp runs at http://localhost:3000.
For full setup (env vars, Docker, Vercel deployment), see DEPLOYMENT.md.
DATABASE_URL=postgresql://user:password@host:5432/db?schema=public
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
OPENROUTER_API_KEY=sk-or-...# Queue & Caching
UPSTASH_REDIS_REST_URL=https://...
UPSTASH_REDIS_REST_TOKEN=...
# Email
RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=notifications@yourdomain.com
# App Configuration
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Cron Jobs (required for /api/cron/*)
CRON_SECRET=your-secret-key-here
# Analytics (privacy-compliant; fail-safe)
NEXT_PUBLIC_ANALYTICS_ENABLED=true
ANALYTICS_API_KEY=optional-key-for-metrics-summary
# Sentry
NEXT_PUBLIC_SENTRY_DSN=https://...@sentry.io/...
SENTRY_PERFORMANCE_SAMPLE_RATE=0.1
# Rate Limiting (Redis required; defaults apply if unset)
RATE_LIMIT_STRICT=30
RATE_LIMIT_LENIENT=200Note: Integration API keys (Salesforce, HubSpot, Google Calendar) are stored per-user in the database and configured through the Settings UI, not as environment variables.
Browser requests use Clerk session cookies automatically. For API clients, send a Bearer token in the Authorization header when the route supports it. Some routes (e.g. cron) expect Authorization: Bearer CRON_SECRET.
curl -X GET "https://your-domain.com/api/deals/search?q=Acme" \
-H "Authorization: Bearer YOUR_CLERK_TOKEN" \
-H "Content-Type: application/json"| Method | Endpoint | Description |
|---|---|---|
| GET | /api/deals |
List deals (optional stage, limit) |
| POST | /api/deals |
Create deal |
| GET | /api/deals/:id |
Get deal by ID |
| PATCH | /api/deals/:id |
Update deal |
| GET | /api/deals/search |
Search deals (query params) |
| POST | /api/deals/export |
Export deals as CSV/JSON |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/insights/chat |
AI chat (body: { messages }) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/notifications |
List notifications |
| POST | /api/notifications/read-all |
Mark all as read |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/auth/me |
Current user |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/alerts |
Deal alerts / at-risk summary |
| POST | /api/analytics/track |
Client analytics events (rate-limited; no PII) |
| GET | /api/metrics/performance |
Performance metrics |
| GET | /api/metrics/summary |
Aggregate business metrics (x-api-key or auth) |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/integrations/salesforce/sync |
Manually sync Salesforce deals |
| POST | /api/integrations/hubspot/sync |
Manually sync HubSpot deals |
| POST | /api/integrations/google-calendar/sync |
Manually sync Google Calendar events |
| GET | /api/integrations/google-calendar/events |
Get upcoming meetings (optional ?dealId=xxx) |
| POST | /api/integrations/google-calendar/events |
Create a new meeting |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/cron/sync-integrations |
Auto-sync all integrations |
| GET | /api/cron/process-webhooks |
Process webhook delivery queue |
| GET | /api/cron/process-emails |
Process email queue |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/deals/bulk |
Bulk deal operations |
| DELETE | /api/user/delete |
Delete current user |
curl -X POST "https://your-domain.com/api/integrations/salesforce/sync" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json"Response:
{
"success": true,
"synced": 15,
"created": 8,
"updated": 7
}curl -X GET "https://your-domain.com/api/integrations/google-calendar/events?dealId=clx123" \
-H "Authorization: Bearer YOUR_TOKEN"Response:
{
"success": true,
"meetings": [
{
"id": "meet_abc",
"title": "Q4 Review with Acme Corp",
"startTime": "2025-01-30T14:00:00Z",
"endTime": "2025-01-30T15:00:00Z",
"attendees": ["john@acme.com", "jane@acme.com"],
"location": "Conference Room A",
"meetingLink": "https://meet.google.com/xxx"
}
]
}Full OpenAPI spec and interactive docs: /api-docs.
Verify X-Webhook-Signature with HMAC-SHA256:
const crypto = require("crypto");
const sig = req.headers["x-webhook-signature"];
const expected = crypto
.createHmac("sha256", webhookSecret)
.update(JSON.stringify(payload))
.digest("hex");
if (sig === expected) {
// Payload is authentic
}src/
├── app/ # Next.js App Router
│ ├── actions/ # Server Actions
│ │ ├── deals.ts # Deal CRUD operations
│ │ ├── integrations.ts # Unified integration status & logging
│ │ ├── salesforce.ts # Salesforce connect, sync, settings
│ │ ├── hubspot.ts # HubSpot connect, sync, settings
│ │ ├── google-calendar.ts # Calendar sync, meetings management
│ │ ├── slack.ts # Slack webhook integration
│ │ ├── teams.ts # Team management
│ │ └── webhooks.ts # Webhook management
│ ├── api/ # REST routes
│ │ ├── deals/ # Deal endpoints
│ │ ├── integrations/ # Integration sync endpoints
│ │ │ ├── salesforce/sync/
│ │ │ ├── hubspot/sync/
│ │ │ └── google-calendar/
│ │ │ ├── sync/
│ │ │ └── events/
│ │ ├── cron/ # Scheduled jobs
│ │ │ └── sync-integrations/ # Auto-sync cron
│ │ ├── insights/ # AI chat endpoint
│ │ └── notifications/ # Notification endpoints
│ ├── dashboard/ # Dashboard page
│ ├── deals/ # Deal list and detail pages
│ ├── settings/ # Settings pages (integrations, team, etc.)
│ └── ... # Auth, pricing, docs, static pages
├── components/ # React UI components
│ ├── deal-meetings.tsx # Meeting management for deals
│ ├── upcoming-meetings-widget.tsx # Dashboard meetings widget
│ ├── dashboard-layout.tsx
│ ├── insights-panel.tsx
│ └── ui/ # Shared primitives (sidebar, skeleton, etc.)
├── lib/ # Core logic
│ ├── auth.ts # Authentication helpers
│ ├── prisma.ts # DB client singleton
│ ├── redis.ts # Upstash Redis (optional)
│ ├── dealRisk.ts # Risk calculation
│ ├── ai-router.ts # AI model routing
│ ├── ai-context.ts # AI context building
│ ├── rate-limit.ts # Redis-backed rate limiter
│ ├── api-rate-limit.ts # API route rate-limit wrapper
│ ├── cache.ts # Caching (withCache, invalidate)
│ ├── retry.ts # Retry with backoff
│ ├── circuit-breaker.ts # Circuit breaker for external calls
│ ├── realtime.ts # SSE event publish
│ ├── audit-log.ts # Audit logging
│ ├── salesforce.ts # Salesforce API client
│ ├── hubspot.ts # HubSpot API client
│ ├── google-calendar.ts # Google Calendar API client
│ ├── slack.ts # Slack webhook utilities
│ ├── analytics-client.ts# Client-side analytics
│ ├── business-metrics.ts# Server-side metrics (Redis)
│ └── utils.ts # Utility functions
├── hooks/ # React hooks
│ ├── use-realtime.ts # SSE client for real-time updates
│ └── use-keyboard-shortcuts.ts
├── types/ # Shared TypeScript types
├── test/ # Mocks and Vitest setup
└── middleware.ts # Auth boundary
| Command | Description |
|---|---|
npm run test |
Vitest watch mode |
npm run test:run |
Unit tests (CI) |
npm run test:coverage |
Coverage report |
npm run test:e2e |
Playwright E2E |
Unit tests live in src/app/actions/__tests__, src/app/api/__tests__, and src/lib/__tests__. E2E specs are in e2e/ (home, dashboard, deals). Use src/test/mocks for auth and Prisma in tests. Run npx playwright install before E2E if browsers are not installed.
vercelSet env vars in the Vercel dashboard. Use Vercel Postgres or an external PostgreSQL URL.
Example Dockerfile:
# Stage 1: deps
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Stage 2: builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# Stage 3: runner
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]Ensure output: "standalone" is set in next.config. Run Prisma migrations before starting (e.g. init container or CI step).
- Clerk auth: Sessions, MFA support; no password storage in-app.
- RBAC: Team roles (owner, admin, member, viewer); scope enforced in Server Actions and API.
- Row-level security: All deal/list queries filtered by
userIdor team membership. - Webhooks: HMAC-SHA256 signatures; verify
X-Webhook-Signaturebefore processing. - Headers: HSTS, X-Frame-Options, X-Content-Type-Options, CSP, Referrer-Policy via
next.config. - Input validation: Zod in
src/lib/env; validate request bodies in API routes. - Rate limiting: Redis-backed (
src/lib/api-rate-limit.ts); per-user/IP tiers for chat, export, search. Graceful degradation when Redis unavailable. - Request size: Middleware caps body size (10MB) for POST/PUT/PATCH.
| Metric | Target |
|---|---|
| Lighthouse | 98 |
| FCP | 0.8 s |
| TTI | 1.2 s |
| LCP | < 1.5 s |
| CLS | < 0.1 |
- React Server Components and Server Actions to reduce client JS.
- Dynamic imports for heavy UI (e.g. Swagger, charts).
- Prisma query tuning; indexed access on
userId,teamId,dealId. - Optional Redis for queue and caching.
- Image optimization via
next/imagewhere used.
Early sync failures. Initial CRM syncs failed silently when provider APIs returned non-standard errors or rate limits. We added structured logging, per-integration status (lastSyncAt, lastSyncStatus, syncErrors), and retry with backoff. Failures are now visible in Settings and logs; operators can trigger manual sync or fix credentials.
AI hallucinations on deal detail. The assistant sometimes invented deal attributes or stages that didn’t exist when context was too small or stale. We tightened context construction: we only include deals and fields the user can access, and we pass explicit schema (stage list, field names) so the model stays grounded. We also avoid letting the model perform mutations; it suggests, the user acts.
Retrieval mistakes. When chat pulled “similar” deals by keyword alone, it sometimes surfaced the wrong deal or missed the one the user meant. We improved by always scoping to the current user (and team when relevant) and by including deal IDs in context so the model can refer to specific records. We still don’t do semantic search over a large corpus; we keep retrieval deterministic and narrow.
Real-time and serverless. The first real-time design assumed a long-lived connection; serverless timeouts and cold starts made it unreliable. We switched to SSE with Redis-backed events and a polling loop inside the stream, plus heartbeats. Connections stay within function limits; when Redis is missing, we fall back to polling so the app still works.
Scaling concerns. Deal list and risk computation run on every request for that view. For users with thousands of deals, we’d need pagination, caching, or background risk precomputation. We’ve kept the current design for “normal” pipeline sizes and documented that heavy usage would require read replicas or async jobs.
What required redesign. Sync status and errors were initially buried in logs. We introduced IntegrationLog and per-integration status fields so the UI can show last sync time, status, and recent errors. That single change made operations and support much easier.
What improved performance the most. Indexing userId, teamId, dealId, and createdAt on the main query paths; keeping AI context small and structured; and using Server Components for initial deal list so the client doesn’t refetch everything on load.
Rate limiting and optional Redis. Early on, missing Redis caused runtime errors in rate-limit and cache code. We refactored to null-check Redis everywhere and bypass gracefully. That let us deploy without Redis for small setups while still using it when available for production hardening.
Async ingestion. CRM and calendar syncs are today triggered by cron or manual action. At scale, we’d move to a queue: cron or webhook enqueues a sync job; a worker (or serverless function) runs the sync and updates status. That isolates long-running syncs from request timeouts and allows retries and backpressure.
Queue-based indexing and notifications. Real-time events are pushed via Redis list and consumed by the SSE route. At higher volume, we’d consider a proper queue (e.g. Upstash Kafka, SQS) with per-user or per-team channels, dead-letter handling, and at-least-once delivery for critical notifications.
Caching layers. Deal lists and risk summaries could be cached per user with short TTLs (e.g. 60s) so repeated navigations don’t hit the DB every time. Cache keys would include userId (and teamId when relevant). Invalidation would happen on deal/team mutations and optionally on real-time events.
Shardable storage. PostgreSQL and Prisma scale with connection pooling and read replicas. If deal volume grew into the millions, we’d consider sharding by userId or tenant and routing reads/writes accordingly. Vector search, if added later, would be a separate store with its own scaling story.
Background workers. Risk precomputation, heavy analytics, and bulk exports would move to background jobs so the request path stays fast. Same for summarization or embedding if we ever add semantic search.
Observability and metrics. We’d add metrics for: sync duration and success rate per integration, AI request latency and token usage, SSE connection count and event delivery latency, and error rates per route. Logs are structured; we’d ship them to a central store and set alerts on sync failures and API errors.
Cost control. AI cost is dominated by token usage. We’d cap context size per request, set per-user or per-org limits, and optionally cache frequent queries. Sync and real-time costs are bounded by cron frequency and connection count; we’d monitor and tune those knobs.
Fault tolerance. Integrations fail; we already log and surface status. At scale we’d add circuit breakers per provider, automatic backoff on repeated failures, and clear degradation (e.g. “Salesforce sync paused due to errors”). Real-time degrades to polling when Redis or the stream fails; we’d extend that pattern to other optional services.
Onboarding. New engineers see a single repo with clear domains: actions/deals.ts, lib/dealRisk.ts, app/api/events/route.ts. Auth, DB, and external services are centralized (Clerk, Prisma, OpenRouter, Redis). A new dev can run the app locally, read one slice (e.g. deals or integrations), and make a small change without touching the whole stack.
Reduced tribal knowledge. Risk logic, AI routing, and sync behavior live in code and README, not only in someone’s head. Technology Decisions and Design Philosophy sections explain why we chose PostgreSQL, OpenRouter, SSE, and optional Redis. New joiners can reason about tradeoffs without hunting for design docs.
Code reviews. Server Actions and API routes have a clear contract: actions for app-driven mutations, API for webhooks and cron. Reviewers can check auth, scoping (userId/teamId), and error handling in one place. Integration code is isolated (e.g. salesforce.ts, hubspot.ts), so changes to one provider don’t ripple everywhere.
Documentation automation. API routes are documented in the README and exposed via OpenAPI at /api-docs. Webhook payloads and env vars are specified in one file. That reduces “how do I call X?” questions and keeps docs close to the code. Real-time updates are delivered via SSE at /api/events and the useRealtime hook; deal and team actions are recorded in an immutable audit trail.
Architectural clarity. The stack diagram and project structure show where presentation, application, and services sit. Real-time, notifications, and deal risk are additive modules with clear entry points. That makes it easier to extend (e.g. a new integration or a new event type) without reworking the core.
- ARCHITECTURE.md — System overview, tech stack, directory structure, data flow, caching, security
- DEPLOYMENT.md — Prerequisites, local setup, Vercel deployment, env vars
- CONTRIBUTING.md — How to run locally, branch naming, PR checks, code style
- docs/API.md — API index and links to full OpenAPI spec
- CHANGELOG.md — Version history and notable changes
In-app: API Reference (OpenAPI/Swagger), Developer Docs.
We welcome contributions. Please read our contributing guidelines before submitting a pull request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/new-feature) - Commit your changes (
git commit -m 'Add new feature') - Push to the branch (
git push origin feature/new-feature) - Open a Pull Request
- Documentation: docs
- API Reference: api-docs
- Issues: GitHub Issues
- Next.js — The React framework for production
- Prisma — Next-generation ORM for Node.js
- Clerk — Authentication and user management
- OpenRouter — Unified API for AI models
- Tailwind CSS — Utility-first CSS framework
- Vercel — Deployment and hosting
This project is licensed under the MIT License. See the LICENSE file for details.