Analyzes how well a resume matches a job posting using Gemini. It returns a score from 0 to 100, strengths, improvements and missing keywords, and it can rewrite the professional summary to focus on the job. Analyses are saved to a history.
Stack: Next.js 16 (App Router, Server Actions, Cache Components) · React 19 · Bulma 1 · Google Gemini (@google/genai) · Zod 4 · PostgreSQL + Prisma 7 · Vitest
- Match score: a 0–100 bar with ranges (low, medium and high).
- Structured feedback: summary, strengths, improvements and missing keywords.
- Summary rewrite: generates a new professional summary using only facts from the resume, with a copy button.
- History: lists the last 50 analyses and lets you open or delete each one.
- File import: loads
.txt/.mdfiles directly in the browser. - English and Portuguese: English by default (
/redirects to/en) and an EN/PT switch that keeps the current page. AI feedback is written in the interface language. The rewritten summary is written in the resume's language, since it goes into the resume.
Routes live under app/[lang], and proxy.ts redirects paths without a language prefix. Texts are in lib/i18n.ts, where the Portuguese dictionary is typed from the English one, so a missing key breaks the build. Server Actions return error codes (ErrorCode) instead of sentences, and the interface translates them.
The same Zod schema (lib/schemas.ts) does two jobs:
- It is converted to JSON Schema (
z.toJSONSchema) and sent to Gemini viaresponseJsonSchema, which constrains the model's output. - It validates the response in
parseModelJson. Invalid JSON, a score outside 0–100 or oversized lists become a typed error (AiError("invalid_output")) with a friendly message. Nothing outside the contract reaches the database or the screen.
Stored JSON is also revalidated on read, and the database has a CHECK constraint enforcing score BETWEEN 0 AND 100.
- Signed anonymous session: an
HttpOnly+SameSite=Laxcookie (Securein production) holding a UUID signed with HMAC-SHA256 and compared withtimingSafeEqual. History is isolated per session and every query filters bysessionId, so an analysis id alone does not grant access to it (IDOR protection). - Rate limiting persisted in Postgres: works across serverless instances. Limits per session (3/h), per IP (3/h, with the IP stored only as an HMAC) and a global daily cap (500) to control cost. All values are configurable through environment variables.
- Server-side validation: every Server Action validates its input with Zod and caps text length. Origin-based CSRF protection comes built into Server Actions.
- Prompt injection: the resume and job posting are delimited as data, and the model is instructed not to follow instructions inside them. Output is restricted to the schema and rendered as text (React escapes it).
- Privacy: resume text never goes to the logs. The Gemini key exists only on the server, and users can delete their analyses.
- Security headers:
X-Frame-Options,nosniff,Referrer-Policy,Permissions-Policyand HSTS.
Requirements: Node 20+ and PostgreSQL.
npm install
cp .env.example .env # fill in DATABASE_URL, GEMINI_API_KEY and SESSION_SECRET
npm run db:migrate
npm run devGEMINI_API_KEY: create one in Google AI Studio.SESSION_SECRET:openssl rand -base64 32.GEMINI_MODEL: defaults togemini-flash-latest.
npm testThey cover the LLM contract (invalid JSON, out-of-range score, missing fields), input validation, error-code mapping and the session signature (wrong secret, swapped id, malformed tokens).
- Create a Postgres database on Neon, Supabase or Render.
- Import the repository into Vercel and set the variables from
.env.example. - Run migrations with
npm run db:deploy, which can be part of the build command (npm run db:deploy && next build).
postinstall runs prisma generate.
- The rate limiter counts and records events in two steps, so simultaneous bursts can exceed the limit by 1–2 calls. For exact control, use Redis (Upstash).
- The anonymous session is lost when cookies are cleared. User accounts (Auth.js) would be the next step.
- There is no PDF import and no response streaming.
- There is no automatic retention policy: analyses stay saved until the user deletes them.