{"id":51198918,"url":"https://github.com/lahavrud/rs-recruiting","last_synced_at":"2026-06-27T23:01:08.440Z","repository":{"id":357700926,"uuid":"1132664314","full_name":"lahavrud/rs-recruiting","owner":"lahavrud","description":"Specialized CRM for boutique recruitment agency - MVP. FastAPI backend with vertical slices architecture.","archived":false,"fork":false,"pushed_at":"2026-06-26T22:19:12.000Z","size":16627,"stargazers_count":0,"open_issues_count":37,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-27T00:30:26.464Z","etag":null,"topics":["crm","docker","fastapi","mvp","postgresql","python","recruitment"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lahavrud.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-01-12T09:28:09.000Z","updated_at":"2026-06-26T11:37:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/lahavrud/rs-recruiting","commit_stats":null,"previous_names":["lahavrud/rs-recruitment","lahavrud/rs-recruiting"],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/lahavrud/rs-recruiting","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lahavrud%2Frs-recruiting","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lahavrud%2Frs-recruiting/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lahavrud%2Frs-recruiting/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lahavrud%2Frs-recruiting/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lahavrud","download_url":"https://codeload.github.com/lahavrud/rs-recruiting/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lahavrud%2Frs-recruiting/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34870654,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-27T02:00:06.362Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["crm","docker","fastapi","mvp","postgresql","python","recruitment"],"created_at":"2026-06-27T23:00:53.612Z","updated_at":"2026-06-27T23:01:08.434Z","avatar_url":"https://github.com/lahavrud.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RS Recruitment\n\nA full-stack recruitment CRM built for a boutique agency. Manages the full pipeline from company onboarding and job posting through candidate applications to admin-gated match decisions — with a dark luxury React frontend served over a production AWS stack.\n\n**Live:** [rs-recruiting.com](https://rs-recruiting.com)\n\n\u003cimg src=\"docs/screenshots/apply-flow.gif\" width=\"700\" alt=\"Landing page and public job board\" /\u003e\n\u003cp\u003e\u003cem\u003ePublic-facing site — landing page, job board, and candidate application flow\u003c/em\u003e\u003c/p\u003e\n\n\u003cimg src=\"docs/screenshots/admin-dashboard.png\" width=\"650\" alt=\"Admin dashboard\" /\u003e\n\u003cp\u003e\u003cem\u003eAdmin dashboard — live stats across companies, jobs, applications, and candidates with quick-action shortcuts\u003c/em\u003e\u003c/p\u003e\n\n---\n\n## Features\n\n**Public**\n- Job board with per-job detail pages and JSON-LD `JobPosting` structured data\n- Candidate application form with resume upload (PDF/DOCX → S3)\n- GDPR-style consent tracking: timestamp, policy version, IP, user-agent stored per submission\n- SEO: dynamic sitemap.xml, robots.txt, Open Graph meta, server-side prerendered OG pages\n\n**Admin**\n- Invite-based company onboarding (token → registration → approval → activation)\n- Job approval queue (review, approve, or reject postings)\n- Application management with status tracking (New → Approved → Hired/Rejected/Withdrawn)\n- Candidate directory with profile and resume access\n- Append-only audit log: every admin action is recorded with actor, target, IP, and timestamp\n\n**Company**\n- Job posting and management dashboard\n- View applications per job\n\n**Candidate**\n- Self-registration with email verification (2-hour activation window)\n- Profile management (name, phone, LinkedIn URL, resume upload)\n- View submitted applications and their status\n- GDPR data export (profile + per-application resumes as ZIP)\n- Password reset (forgot-password → email link → reset flow)\n\n**Auth**\n- JWT access token (10 min) + HttpOnly refresh cookie (7 days)\n- Role-based route guards (ADMIN / COMPANY / CANDIDATE / public)\n- Account lockout after 5 failed attempts (15-min cooldown, database-backed)\n- Refresh token rotation: single-use tokens deleted on use, logout, or password reset\n\n---\n\n## Tech Stack\n\n| Layer | Technologies |\n|---|---|\n| Frontend | React 19, TypeScript, Vite, Tailwind CSS v4, React Router v7 |\n| Backend | FastAPI, SQLModel (SQLAlchemy + Pydantic), Alembic, Python 3.12 |\n| Database | PostgreSQL 16, asyncpg (connection pool + pre-ping) |\n| Background Jobs | AWS SQS + custom Python worker, EventBridge Scheduler (nightly purge) |\n| File Storage | AWS S3 (production), local filesystem (dev) — provider abstraction |\n| Email | Resend via SMTP relay (production) — provider abstraction; 10+ HTML templates |\n| Auth | JWT (PyJWT), bcrypt, HttpOnly refresh cookie, slowapi rate limiting |\n| Observability | Sentry (backend + frontend with source maps), Google Tag Manager, CloudWatch |\n| Infrastructure | EC2 + RDS + S3 + SQS + ECR + SSM + CloudFront, Cloudflare (DNS only) |\n| CI/CD | GitHub Actions — OIDC auth, change detection, Pytest against PostgreSQL, SSM deploy |\n| Code Quality | Ruff, ESLint, TypeScript strict, 5 custom validation scripts, weekly pip-audit |\n\n---\n\n## Architecture\n\n\u003cimg src=\"docs/screenshots/aws-architecture.png\" width=\"750\" alt=\"AWS architecture diagram\" /\u003e\n\n\u003cp\u003e\u003cem\u003eRequest path: Users → Cloudflare (DNS only) → CloudFront → S3 (frontend SPA) or EC2 via API/auth/health behaviors (Lambda@Edge handles bot detection for OG prerender). Background jobs: SQS → worker container. CI/CD path: GitHub Actions → S3 (frontend bundle) + ECR (Docker images) + SSM Run Command → EC2. Observability: CloudWatch alarms → SNS ops-alerts; Inspector2 scanning ECR images. All secrets live in SSM Parameter Store as SecureStrings.\u003c/em\u003e\u003c/p\u003e\n\n### Data model\n\n```mermaid\nerDiagram\n    User ||--o| CompanyProfile : owns\n    User ||--o| CandidateProfile : \"linked (optional)\"\n    CompanyProfile ||--o{ Job : posts\n    Job ||--o{ Application : receives\n    CandidateProfile ||--o{ Application : submits\n\n    User {\n        int id\n        string email\n        string hashed_password\n        enum role \"ADMIN, COMPANY, CANDIDATE\"\n        bool is_active\n    }\n    CompanyProfile {\n        int id\n        int user_id\n        string name\n        string logo_url\n    }\n    Job {\n        int id\n        int company_id\n        string title\n        enum status \"PENDING_APPROVAL, PUBLISHED, CLOSED\"\n    }\n    CandidateProfile {\n        int id\n        int user_id \"nullable — anonymous leads have no linked User\"\n        string full_name\n        string email\n        string resume_path\n        datetime consent_given_at\n    }\n    Application {\n        int id\n        int job_id\n        int candidate_id\n        enum status \"NEW, APPROVED_BY_ADMIN, REJECTED, HIRED, WITHDRAWN\"\n        text admin_notes\n    }\n```\n\n---\n\n## Design Decisions\n\n**Three-tier authentication** — Admins, companies, and candidates are all full authenticated roles (ADMIN, COMPANY, CANDIDATE). Admins approve company invites; companies post jobs; candidates self-register, activate via email, and claim their applications. The schema distinguishes authenticated candidates (`user_id` linked) from anonymous leads (applications submitted before registration), enabling a seamless \"register and claim\" flow without breaking legacy data.\n\n**Stateless JWT with short-lived access tokens** — Access tokens have a 10-minute TTL; refresh tokens are single-use and deleted from the database on logout or refresh. There is no blacklist — the short TTL serves as the post-logout tolerance window. Refresh token rotation (delete consumed token, issue new pair) prevents replays. Failed login attempts and account lockout are tracked on the `User` row with a `locked_until` timestamp.\n\n**Storage and email abstraction** — Both file storage and email are behind provider interfaces. A single env var switches between local/S3 for storage with no code changes. Email providers can be SES or SMTP; production uses Resend via SMTP relay. This made local development cheap and production deployment straightforward.\n\n**Async task queue with AWS SQS** — Sending email from inside a request handler risks timeouts and drops on provider throttling. All outbound email is pushed to an SQS queue and processed by a separate worker container (`src/worker.py`) with retry logic. Ten transactional email templates cover the full company and candidate lifecycle. The `defer_after_commit` pattern ensures tasks are enqueued only after the originating transaction commits, preventing phantom messages on rollback.\n\n**OIDC-based CI/CD with change detection** — GitHub Actions authenticates to AWS via OIDC (no stored credentials). A `detect-changes` job skips irrelevant work — a docs-only PR never runs backend tests or builds Docker. The deploy workflow supports manual re-deploy by SHA, checks if an ECR image already exists before rebuilding, and polls SSM run-command status rather than fire-and-forget. Deployments are never cancelled mid-flight.\n\n**Custom CI validation scripts** — Beyond Ruff and TypeScript, five custom scripts run in CI: SOC import enforcement (services must not import FastAPI), blocking I/O detection in async functions (catches `open()`, `requests.*`, `time.sleep()`), type hint coverage on public functions, test file existence checks (1:1 mapping with source files), and file size limits. Catches architecture drift that standard linters miss.\n\n**Docker hardening** — Multi-stage build with layer caching on the lockfile. Runtime image runs as a non-root `appuser` (permissions fixed in entrypoint script). Dev and test dependencies are excluded. Health check hits the `/health` endpoint via the same proxy path a real client uses.\n\n**SEO prerendering for a SPA** — Client-side React can't be indexed for job-specific pages. The backend generates server-side HTML snapshots with full Open Graph meta, canonical URLs, and JSON-LD `JobPosting` structured data (title, salary range, location, dates). A dynamic sitemap.xml lists all published jobs with `lastmod` from `updated_at`. Googlebot gets a real HTML response; users get the SPA.\n\n**Hebrew-only RTL UI** — The entire frontend is in Hebrew with `\u003chtml dir=\"rtl\"\u003e` forced globally. All UI strings live in per-namespace JSON files under `locales/he/` (13 files, one per feature area); raw backend error strings are never surfaced to the user.\n\n---\n\n## Testing\n\n70+ test files, ~18k lines, parallel execution via `pytest-xdist` (each worker gets a dedicated database).\n\n```\ntests/\n├── models/           # ORM model validation\n├── services/         # Business logic (auth, admin, company, public, candidate flows)\n├── api/              # Endpoint tests (SEO, rate limiting, request handling)\n├── templates/        # Email template rendering\n└── core/\n    ├── services/     # Email, storage, file validation\n    └── infrastructure/  # Database, config, security, transactions, rate limiting\n```\n\nNotable coverage: full auth lifecycle (invite → registration → approval → activation → login → lockout → logout), candidate registration and activation, SEO output (sitemap, JSON-LD, OG prerender), SQS task enqueue/handling (email, data export, candidate purge), storage abstraction, database transactions and rollback guarantees.\n\n```bash\nuv run pytest -n auto\n```\n\n---\n\n## Local Development\n\n**Prerequisites:** Python 3.12+, [uv](https://github.com/astral-sh/uv), Docker + Docker Compose, Node 18+\n\n```bash\n# 1. Clone and install\ngit clone https://github.com/lahavrud/rs-recruiting.git\ncd rs-recruiting\nuv sync\n\n# 2. Start services (PostgreSQL + Mailpit local SMTP)\ndocker-compose up -d\n\n# 3. Run migrations\nuv run alembic upgrade head\n\n# 4. Start backend\nuv run uvicorn src.main:app --reload\n\n# 5. Start frontend (separate terminal)\ncd frontend\nnpm install\nnpm run dev\n```\n\nThe frontend proxies `/api/*` to `http://localhost:8000`. Outbound email goes to [Mailpit](http://localhost:8025) — no provider account needed in development. Tasks (email, exports) run inline in the API process when `SQS_QUEUE_URL` is unset.\n\n### Environment\n\n```bash\n# Minimum required\nexport JWT_SECRET_KEY=$(python3 -c \"import secrets; print(secrets.token_urlsafe(32))\")\n```\n\nProduction env vars (AWS credentials, Sentry DSN, Resend SMTP credentials, S3 bucket) are only needed outside local dev — the defaults in `docker-compose.yml` cover everything for local work.\n\n### Linting\n\n```bash\nuv run ruff check . \u0026\u0026 uv run ruff format --check .\ncd frontend \u0026\u0026 npx tsc --noEmit \u0026\u0026 npm run lint\n```\n\n---\n\n## Project Structure\n\n```\nrs-recruiting/\n├── src/\n│   ├── api/          # Thin FastAPI routers (auth, admin, company, public, seo)\n│   ├── services/     # Business logic, decoupled from routers\n│   │   ├── auth/     # session, registration, activation, password_reset, candidate_registration, password_change\n│   │   ├── admin/    # companies, jobs, applications, candidates, invites, audit\n│   │   ├── company/  # jobs, profile, candidates\n│   │   └── utils/    # audit logging, contract PDF, legal text\n│   ├── core/         # Infrastructure abstractions: storage, email, task queue definitions\n│   ├── models.py     # SQLModel ORM models\n│   ├── templates/    # Transactional email templates (HTML)\n│   └── worker.py     # SQS worker — polls queue and dispatches to task registry\n├── frontend/src/\n│   ├── pages/        # public/, admin/, company/, candidate/ + auth pages\n│   ├── components/   # layout/, guards/, ui/ — shared React components\n│   ├── hooks/        # useAuth, useInfiniteList, useDebounce, usePageTitle…\n│   └── locales/he/   # per-namespace translation files (common, auth, admin, …)\n├── tests/            # 70+ test files, pytest-xdist parallel execution\n├── scripts/          # 5 CI validation scripts\n├── docs/             # Architecture decisions, API design, infrastructure, runbooks\n└── .github/workflows/\n    ├── ci.yml        # Lint, test, docker-build (change-aware)\n    ├── deploy.yml    # Build + deploy to production (OIDC + SSM)\n    └── security-audit.yml  # Weekly pip-audit for CVEs\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flahavrud%2Frs-recruiting","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flahavrud%2Frs-recruiting","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flahavrud%2Frs-recruiting/lists"}