Imported from thethien8a/Job-Search-Web-App-Vibe-Coding (
AGENTS.md). Install upstream withnpx skills add thethien8a/Job-Search-Web-App-Vibe-Coding. Copyright stays with the author.
AGENTS.md
This file provides guidance to agents when working with code in this repository.
Build/Run Commands
# Full stack (Docker)
docker-compose up -d --build
# Backend dev (from backend/)
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000
# Frontend dev (from frontend/)
npm install
npm run dev # Vite on :3000, proxies /api to :8000
Architecture
Supabase (public.ready_jobs) <-- Lakehouse-Lite pipeline loads data here
|
FastAPI :8000 # Read-only search API
|
Nginx :80 # Serves React SPA + proxies /api -> backend:8000
Frontend: React 18 + Vite + TailwindCSS + TanStack Query + Axios + Lucide React.
Backend: FastAPI + SQLAlchemy 2.0 (sync) + psycopg2 → read-only on public.ready_jobs.
Critical Gotchas
- Model vs Index Script mismatch:
models.py:14mapsSilverJobtopublic.ready_jobs, butdatabase_indexes.sqlreferencespublic_app_layer.silver_jobs. The index script is stale — do not run it as-is on the live DB. The app's actual table ispublic.ready_jobswith schemapublic. docker-compose.ymlinit script path: references./backend/init_db.sqlas a volume mount, but the actual file is./backend/database_indexes.sql. At deployment, either rename the file or fix the compose path.- Docs disabled in production:
main.py:41-44gates/api/docs,/api/redoc,/api/openapi.jsononsettings.DEBUG. IfAPI_DEBUG=false, these routes return 404. - SSL auto-detection:
config.py:45appends?sslmode=requireto the database URL whenPOSTGRES_HOSTis NOTlocalhostor127.0.0.1. For local dev, uselocalhostto avoid SSL errors.
Search Implementation (Non-Obvious)
The /api/v1/jobs endpoint uses a fetch-then-sort-in-Python pattern (routers/jobs.py:108-165):
- Queries ALL matching
job_urls (up to 2000) withoutORDER BY(explicitly cleared viaquery.order_by(None)) - Sorts them in Python by
job_deadlineDESC, thenjob_title - Slices the sorted list for pagination
- Re-fetches full
SilverJobobjects for only the current page
This is an intentional workaround for PostgreSQL optimizer issues when mixing LIMIT/OFFSET with complex GIN index searches. Do NOT "optimize" this back to a SQL ORDER BY ... LIMIT pattern — pagination will silently return wrong/unsorted results.
The search combines three strategies with OR logic:
- Synonym ILIKE: Hardcoded bilingual EN-VN synonym groups in
routers/jobs.py:19-28(e.g.,"intern"→ also matches"thực tập","trainee") - Full-text search: PostgreSQL
plainto_tsquery('english', ...)for English stemming - Fuzzy matching:
pg_trgmsimilarity with threshold 0.25
Requires pg_trgm extension enabled on the DB.
In-Memory Caching
utils.py defines a custom TTLCache class used via @cache_response() decorator:
dropdown_cache: TTL 1 hour (locations, source-sites endpoints)search_cache: TTL 1 minute (stats endpoint)
This is process-level, in-memory only — cache is not shared across workers.
API Characteristics
- Read-only: CORS allows only
GETmethods (main.py:57) - API prefix:
/api/v1(configurable viaAPI_PREFIXenv var) - Rate limiting: 60 req/min per IP via
slowapi(main.py:21) - Error handling: Production hides internal details; DEBUG mode exposes full traceback (
main.py:74)