Talent-Acquisition-Platform

Architecture: P99soft Recruitment Platform v3

Overview

P99soft is a multi-tenant SaaS recruitment platform built for enterprise hiring teams. It follows the WAT architecture (Workflows, Agents, Tools) with a clean separation between the AI orchestration layer and deterministic execution scripts.

┌─────────────────────────────────────────────────────────────────────┐
│                        Browser / Client                             │
│                    React 18 + TypeScript + Vite                     │
└──────────────────────────────┬──────────────────────────────────────┘
                               │ HTTPS / REST + SSE
┌──────────────────────────────▼──────────────────────────────────────┐
│                        Nginx (Reverse Proxy)                        │
│          Static assets → /usr/share/nginx/html                      │
│          /api/* → proxy_pass http://backend:8000                    │
└──────────────────────────────┬──────────────────────────────────────┘
                               │
┌──────────────────────────────▼──────────────────────────────────────┐
│                    FastAPI Backend (Python 3.12)                    │
│                                                                     │
│  ┌─────────────────┐   ┌──────────────┐   ┌────────────────────┐   │
│  │   API Layer      │   │  Services    │   │   Background Jobs  │   │
│  │  /api/v1/...    │   │  ai_service  │   │  threading.Thread  │   │
│  │  auth, users    │   │  sourcing    │   │  (sourcing jobs)   │   │
│  │  candidates     │   │  candidate   │   └────────────────────┘   │
│  │  jobs, pipeline │   │  pipeline    │                             │
│  │  sourcing, ai   │   └──────────────┘                             │
│  └─────────────────┘                                                │
│                                                                     │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                    Core Layer                               │   │
│  │  config.py  database.py  security.py  deps.py              │   │
│  └─────────────────────────────────────────────────────────────┘   │
└──────┬──────────────────────────────────────────────────────────────┘
       │                              │                        │
┌──────▼──────┐              ┌────────▼──────┐    ┌───────────▼──────┐
│  PostgreSQL  │              │     Redis     │    │  Claude API      │
│  (primary   │              │  (rate limit) │    │  (claude-sonnet) │
│   database) │              └───────────────┘    └──────────────────┘
└─────────────┘
       │
┌──────▼──────────────────────────────────────────────────────────────┐
│                     tools/ (Python scripts)                         │
│  search_linkedin.py  search_github.py  scrape_naukri.py            │
│  search_indeed.py    search_twitter.py  dedupe_and_score.py        │
│  export_to_excel.py  read_google_sheet.py                          │
└─────────────────────────────────────────────────────────────────────┘

Multi-Tenancy Model

Single-database, row-level isolation. Every table that holds customer data has an org_id UUID column. The RBAC middleware injects current_user.org_id into every query — making cross-tenant data leakage structurally impossible through the API layer.

organizations
    └─── users (org_id FK)
         └─── user_roles (org_id FK)
    └─── job_requisitions (org_id FK)
    └─── candidates (org_id FK)
    └─── pipeline_entries (org_id FK)
    └─── sourcing_jobs (org_id FK)
    └─── audit_logs (org_id)

Tenant isolation pattern

# Every query is always scoped to current org
result = await db.execute(
    select(Candidate).where(
        Candidate.id == candidate_id,
        Candidate.org_id == current_user.org_id  # ← always enforced
    )
)

RBAC Model

See RBAC.md for full details.

6 system roles (seeded at migration time):

Role Level
super_admin Platform-wide
org_admin Full org access
hiring_manager Jobs + pipeline
recruiter Sourcing + candidates
interviewer Read + feedback only
viewer Read-only

20 granular permissions covering: candidates, jobs, pipeline, sourcing, analytics, users, roles, ai, settings, integrations.

Permission enforcement is a FastAPI dependency factory:

@router.delete("/{id}")
async def delete_candidate(
    id: uuid.UUID,
    user = Depends(require_permission("candidates:delete"))  # ← enforced here
):
    ...

Claude AI Integration

The AIService class in app/services/ai_service.py wraps the Anthropic SDK with:

  1. Prompt caching (cache_control: ephemeral) on system prompts — reduces costs on repeated calls
  2. Per-org API key override — each org can configure their own Anthropic key in Settings
  3. Async — all calls use anthropic.AsyncAnthropic

6 AI capabilities

Endpoint Description
POST /api/v1/ai/score-candidate 0-10 score with reasoning, strengths, gaps
POST /api/v1/ai/parse-jd Extract skills, seniority, requirements from raw JD text
POST /api/v1/ai/interview-questions 10 questions by round type (technical/behavioral/culture_fit/hr)
POST /api/v1/ai/draft-offer-letter Full offer letter with subject line
POST /api/v1/ai/recruiter-email Stage-appropriate recruiter email
POST /api/v1/ai/enhance-queries AI-optimized Boolean search queries per sourcing channel

Data Flow: Candidate Sourcing

User triggers sourcing
       │
       ▼
POST /api/v1/sourcing/run
       │
       ├─ Creates SourcingJob record (status=pending)
       ├─ Spawns background thread
       └─ Returns {job_ids: [...]}
              │
              ▼
    Background Thread (run_sourcing_job)
              │
    ┌─────────┴──────────────────────────────────────────────┐
    │ 1. search_linkedin.py  → 60 candidates                 │
    │ 2. search_github.py    → 40 candidates                 │
    │ 3. scrape_naukri.py    → 45 candidates                 │
    │ 4. search_indeed.py    → 40 candidates                 │
    │ 5. search_twitter.py   → 25 candidates                 │
    └─────────┬──────────────────────────────────────────────┘
              │
    ┌─────────▼──────────┐
    │ dedupe_and_score.py │  ← fuzzy dedup + rule-based scoring
    └─────────┬──────────┘
              │
    ┌─────────▼──────────┐
    │ export_to_excel.py  │  ← Excel with hyperlinks + color scores
    └─────────┬──────────┘
              │
    Updates SourcingJob (status=done, candidates_found=N)
              │
              ▼
User polls GET /api/v1/sourcing/jobs/{id}
   or streams SSE GET /api/v1/sourcing/jobs/{id}/stream

Backend Directory Structure

backend/
├── app/
│   ├── main.py              # App factory: CORS, audit middleware, rate limiter, routes
│   ├── api/v1/
│   │   ├── router.py        # Aggregate all v1 routes under /api/v1
│   │   ├── auth.py          # Login, refresh, logout, profile
│   │   ├── users.py         # User CRUD (org-scoped)
│   │   ├── roles.py         # Role + permission management
│   │   ├── candidates.py    # Candidate CRUD + notes
│   │   ├── jobs.py          # Job requisitions CRUD
│   │   ├── pipeline.py      # Pipeline stages, interview rounds, offers
│   │   ├── sourcing.py      # Sourcing jobs + SSE log stream
│   │   ├── analytics.py     # Funnel stats, source quality
│   │   └── ai.py            # All Claude AI endpoints
│   ├── core/
│   │   ├── config.py        # All settings via pydantic-settings + .env
│   │   ├── database.py      # Async SQLAlchemy engine + session factory
│   │   ├── security.py      # JWT, bcrypt, Fernet encryption
│   │   └── deps.py          # FastAPI deps: get_current_user, require_permission
│   ├── models/              # SQLAlchemy models (one file per domain)
│   ├── schemas/             # Pydantic v2 request/response models
│   ├── services/
│   │   └── ai_service.py    # Claude API integration (all 6 AI features)
│   └── middleware/
│       └── audit.py         # Auto-audit-log all mutating requests
├── alembic/                 # Database migrations
│   └── versions/001_initial_schema.py  # All tables + seed data
├── tests/
├── Dockerfile
├── requirements.txt
└── .env.example

Frontend Directory Structure

frontend/src/
├── components/
│   ├── ui/                  # Design system primitives
│   │   ├── Button.tsx       # variant: primary|secondary|ghost|danger|outline
│   │   ├── Input.tsx        # with label, error, helpText, icons
│   │   ├── Card.tsx         # Card, CardHeader, CardBody, CardFooter
│   │   ├── Badge.tsx        # status badges + stageBadgeVariant helper
│   │   ├── Modal.tsx        # accessible dialog with Escape/overlay close
│   │   ├── Spinner.tsx      # Spinner + FullPageSpinner
│   │   ├── Avatar.tsx       # image or initials with color hash
│   │   ├── Skeleton.tsx     # loading placeholders + TableSkeleton
│   │   ├── Toast.tsx        # toast store + ToastContainer
│   │   └── index.ts         # barrel export — import from 'components/ui'
│   ├── layout/
│   │   ├── AppShell.tsx     # sidebar + main content wrapper
│   │   ├── Sidebar.tsx      # role-filtered nav using usePermission
│   │   └── PageHeader.tsx   # title + breadcrumbs + action slot
│   └── rbac/
│       ├── PermissionGate.tsx  # render-level permission check
│       └── RoleGuard.tsx       # route-level redirect guard
├── services/
│   ├── api.ts               # Axios instance with JWT inject + 401 refresh
│   ├── auth.service.ts      # login, logout, profile
│   └── ai.service.ts        # all 6 AI endpoints
├── stores/
│   └── authStore.ts         # user, token, refreshToken, permissions[], hasPermission()
├── hooks/
│   ├── usePermission.ts     # hasPermission(), hasRole(), isAdmin()
│   └── useAuth.ts           # login/logout with navigation + toast
└── pages/                   # Route-level page components

Security Model

Layer Mechanism
Auth JWT access tokens (15 min) + rotating refresh tokens (7 days, hashed in DB)
Passwords bcrypt, cost factor 12
API keys at rest Fernet symmetric encryption
Tenant isolation org_id injected into every DB query
Rate limiting SlowAPI: 100 req/min per IP
CORS Explicit allowed origins list
Input validation Pydantic v2 on all request schemas
SQL injection SQLAlchemy ORM only, no raw f-string queries
Audit log All POST/PUT/DELETE auto-logged with user, IP, resource