Squad Five capstone for the Andela AI Engineering Bootcamp. The goal is straightforward on paper: help a candidate move from “I found a role” to “I submitted strong materials” without burning an afternoon on manual rewrites. This repository is the scaffold only—FastAPI for the API surface, a static-export Next.js client, and a Terraform root that is intentionally empty except for provider wiring, variables, outputs, and comments that describe the AWS shape you are heading toward (Aurora Serverless v2 + Data API, Secrets Manager, ECS or Lambda for LangGraph/OpenRouter, API Gateway + Clerk JWT, CloudFront in front of S3). Implementers add resources when they are ready.
Product direction (for context while you build):
- Ingest a resume plus a job posting URL.
- Diff the candidate's story against the role (ATS-oriented gap analysis).
- Generate refreshed resume copy, a cover letter with narrative structure, and a Gmail-ready draft.
The backend is now fully implemented with a LangGraph-based workflow for generating job application materials.
| Endpoint | Method | Description |
|---|---|---|
/api/v1/apply |
POST | Run the complete TalentStreamAI workflow |
/api/v1/fetch-job |
POST | Fetch and parse a job description from URL |
/api/v1/parse-resume |
POST | Parse a resume file (PDF or DOCX) |
/api/v1/score-ats |
POST | Score resume against job description for ATS compatibility |
The main /api/v1/apply endpoint accepts:
job_url(str): URL of the job postingresume(UploadFile): Resume file (PDF or DOCX)
Returns job data, resume data, ATS score, gap analysis, tailored resume, cover letter, and email draft.
The workflow consists of these nodes executed in sequence:
- fetch_job: Fetches job description from URL using web scraping
- parse_resume: Parses PDF/DOCX resume into structured data
- score_ats: Scores resume against job requirements
- analyze_gaps: Identifies keyword and skill gaps
- generate_resume: Generates ATS-optimized tailored resume (LLM)
- generate_cover_letter: Generates narrative cover letter (LLM)
- generate_email: Generates Gmail-ready email draft (LLM)
Located in backend/app/tools/:
- job_fetcher.py: Fetches and parses job descriptions from URLs
- resume_parser.py: Parses PDF and DOCX resumes
- ats_scorer.py: Scores resumes against job requirements for ATS compatibility
- models.py: Pydantic models for all tool inputs and outputs
Required in .env:
| Key | Description |
|---|---|
OPENROUTER_API_KEY / OPENAI_API_KEY |
At least one for AGENT_MODE=llm: OpenRouter for chat if set, else OPENAI_API_KEY (see .env.example). |
LANGFUSE_* |
Optional: Langfuse keys for API-side LLM observability (see .env.example). |
Pick what matches how you work day to day:
- Docker Desktop (or Docker Engine) plus the Compose plugin for the one-command local stack.
- uv for installing and running the Python service (lockfile-backed).
- Python 3.12 (
backend/.python-versionhelps pyenv/asdf) and Node.js 20+ for host-only development. - Terraform 1.6+ and the AWS CLI v2 when you touch AWS resources.
- An AWS account with credentials (
AWS_PROFILEor standard access keys) when running Terraform or deploy workflows locally.
Secrets stay out of git. Copy .env.example at the repository root to .env and fill in values for your machine. Deployed environments should load the same keys from your secret manager or GitHub Actions secrets/variables.
chmod +x scripts/*.sh # first clone only
cp .env.example .env # optional but recommended
./scripts/bootstrap-local.shStart each service in its own terminal:
cd backend
uv run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000cd frontend
npm run devOpen http://localhost:3000 for the UI and http://localhost:8000/docs for OpenAPI. The home page calls /api/v1/health so you can confirm both halves are talking.
Both FastAPI (pydantic-settings) and Next.js (via dotenv-cli in frontend/package.json scripts) read from the same .env at the repository root. Keep per-environment values out of source code—set them here locally, or inject them in CI/CD.
| Key | Consumers | Purpose |
|---|---|---|
API_HOST |
API | Bind address inside the container or host. |
API_PORT |
API | Port for Uvicorn when you run it manually. |
CORS_ORIGINS |
API | Comma-separated browser origins allowed to call the API. |
NEXT_PUBLIC_API_URL |
UI (build + browser) | Public API base URL the browser calls. Leave empty for production builds that should call same-origin /api/* through CloudFront. |
DEPLOYMENT_ENVIRONMENT |
API (/api/v1/health metadata) |
Optional label such as local, dev, staging, or prod. |
OPENROUTER_API_KEY |
API | OpenRouter key; used for chat when set (with LLM_BASE_URL for OpenRouter). |
OPENAI_API_KEY |
API | Used for chat when OPENROUTER_API_KEY is unset; optional when OpenRouter is set. |
LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY |
API | Optional. Langfuse project keys for LLM tracing; set with LANGFUSE_BASE_URL (or LANGFUSE_HOST) for EU/US/self-hosted. |
chmod +x scripts/*.sh # first clone only
cp .env.example .env # optional; run.sh exports it for Compose variable substitution
./scripts/run.sh- API on
http://localhost:8000 - UI on
http://localhost:3000(next devinside Compose — hot reload, not a productionnext build) - Why the first run can feel slow:
./scripts/run.shrunsdocker compose up --build. The backend image pulls base layers and runsuv synconce; the frontend image runsnpm ciduringdocker compose build. After that, Compose just starts the container; the UI isnext devfromfrontend/Dockerfile. To pick up new frontend dependencies after you editpackage-lock.json, rundocker compose build frontend(ordocker compose up --build) again.
Stop everything with ./scripts/stop.sh.
main.tf holds the terraform {} block (including the empty S3 backend "s3" {} stub), a locals name helper, and a numbered checklist for the architecture you are building toward. There are no resource blocks in this repository on purpose.
variables.tf / outputs.tf / providers.tf give you tagging defaults and a couple of outputs (stack_name, aws_region, next_steps) so CI and humans can sanity-check wiring before anyone adds modules or resources.
Nothing in the FastAPI or Next.js code hardcodes dev/staging/prod. Terraform’s environment variable is validated to those three values for tags and future state keys.
The terraform {} block declares an S3 backend (empty configuration). Every terraform init needs either:
terraform/backend.hcl(copyterraform/backend.hcl.example), orTALENTSTREAM_USE_LOCAL_TF_STATE=1with./scripts/deploy-aws.shfor disposable local state (not for teams).
cd terraform
cp terraform.tfvars.example terraform.tfvars
# set aws_region, project_name, environment (dev | staging | prod)From the repository root:
./scripts/deploy-aws.sh # writes a plan (no apply)
./scripts/deploy-aws.sh staging
TF_ENVIRONMENT=prod ./scripts/deploy-aws.sh./scripts/destroy-aws.sh is a thin wrapper around terraform destroy for when resources eventually exist.
Repository root TalentStreamAI/
TalentStreamAI/
├── backend/ # FastAPI API (uv / pyproject.toml)
│ ├── app/
│ │ ├── main.py # App factory + CORS + router mount
│ │ ├── api/ # HTTP routers (e.g. v1/health)
│ │ ├── core/ # Settings (reads repo-root .env)
│ │ └── services/
│ │ └── langgraph/ # Placeholder for agent graphs / tools
│ ├── Dockerfile
│ ├── pyproject.toml
│ └── uv.lock
├── frontend/ # Next.js 15 (App Router, static export)
│ ├── src/
│ │ ├── app/ # Routes, layouts, pages
│ │ └── lib/ # Shared helpers (e.g. API URL builder)
│ ├── public/ # Static assets for export
│ ├── Dockerfile # Local `next dev` in Docker (used by Compose)
│ ├── next.config.ts
│ └── package.json
├── terraform/ # AWS scaffold (no resources yet)
│ ├── main.tf # terraform {} + locals + architecture checklist
│ ├── variables.tf
│ ├── outputs.tf
│ ├── providers.tf
│ ├── backend.hcl.example
│ ├── terraform.tfvars.example
│ └── .terraform.lock.hcl
├── scripts/
│ ├── bootstrap-local.sh # uv sync + npm install
│ ├── run.sh / stop.sh # Docker Compose up / down
│ └── deploy-aws.sh / destroy-aws.sh # Terraform plan / destroy helpers
├── .github/
│ ├── workflows/ # CI + deploy placeholder workflows
│ └── aws/
│ └── github-oidc-trust-policy.json.example
├── docker-compose.yml # Local API (:8000) + Next dev server (:3000)
├── .env.example # Shared env template (copy to repo-root .env)
├── .gitignore
└── README.md
.github/workflows/ci.yml runs on pushes and pull requests to main but only checks out the repository and echoes that real CI is still to be defined (backend uv/tests, frontend npm/lint/build, Terraform fmt/validate, and so on).
.github/workflows/deploy-aws.yml is manual (workflow_dispatch) only and does not call AWS or Terraform. It prints a short checklist for when you add OIDC, secrets, terraform apply, image pushes, and static asset publishing.
For OIDC trust policy shaping, see .github/aws/github-oidc-trust-policy.json.example and replace ACCOUNT_ID, GITHUB_ORG, and REPO before attaching it to an IAM role.
- FastAPI routers: add packages under
backend/app/api/v1/(or new version folders) and include them frombackend/app/api/router.py. - Agents / LangGraph: keep graphs, tools, and state machines under
backend/app/services/langgraph/so onboarding stays predictable. - UI routes and data fetching: colocate routes in
frontend/src/app(static export friendly) and keep shared helpers infrontend/src/lib. - Infrastructure growth: add child modules under
terraform/modules/(or keep everything flat) and call them fromterraform/main.tfonce you outgrow the four-file scaffold.
cd frontend && npm run lint && npm run buildcd backend && uv sync && uv run python -m compileall -q appAdd pytest, Ruff, or mypy when the API surface grows; the scaffold stays intentionally small.
- UI shows “Backend not responding.” Confirm Uvicorn is listening on
8000,CORS_ORIGINSincludes your UI origin, andNEXT_PUBLIC_API_URLmatches how your browser reaches the API. docker composecannot reach Docker. Start Docker Desktop (macOS/Windows) or the Linux daemon, then rerun./scripts/run.sh.http://localhost:3000does nothing. Confirm the frontend container is up (docker compose ps) and checkdocker compose logs frontend. Ifdocker compose build frontendfails or the container exits with 137, raise Memory in Docker Desktop (Settings → Resources), then rebuild. Afterpackage-lock.jsonchanges, rundocker compose build frontend(or./scripts/run.shwith--build) again.- Terraform init asks for backend settings. Create
terraform/backend.hclor exportTALENTSTREAM_USE_LOCAL_TF_STATE=1for disposable local state. - GitHub Actions. The bundled workflows are placeholders only; there is nothing to “fix” for credentials until you replace them with real jobs.