AI-powered resume analysis built with a FastAPI backend, a Streamlit frontend, and Azure OpenAI (GPT-5 Mini). It scores resumes, compares them against job descriptions, and generates tailored rewrites. It's hosted on Azure App Service.
"Generate Tailored Resume" rewrites your resume for a job description and returns it as structured data. The app renders that into a Markdown preview and into ten self-contained LaTeX templates (Jake's Resume, four moderncv styles, and five built-in layouts). Pick one in the UI and download the .tex; compile it in Overleaf or with pdflatex / xelatex / lualatex.
- Nothing is compiled on the server. The model never writes LaTeX: our code fills the templates and escapes every character, so a resume containing
\input{...}or\write18{...}comes out as plain text. - Templates live in
api/services/latex_templates.py. Jake's Resume is MIT-licensed (Jake Gutierrez, based on sb2nov/resume); the moderncv templates use themoderncvclass (LPPL) from TeX Live. - To test compilation locally, install Tectonic (
brew install tectonic);tests/test_latex_templates.pythen compiles every template. Without it that test is skipped.
Browser ──▶ proxy.py :8000 ──▶ Streamlit 127.0.0.1:8001 ──▶ FastAPI 127.0.0.1:8080 ──▶ Azure OpenAI
(public entry, verifies Clerk session)
- Authentication: Clerk. The app stores no users or passwords. Anonymous visitors are sent to
/auth/sign-in(clerk-js);proxy.pyverifies Clerk's session JWT against its JWKS and passes the user to Streamlit asX-Auth-*headers. Streamlit forwards that identity to FastAPI as a 5-minute HS256 token signed withJWT_SECRET. Seeclerk_auth.py. - Frontend: Streamlit. Backend: FastAPI (PDF parsing with PyMuPDF, per-user rate limiting).
- Hosting / CI: Azure App Service (Linux). GitHub Actions deploys on every push to
master.
Requires Python 3.9–3.11.
git clone https://github.com/<your-username>/AnalyzeMyCV.git
cd AnalyzeMyCV
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # then fill in the values
./entrypoint.sh # open http://localhost:8000With CLERK_PUBLISHABLE_KEY empty, the app signs you in as LOCAL_DEV_USER_EMAIL (ignored on App Service). Set the key (and JWT_SECRET) to test real Clerk sign-in locally. Without Azure OpenAI credentials, the API returns mock results.
- Clerk: create an application at dashboard.clerk.com. A development instance works on
*.azurewebsites.netbut shows a dev banner; a production instance requires a custom domain you own, with the DNS records Clerk lists; it cannot run on*.azurewebsites.net. The proxy warns at startup if the Frontend API host does not resolve. Once the production instance works, setCLERK_PUBLISHABLE_KEY,CLERK_SECRET_KEYandCLERK_AUTHORIZED_PARTIESin App Service, and add thename/emailsession claims to it (clerk config patch --instance prod). - Environment variables: set the following in App Service. For local runs,
.env.localoverrides.env(both git-ignored).AZURE_OPENAI_API_KEY,AZURE_OPENAI_ENDPOINT,AZURE_OPENAI_DEPLOYMENT_NAME(gpt-5-mini)JWT_SECRET: at least 32 random characters (python -c "import secrets; print(secrets.token_urlsafe(48))")CLERK_PUBLISHABLE_KEY, and optionallyCLERK_SECRET_KEY(email/name lookup) andCLERK_AUTHORIZED_PARTIESSCM_DO_BUILD_DURING_DEPLOYMENT=true
- Turn on HTTPS Only and Web sockets (Streamlit needs them). Do not enable App Service Authentication; Clerk replaces it.
- Startup command:
chmod +x ./entrypoint.sh && ./entrypoint.sh. The GitHub workflow also sets this. - Verify: the startup log should show
Proxy: Clerk authentication enabled: True, and an anonymous request should redirect to/auth/sign-in.
Password reset and email verification are handled by Clerk. To disable a user, ban them in the Clerk dashboard; it applies within an hour (the proxy session lifetime, SESSION_TTL_SECONDS, default 3600).
- Identity:
X-Auth-*headers are set only by the proxy after it verifies the Clerk session; client-sent copies are dropped. If Clerk is misconfigured the proxy returns 503 instead of serving the app. Session cookies are never forwarded to Streamlit. - Request hardening (
proxy.py): paths with dot segments, encoded slashes or double encoding are rejected (they could make the public-path rule and Streamlit disagree about the route); the original encoded path is forwarded untouched; WebSocket and state-changing requests whoseOriginis another site are refused (setALLOWED_HOSTSif your host rewrites theHostheader); onlyGET/HEADcan use the public paths (/static/, health). - Recommended in production: set
CLERK_AUTHORIZED_PARTIESto your site's origin(s) so session tokens minted for any other origin are rejected.SESSION_TTL_SECONDS(default 3600) is how long a Clerk ban can take to apply. - Model output is untrusted: the analysis report has images stripped and links flattened to
text (url)(an image URL carrying resume text would otherwise be fetched by the browser with no click); tailored resumes are validated data, Markdown-escaped, and LaTeX-escaped (see above). - FastAPI and Streamlit listen only on
127.0.0.1.proxy.pyis the only public listener, and every response it sends carries anti-framing,nosniff, referrer and HSTS headers. - Uploads are capped at 8 MB, 15 pages and 50,000 characters. The LLM endpoints allow one request per user every 5 minutes, shared across both. Error responses never include exception details. Resume content is never logged.
- GitHub Actions are pinned to commit hashes; update them deliberately. Dependencies are not pinned, so run a dependency audit (for example
pip-audit -r requirements.txt) regularly. - Keep secrets in App Service settings or Key Vault, never in git. Rotate
JWT_SECRET, the Clerk secret key and the Azure OpenAI key if they're exposed.