# Sole secrets / config file for the whole monorepo (app + backend + Compose). # Copy to backend/.env and fill in. Never commit a filled .env. # # Compose: docker compose --env-file ./backend/.env up -d --build # true → RDS over SSL (asyncpg). false → local Postgres over asyncpg (no SSH). PROD_ENV=false DB_USERNAME=postgres DB_PASSWORD= DB_HOST=localhost DB_PORT=5432 DB_NAME=hrms # Blank: require when PROD_ENV=true, off when local. Override only if needed. DB_SSLMODE= DB_AUTO_MIGRATE=true DB_AUTOGENERATE=true EMAIL_URL= EMAIL_API_TOKEN= # Optional overrides; blank falls back to EMAIL_URL / EMAIL_API_TOKEN. CALENDAR_URL= CALENDAR_API_TOKEN= EMAIL_SYNC_FOLDER=inbox EMAIL_SYNC_SINCE= EMAIL_SYNC_CRON=* * * * * JWT_SECRET_KEY= JWT_ALGORITHM=HS256 JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30 JWT_REFRESH_TOKEN_EXPIRE_DAYS=7 JWT_RESET_TOKEN_EXPIRE_MINUTES=10 TEAMS_MAIL_API_URL= TEAMS_API_TOKEN= RESET_CODE_TTL_SECONDS=60 RESET_CODE_RESEND_SECONDS=30 RESET_CODE_MAX_ATTEMPTS=5 FRONTEND_URL=http://127.0.0.1:5173 CONFIRM_EMAIL_PATH=/auth/confirm-email CONFIRM_TOKEN_TTL_SECONDS=86400 CONFIRM_TOKEN_RESEND_SECONDS=60 BUFFER_API= BUFFER_API_URL=https://api.buffer.com BUFFER_CHANNEL_ID= # Talent sourcing via Apify (talent/). Token from console.apify.com → Settings → # API & Integrations. APIFY_TOKEN is honoured as a fallback name for the token. APIFY_API_TOKEN= APIFY_API_BASE=https://api.apify.com/v2 APIFY_ACTOR_ID=harvestapi~linkedin-profile-search # Hard per-run cap; client requests are clamped to it. "Full" mode costs # $0.10 per search page + $0.004 per profile (~$0.20 for a 25-profile run). APIFY_MAX_RESULTS=25 # Server-side spend ceiling per run (Apify maxTotalChargeUsd; minimum $0.10). APIFY_MAX_COST_USD=1.0 # Own companies whose CURRENT employees must never appear in sourced results. # Names feed the always-on server-side filter (case-insensitive substring); # URLs feed the actor's excludeCurrentCompanies filter (full LinkedIn company # URLs) so those profiles are not even scraped. Comma-separated. APIFY_EXCLUDE_COMPANIES=Utopia Brands,Utopia Deals APIFY_EXCLUDE_COMPANY_URLS=https://www.linkedin.com/company/utopiadeals,https://www.linkedin.com/company/utopia-brands-usa,https://www.linkedin.com/company/utopiabrands # Short | Full | Full + email search APIFY_PROFILE_MODE=Full APIFY_TIMEOUT=30 OPENAI_API_KEY= OPENAI_MODEL=gpt-5.4-mini # Blank omits the parameter, for reasoning models that reject it. OPENAI_TEMPERATURE=0 OPENAI_MAX_OUTPUT_TOKENS=4096 OPENAI_TIMEOUT=60 OPENAI_MAX_RETRIES=3 OPENAI_CONNECT_RETRIES=3 # Set only for Azure OpenAI or a gateway; blank uses api.openai.com. OPENAI_BASE_URL= OPENAI_ORGANIZATION= OPENAI_PROJECT= # ATS scoring (bulk-ats engine). Shared OPENAI_* names above. OPENAI_EFFORT=low OPENAI_ENABLE_PROMPT_CACHE=true OPENAI_TIMEOUT_SECONDS=120 SCORING_CONCURRENCY=5 MAX_RESUMES_PER_REQUEST=50 MAX_PDF_SIZE_MB=10 MAX_JD_CHARS=30000 MAX_RESUME_CHARS=60000 # Inbox intake gate (inbox_classifier/). INBOX_TRIAGE_ENABLED=true INBOX_TRIAGE_FAIL_OPEN=true INBOX_TRIAGE_CONCURRENCY=5 INBOX_TRIAGE_MAX_SUBJECT_CHARS=300 INBOX_TRIAGE_MAX_BODY_CHARS=4000 INBOX_TRIAGE_MIN_CONFIDENCE=0 # Compose overrides these on the network; keep docker DNS names for containers. REDIS_URL=redis://redis:6379/0 BACKEND_URL=http://backend-api:8000 TASKIQ_QUEUE_NAME=inbox TASKIQ_CV_QUEUE_NAME=cv_upload TASKIQ_MAX_RETRIES=3 TASKIQ_RETRY_DELAY=5 TASKIQ_MAX_DELAY=120 TASKIQ_DLQ_STREAM=taskiq:dlq TASKIQ_IDLE_TIMEOUT_MS=600000 MANUAL_UPLOAD_TO_ADDRESS=manual-cv-upload@hr-ats.local APP_VERSION=dev # CV Bank. Retention is stamped on the row at upload, so raising this later does # not extend CVs already taken in. The sweep flags expired entries; it never # deletes. Leave the notify address blank to keep the log line only. CV_BANK_RETENTION_MONTHS=24 CV_BANK_RETENTION_CRON=0 3 * * * CV_BANK_RETENTION_NOTIFY_EMAIL= # Tier-1 rank (free keyword overlap) a banked CV must clear to notify a recruiter # when a job opens; and the ATS score a rejected applicant needs to count as a # silver medalist. CV_BANK_SUGGEST_THRESHOLD=55 CV_BANK_SILVER_FLOOR=60 # Compose host ports (docker compose --env-file ./backend/.env …). FRONTEND_PORT=5173 BACKEND_PORT=8000 ATS_PORT=8100 REDIS_PORT=6379 POSTGRES_PORT=5433 UVICORN_WORKERS=2 # Empty = same-origin via nginx on :5173. For Vite on the host, use # VITE_API_BASE=http://127.0.0.1:8000 (backend is published on BACKEND_PORT). VITE_API_BASE= # --- AWS S3 (s3/) — private CVs (no Principal "*" public policy) ------------ # Bucket from your console, e.g. hr-ats-416818527652-us-east-2-an AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_REGION=us-east-2 S3_BUCKET= # Optional CDN / custom domain for stable DB identity URLs only (objects stay private). S3_PUBLIC_BASE_URL= # Leave blank. Do NOT set public-read — CVs are confidential. S3_OBJECT_ACL= # Short-lived browser open links via GET /s3/open (seconds; max 604800). S3_PRESIGN_EXPIRES_SECONDS=900 # CV object keys (after DB row exists): # Email/{inbox_messages.id}/{user_id}/{file}.pdf # Manual/{manual_upload_candidate.id}/{user_id}/{file}.pdf # Form/{form_data.id}/{recruiter_id}/{file}.pdf # Open a CV: GET /s3/open?key= (auth) → temporary URL # Or stream: GET /s3/download?key=... (auth) LOG_FORMAT=json LOG_LEVEL=INFO