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
- Features
- Tech Stack
- Prerequisites
- Getting Started
- Architecture
- Environment Variables
- Available Scripts
- Testing
- API
- MCP Server
- Deployment
- Pipeline Stages
- Troubleshooting
- License
- 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
| Layer | Technology |
|---|---|
| Framework | Next.js 16 — App Router, React 19, standalone output |
| Database | Prisma 6 + PostgreSQL (swappable adapter for Firestore) |
| Auth | better-auth — Google OAuth, session cookies |
| Data Fetching | TanStack Query v5 — caching, optimistic UI |
| Table | TanStack Table v8 — headless sort & filter |
| Drag & Drop | @dnd-kit — Kanban board |
| i18n | next-intl — DE / EN |
| Styling | Tailwind CSS v3 — class-based dark mode |
| Validation | Zod 4 — runtime schema validation |
| AI Integration | @modelcontextprotocol/sdk — MCP server |
| File Storage | Google Cloud Storage or local filesystem |
| Hosting | Google Cloud Run — container hosting |
| Database Hosting | Neon — serverless PostgreSQL |
| Testing | Vitest 4 |
| CI/CD | GitHub Actions — lint, build, test, deploy to Cloud Run |
- Node.js 22+ (matches the Docker image; 20+ works locally)
- PostgreSQL 15+ — or a hosted provider like Neon (free tier available)
- Google OAuth Client ID — create one here
- 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)
git clone https://github.com/5queezer/job-tracker.git
cd job-trackernpm installcp .env.example .envEdit .env with your values. See Environment Variables for the full reference.
At minimum, you need:
DATABASE_URL— a PostgreSQL connection stringBETTER_AUTH_SECRET— generate withopenssl rand -base64 32BETTER_AUTH_URL— your app's public URL (usehttp://localhost:3001for local dev)GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET— from Google Cloud ConsoleALLOWED_EMAIL— comma-separated list of emails permitted to log in
Push the Prisma schema to your database:
npx prisma db pushThis creates all tables. For an existing database with prior migrations:
npx prisma migrate deployGenerate the Prisma client (usually automatic after npm install):
npx prisma generatenpm run devOpens at 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.
├── 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
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 PostgreSQLfirestore— Firebase Admin SDK with Firestore
All API routes call getDb() which returns a DatabaseAdapter interface. Swapping backends requires zero code changes.
File storage (lib/storage.ts) switches between:
- Google Cloud Storage — when
GCS_BUCKETis set - Local filesystem — default, stores in
uploads/
- Google OAuth via better-auth → session cookie
- API tokens — per-user Bearer tokens (
jt_prefix), generated in the dashboard - MCP OAuth 2.1 — full authorization code flow with PKCE for AI agent integration (
mcp_at_prefix) - 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 → middleware.ts (rate limiting) → API route handler
→ requireAuth() (session/Bearer/dev)
→ getDb() (database adapter)
→ Response with rate-limit headers
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
| 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 |
| 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 | — |
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.
| 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 |
# Run all tests
npm test
# Run in watch mode
npx vitest
# Run a specific test file
npx vitest lib/__tests__/rate-limit.test.tslib/__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.
- Swagger UI: /api-docs — interactive API explorer
- OpenAPI spec: /openapi.json — machine-readable spec
- LLM guide: /llm.txt — plain-text API reference for AI agents
| Method | How | Scope |
|---|---|---|
| Session cookie | Google OAuth login at /login |
Full access |
| Bearer token | Authorization: Bearer jt_<token> |
Owner's data (admin: all data) |
| MCP OAuth | Authorization: Bearer mcp_at_<token> |
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.
| 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 |
| 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 |
| 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 |
| 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 |
| Method | Path | Description |
|---|---|---|
GET |
/api/token |
Get current token metadata |
POST |
/api/token |
Generate new token |
DELETE |
/api/token |
Revoke token |
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.
Nexus CRM exposes a Model Context Protocol server at /api/mcp for AI agent integration.
| 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 |
The MCP server supports two auth methods:
- CRM API token — use your
jt_Bearer token from the dashboard - 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
- Discovery:
{
"mcpServers": {
"nexus-crm": {
"url": "https://nexus.vasudev.xyz/api/mcp",
"headers": {
"Authorization": "Bearer jt_<your-token>"
}
}
}
}Build and run:
docker build -t nexus-crm .
docker run -p 3001:3001 --env-file .env -e PORT=3001 nexus-crmOr use docker-compose.yml for a production setup:
# Edit .env.production with your values
docker compose up -dThe compose file mounts ./data for the Prisma directory and ./uploads for file storage.
The repo includes a GitHub Actions workflow (.github/workflows/deploy-gcp.yml) that on push to main:
- Pushes schema updates (
prisma db push) - Builds a Docker image
- Pushes to Artifact Registry (
europe-west1) - Deploys to Cloud Run with secrets from Secret Manager
Uses Workload Identity Federation — no service account keys. Requires these GitHub secrets:
GCP_WORKLOAD_IDENTITY_PROVIDERGCP_SERVICE_ACCOUNT
Cloud Run configuration: 512Mi memory, 1 CPU, 0–2 instances, port 8080.
npm run build
./deploy.sh # builds, copies standalone output, restarts systemd serviceThe standalone output (via next.config.ts output: "standalone") produces a self-contained server.js in .next/standalone/.
| Stage | Meaning |
|---|---|
inbound |
New lead |
applied |
Contacted |
interview |
Negotiation |
offer |
Closing |
rejected |
Lost |
Error: Can't reach database server
- Verify PostgreSQL is running:
pg_isready -h localhost - Check
DATABASE_URLformat:postgresql://USER:PASSWORD@HOST:PORT/DATABASE - For Neon: ensure the connection string includes
?sslmode=require
Error: Prisma Client not generated
npx prisma generateError: The database schema is not in sync
npx prisma db push- Verify
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETare set - Ensure
BETTER_AUTH_URLmatches 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
The app runs on port 3001 by default. If it's taken:
npm run dev -- -p 3002Install hooks:
pip install pre-commit
pre-commit installThe hooks run gitleaks (secrets scan), ESLint, and general file hygiene checks.
Private project. All rights reserved.