164 lines
5.4 KiB
Bash
164 lines
5.4 KiB
Bash
# 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=* * * * *
|
|
# Daily Sync Inbox (POST /email/sync) at 05:00 AM PKT. Token must match the
|
|
# Bearer the cron sends; leave blank to skip the tick with a warning.
|
|
INBOX_SYNC_CRON=0 5 * * *
|
|
INBOX_SYNC_CRON_TZ=Asia/Karachi
|
|
CRON_INBOX_SYNC_TOKEN=
|
|
|
|
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=<file_path or key> (auth) → temporary URL
|
|
# Or stream: GET /s3/download?key=... (auth)
|
|
|
|
LOG_FORMAT=json
|
|
LOG_LEVEL=INFO
|