https://github.com/5queezer/nexus-crm
Lead & opportunity management suite — Next.js, TanStack, Prisma, Google OAuth
https://github.com/5queezer/nexus-crm
Last synced: 2 months ago
JSON representation
Lead & opportunity management suite — Next.js, TanStack, Prisma, Google OAuth
- Host: GitHub
- URL: https://github.com/5queezer/nexus-crm
- Owner: 5queezer
- Created: 2026-03-06T13:42:30.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-04-01T11:17:18.000Z (2 months ago)
- Last Synced: 2026-04-04T10:39:58.671Z (2 months ago)
- Language: TypeScript
- Size: 1.75 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Security: SECURITY_AUDIT.md
Awesome Lists containing this project
README
# Nexus CRM
A lead and opportunity management suite for tracking your sales pipeline. Manage applications across a Kanban board or sortable table, store documents, track contacts, scan emails for leads, and integrate with AI agents via a built-in MCP server.
**Live:** [nexus.vasudev.xyz](https://nexus.vasudev.xyz)
---
## Table of Contents
- [Features](#features)
- [Tech Stack](#tech-stack)
- [Prerequisites](#prerequisites)
- [Getting Started](#getting-started)
- [Architecture](#architecture)
- [Environment Variables](#environment-variables)
- [Available Scripts](#available-scripts)
- [Testing](#testing)
- [API](#api)
- [MCP Server](#mcp-server)
- [Deployment](#deployment)
- [Pipeline Stages](#pipeline-stages)
- [Troubleshooting](#troubleshooting)
- [License](#license)
---
## Features
- **Table & Kanban Views** — sortable, filterable table powered by TanStack Table, or drag-and-drop Kanban board with optimistic updates via @dnd-kit
- **Follow-up Reminders** — set per-opportunity follow-up dates with overdue alerts
- **Contact Management** — track contacts per opportunity (name, role, email, phone, LinkedIn)
- **Document Storage** — upload PDFs and images, link to opportunities, shareable download links
- **Client Portal** — read-only public share page with short-code URLs
- **Analytics Dashboard** — interactive pipeline statistics and insights
- **Resume Review** — AI-powered resume analysis and job matching
- **Resume Tailoring** — duplicate and tailor resumes per application via Reactive Resume integration
- **Dark / Light / System Theme** — three-way toggle, persisted in localStorage, flash-free
- **DE / EN Language Switcher** — full i18n via next-intl (default: German)
- **CSV Export** — one-click export, Excel-compatible
- **Bulk Archive** — archive old or low-rated applications by age or star rating
- **Google OAuth** — secure login, multi-user with per-user admin roles
- **Email Intelligence** — Gmail integration to auto-detect and import client communications
- **MCP Server** — Model Context Protocol endpoint for AI agent integration (OAuth 2.1 + PKCE)
- **API Docs** — OpenAPI 3.1 spec with Swagger UI at `/api-docs`, LLM-friendly guide at `/llm.txt`
- **Rate Limiting** — per-IP rate limits on all API routes with standard headers
- **Security Hardened** — CSP, HSTS, X-Frame-Options, gitleaks pre-commit hooks, encrypted email tokens
---
## Tech Stack
| Layer | Technology |
| ----- | ---------- |
| **Framework** | [Next.js 16](https://nextjs.org) — App Router, React 19, standalone output |
| **Database** | [Prisma 6](https://prisma.io) + PostgreSQL (swappable adapter for Firestore) |
| **Auth** | [better-auth](https://better-auth.com) — Google OAuth, session cookies |
| **Data Fetching** | [TanStack Query v5](https://tanstack.com/query) — caching, optimistic UI |
| **Table** | [TanStack Table v8](https://tanstack.com/table) — headless sort & filter |
| **Drag & Drop** | [@dnd-kit](https://dndkit.com) — Kanban board |
| **i18n** | [next-intl](https://next-intl-docs.vercel.app) — DE / EN |
| **Styling** | [Tailwind CSS v3](https://tailwindcss.com) — class-based dark mode |
| **Validation** | [Zod 4](https://zod.dev) — runtime schema validation |
| **AI Integration** | [@modelcontextprotocol/sdk](https://modelcontextprotocol.io) — MCP server |
| **File Storage** | Google Cloud Storage or local filesystem |
| **Hosting** | [Google Cloud Run](https://cloud.google.com/run) — container hosting |
| **Database Hosting** | [Neon](https://neon.tech) — serverless PostgreSQL |
| **Testing** | [Vitest 4](https://vitest.dev) |
| **CI/CD** | GitHub Actions — lint, build, test, deploy to Cloud Run |
---
## Prerequisites
- **Node.js 22+** (matches the Docker image; 20+ works locally)
- **PostgreSQL 15+** — or a hosted provider like [Neon](https://neon.tech) (free tier available)
- **Google OAuth Client ID** — [create one here](https://console.cloud.google.com/apis/credentials)
- **npm** — used as the package manager (see `.npmrc`)
Optional:
- **Docker** — for containerized deployment
- **pre-commit** — for git hooks (`pip install pre-commit && pre-commit install`)
- **gitleaks** — for secrets scanning (used by pre-commit hook)
---
## Getting Started
### 1. Clone the Repository
```bash
git clone https://github.com/5queezer/job-tracker.git
cd job-tracker
```
### 2. Install Dependencies
```bash
npm install
```
### 3. Environment Setup
```bash
cp .env.example .env
```
Edit `.env` with your values. See [Environment Variables](#environment-variables) for the full reference.
At minimum, you need:
- `DATABASE_URL` — a PostgreSQL connection string
- `BETTER_AUTH_SECRET` — generate with `openssl rand -base64 32`
- `BETTER_AUTH_URL` — your app's public URL (use `http://localhost:3001` for local dev)
- `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` — from Google Cloud Console
- `ALLOWED_EMAIL` — comma-separated list of emails permitted to log in
### 4. Database Setup
Push the Prisma schema to your database:
```bash
npx prisma db push
```
This creates all tables. For an existing database with prior migrations:
```bash
npx prisma migrate deploy
```
Generate the Prisma client (usually automatic after `npm install`):
```bash
npx prisma generate
```
### 5. Start Development Server
```bash
npm run dev
```
Opens at [http://localhost:3001](http://localhost:3001).
In development mode, if no Google OAuth session exists, a fake admin user (`dev@localhost`) is automatically used — no OAuth setup required to start coding.
---
## Architecture
### Directory Structure
```text
├── app/ # Next.js App Router
│ ├── page.tsx # Main dashboard (server component)
│ ├── layout.tsx # Root layout with i18n + theme
│ ├── providers.tsx # TanStack Query provider
│ ├── analytics/ # Analytics dashboard page
│ ├── api/ # API routes
│ │ ├── applications/ # CRUD + contacts + documents + tailor
│ │ ├── auth/[...all]/ # better-auth catch-all handler
│ │ ├── documents/ # Document upload, download, metadata
│ │ ├── email/ # Gmail OAuth + scanning
│ │ ├── mcp/ # MCP server + OAuth 2.1 endpoints
│ │ ├── admin/ # User management + audit logs
│ │ ├── share-links/ # Short-code share link management
│ │ └── token/ # API token generation
│ ├── api-docs/ # Swagger UI page
│ ├── documents/ # Document management page
│ ├── resume-review/ # AI resume review page
│ ├── s/[code]/ # Short-code share link resolver
│ ├── settings/ # User settings page
│ └── share/ # Public read-only portal
├── components/ # React client components
│ ├── dashboard.tsx # Main dashboard with table/kanban toggle
│ ├── application-table.tsx # TanStack Table view
│ ├── kanban-view.tsx # Drag-and-drop Kanban board
│ ├── application-modal.tsx # Create/edit application modal
│ ├── analytics-dashboard.tsx # Charts and pipeline stats
│ ├── documents-client.tsx # Document manager
│ ├── email-integration.tsx # Gmail connection UI
│ ├── scanned-emails.tsx # Email scan results
│ ├── resume-analyzer.tsx # Resume review component
│ ├── app-header.tsx # Shared navigation header
│ ├── settings-client.tsx # Settings page (admin, tokens, email)
│ └── ... # Theme, language, audit, API token components
├── lib/ # Server-side utilities
│ ├── auth.ts # better-auth configuration
│ ├── session.ts # Auth middleware (session + Bearer + dev bypass)
│ ├── db/ # Database adapter layer
│ │ ├── adapter.ts # DatabaseAdapter interface
│ │ ├── index.ts # Factory: getDb() → Prisma or Firestore
│ │ ├── prisma-adapter.ts # PostgreSQL implementation
│ │ ├── firestore-adapter.ts # Firestore implementation
│ │ └── types.ts # Shared record types
│ ├── storage.ts # File storage (GCS or local filesystem)
│ ├── mcp-oauth.ts # MCP OAuth 2.1 server implementation
│ ├── email/ # Email scanning pipeline
│ │ ├── gmail.ts # Gmail API client
│ │ ├── scanner.ts # Email scanning orchestrator
│ │ ├── classifier.ts # Email classification logic
│ │ └── encryption.ts # AES-256-GCM token encryption
│ ├── rate-limit.ts # In-memory rate limiter (LRU)
│ ├── token.ts # API token hashing utilities
│ ├── reactive-resume.ts # Reactive Resume API client
│ ├── resume-analysis.ts # AI resume analysis
│ └── logger.ts # Logging utility
├── prisma/
│ ├── schema.prisma # Database schema
│ ├── seed.ts # Database seeder
│ └── migrations/ # SQL migration history
├── messages/
│ ├── de.json # German translations
│ └── en.json # English translations
├── i18n/
│ └── request.ts # Locale detection (cookie-based, default: de)
├── types/
│ └── index.ts # Shared TypeScript types + status validation
├── public/
│ ├── openapi.json # OpenAPI 3.1 spec
│ └── llm.txt # LLM-friendly API guide
├── middleware.ts # Rate limiting middleware
├── Dockerfile # Multi-stage production build
├── docker-compose.yml # Self-hosted deployment
└── .github/workflows/
├── ci.yml # Lint + build + test
└── deploy-gcp.yml # Cloud Run CI/CD
```
### Database Adapter Layer
The data layer uses a factory pattern (`lib/db/index.ts`). The `DB_PROVIDER` env var selects the backend at runtime:
- **`prisma`** (default) — Prisma ORM with PostgreSQL
- **`firestore`** — Firebase Admin SDK with Firestore
All API routes call `getDb()` which returns a `DatabaseAdapter` interface. Swapping backends requires zero code changes.
### Storage Abstraction
File storage (`lib/storage.ts`) switches between:
- **Google Cloud Storage** — when `GCS_BUCKET` is set
- **Local filesystem** — default, stores in `uploads/`
### Authentication Flow
1. **Google OAuth** via better-auth → session cookie
2. **API tokens** — per-user Bearer tokens (`jt_` prefix), generated in the dashboard
3. **MCP OAuth 2.1** — full authorization code flow with PKCE for AI agent integration (`mcp_at_` prefix)
4. **Dev bypass** — in development, a fake admin user is used when no session exists
Admins bypass per-user data scoping. The first user matching `ALLOWED_EMAIL` is auto-promoted to admin.
### Request Lifecycle
```text
Request → middleware.ts (rate limiting) → API route handler
→ requireAuth() (session/Bearer/dev)
→ getDb() (database adapter)
→ Response with rate-limit headers
```
### Database Schema
```text
User ─────────────┬── Application ──┬── Contact
│ │ └── Document (M:N)
├── Session ├── Document
├── Account └── (via userId)
├── UserApiToken
├── ShareLink
├── EmailIntegration
├── ScannedEmail
├── McpAccessToken
└── AdminAuditLog (actor + target)
McpOAuthClient
McpAuthCode
McpRefreshToken
Verification
```
Key relationships:
- Applications belong to a User and have many Contacts
- Documents belong to a User and link to many Applications (M:N)
- Each User has at most one EmailIntegration (encrypted Gmail refresh token)
- MCP OAuth tables handle the full authorization code + refresh token flow
---
## Environment Variables
### Required
| Variable | Description | Example |
| -------- | ----------- | ------- |
| `DATABASE_URL` | PostgreSQL connection string | `postgresql://user:pass@host:5432/nexus` |
| `BETTER_AUTH_SECRET` | Session encryption secret | `openssl rand -base64 32` |
| `BETTER_AUTH_URL` | Public URL of the app | `https://nexus.vasudev.xyz` |
| `GOOGLE_CLIENT_ID` | Google OAuth client ID | `xxx.apps.googleusercontent.com` |
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | `GOCSPX-xxx` |
| `ALLOWED_EMAIL` | Comma-separated allowed emails | `user@example.com,other@example.com` |
### Optional
| Variable | Description | Default |
| -------- | ----------- | ------- |
| `DB_PROVIDER` | Database backend (`prisma` or `firestore`) | `prisma` |
| `GCS_BUCKET` | Google Cloud Storage bucket name (omit for local filesystem) | — |
| `UPLOAD_DIR` | Local upload directory path | `./uploads` |
| `PUBLIC_READ_TOKEN` | Token for the read-only client portal | — |
| `RR_API_URL` | Reactive Resume API URL | — |
| `RR_API_KEY` | Reactive Resume API key | — |
| `RR_BASE_RESUME_ID` | Base resume to duplicate for tailoring | — |
### Development
```env
DB_PROVIDER="prisma"
DATABASE_URL="postgresql://localhost:5432/nexus_dev"
BETTER_AUTH_SECRET="dev-secret-change-me"
BETTER_AUTH_URL="http://localhost:3001"
GOOGLE_CLIENT_ID="your-client-id"
GOOGLE_CLIENT_SECRET="your-client-secret"
ALLOWED_EMAIL="you@example.com"
```
In development mode, OAuth is bypassed — you can use the app without configuring Google credentials.
---
## Available Scripts
| Command | Description |
| ------- | ----------- |
| `npm run dev` | Start development server on port 3001 (with Turbopack) |
| `npm run build` | Production build (standalone output) |
| `npm start` | Start production server on port 3001 |
| `npm run lint` | Run ESLint |
| `npm test` | Run Vitest test suite |
| `npm run seed` | Seed the database (`tsx prisma/seed.ts`) |
| `npx prisma studio` | Open Prisma Studio (database GUI) |
| `npx prisma db push` | Push schema changes to the database |
| `npx prisma migrate dev` | Create and apply a migration |
| `npx prisma generate` | Regenerate Prisma client |
---
## Testing
### Running Tests
```bash
# Run all tests
npm test
# Run in watch mode
npx vitest
# Run a specific test file
npx vitest lib/__tests__/rate-limit.test.ts
```
### Test Structure
```text
lib/__tests__/
├── rate-limit.test.ts # Rate limiter unit tests
└── token.test.ts # Token hashing tests
lib/db/__tests__/
└── firestore-adapter.test.ts # Firestore adapter tests
lib/email/__tests__/
├── classifier.test.ts # Email classification tests
└── encryption.test.ts # AES-256-GCM encryption tests
types/__tests__/
└── index.test.ts # Type validation tests
```
Tests use Vitest with the `@` path alias configured to the project root.
---
## API
### OpenAPI Documentation
- **Swagger UI**: [/api-docs](https://nexus.vasudev.xyz/api-docs) — interactive API explorer
- **OpenAPI spec**: [/openapi.json](https://nexus.vasudev.xyz/openapi.json) — machine-readable spec
- **LLM guide**: [/llm.txt](https://nexus.vasudev.xyz/llm.txt) — plain-text API reference for AI agents
### Authentication
| Method | How | Scope |
| ------ | --- | ----- |
| Session cookie | Google OAuth login at `/login` | Full access |
| Bearer token | `Authorization: Bearer jt_` | Owner's data (admin: all data) |
| MCP OAuth | `Authorization: Bearer mcp_at_` | Owner's data via MCP |
| Share token | Query param on `/share` | Read-only portal |
Generate API tokens in the dashboard under Settings. Tokens are shown once and stored as SHA-256 hashes.
### Endpoints
#### Applications
| Method | Path | Description |
| ------ | ---- | ----------- |
| `GET` | `/api/applications` | List all (paginated, filtered by user) |
| `POST` | `/api/applications` | Create application |
| `GET` | `/api/applications/:id` | Get single application |
| `PATCH` | `/api/applications/:id` | Update application |
| `DELETE` | `/api/applications/:id` | Delete application |
#### Contacts (nested under application)
| Method | Path | Description |
| ------ | ---- | ----------- |
| `GET` | `/api/applications/:id/contacts` | List contacts |
| `POST` | `/api/applications/:id/contacts` | Add contact |
| `PATCH` | `/api/applications/:id/contacts/:cid` | Update contact |
| `DELETE` | `/api/applications/:id/contacts/:cid` | Delete contact |
#### Documents
| Method | Path | Description |
| ------ | ---- | ----------- |
| `GET` | `/api/documents` | List documents |
| `POST` | `/api/documents` | Upload (multipart/form-data) |
| `PATCH` | `/api/documents/:id` | Rename or update application links |
| `DELETE` | `/api/documents/:id` | Delete document |
| `GET` | `/api/documents/:id/file` | Download file |
#### Admin (requires `isAdmin`)
| Method | Path | Description |
| ------ | ---- | ----------- |
| `GET` | `/api/admin/users` | List all users |
| `PATCH` | `/api/admin/users/:id` | Update admin status |
| `GET` | `/api/admin/audit-logs` | View audit log |
#### Token Management
| Method | Path | Description |
| ------ | ---- | ----------- |
| `GET` | `/api/token` | Get current token metadata |
| `POST` | `/api/token` | Generate new token |
| `DELETE` | `/api/token` | Revoke token |
### Rate Limits
Rate limits are enforced per IP via middleware:
| Route Group | Limit (req/min) |
| ----------- | --------------- |
| `/api/auth` | 10 |
| `/api/admin` | 20 |
| `/api/email` | 20 |
| `/api/applications` | 60 |
| All other `/api/*` | 30 |
Standard `X-RateLimit-*` and `Retry-After` headers are included on every response.
---
## MCP Server
Nexus CRM exposes a [Model Context Protocol](https://modelcontextprotocol.io) server at `/api/mcp` for AI agent integration.
### Available Tools
| Tool | Description |
| ---- | ----------- |
| `list_applications` | List all applications |
| `get_application` | Get single application by ID |
| `create_application` | Create a new application |
| `update_application` | Update an existing application |
| `delete_application` | Delete an application |
| `batch_upsert_applications` | Create/update up to 50 applications |
| `batch_delete_applications` | Delete up to 50 applications |
| `list_applications_filtered` | List with filters, sorting, field selection |
| `create_contact` | Add contact to application |
| `update_contact` | Update a contact |
| `delete_contact` | Delete a contact |
| `list_documents` | List all documents |
| `get_document` | Get document by ID |
| `update_document_links` | Update document-application links |
| `delete_document` | Delete a document |
### Authentication
The MCP server supports two auth methods:
1. **CRM API token** — use your `jt_` Bearer token from the dashboard
2. **MCP OAuth 2.1** — full authorization code flow with PKCE:
- Discovery: `GET /.well-known/oauth-authorization-server`
- Registration: `POST /api/mcp/register`
- Authorization: `GET /api/mcp/authorize`
- Token exchange: `POST /api/mcp/token`
### Claude Desktop Configuration
```json
{
"mcpServers": {
"nexus-crm": {
"url": "https://nexus.vasudev.xyz/api/mcp",
"headers": {
"Authorization": "Bearer jt_"
}
}
}
}
```
---
## Deployment
### Docker (Self-Hosted)
Build and run:
```bash
docker build -t nexus-crm .
docker run -p 3001:3001 --env-file .env -e PORT=3001 nexus-crm
```
Or use `docker-compose.yml` for a production setup:
```bash
# Edit .env.production with your values
docker compose up -d
```
The compose file mounts `./data` for the Prisma directory and `./uploads` for file storage.
### GCP Cloud Run (CI/CD)
The repo includes a GitHub Actions workflow (`.github/workflows/deploy-gcp.yml`) that on push to `main`:
1. Pushes schema updates (`prisma db push`)
2. Builds a Docker image
3. Pushes to Artifact Registry (`europe-west1`)
4. Deploys to Cloud Run with secrets from Secret Manager
Uses Workload Identity Federation — no service account keys. Requires these GitHub secrets:
- `GCP_WORKLOAD_IDENTITY_PROVIDER`
- `GCP_SERVICE_ACCOUNT`
Cloud Run configuration: 512Mi memory, 1 CPU, 0–2 instances, port 8080.
### Manual / VPS
```bash
npm run build
./deploy.sh # builds, copies standalone output, restarts systemd service
```
The standalone output (via `next.config.ts` `output: "standalone"`) produces a self-contained `server.js` in `.next/standalone/`.
---
## Pipeline Stages
| Stage | Meaning |
| ----- | ------- |
| `inbound` | New lead |
| `applied` | Contacted |
| `interview` | Negotiation |
| `offer` | Closing |
| `rejected` | Lost |
---
## Troubleshooting
### Database Connection
**Error:** `Can't reach database server`
1. Verify PostgreSQL is running: `pg_isready -h localhost`
2. Check `DATABASE_URL` format: `postgresql://USER:PASSWORD@HOST:PORT/DATABASE`
3. For Neon: ensure the connection string includes `?sslmode=require`
### Prisma Issues
**Error:** `Prisma Client not generated`
```bash
npx prisma generate
```
**Error:** `The database schema is not in sync`
```bash
npx prisma db push
```
### Auth Not Working
- Verify `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` are set
- Ensure `BETTER_AUTH_URL` matches the URL you're accessing (including protocol)
- Check that your email is in `ALLOWED_EMAIL`
- In development, OAuth is bypassed — if you see `Dev User`, auth is intentionally skipped
### Port Already in Use
The app runs on port 3001 by default. If it's taken:
```bash
npm run dev -- -p 3002
```
### Pre-commit Hooks
Install hooks:
```bash
pip install pre-commit
pre-commit install
```
The hooks run gitleaks (secrets scan), ESLint, and general file hygiene checks.
---
## License
Private project. All rights reserved.