A free, self-hostable alternative to AI resume SaaS like Rezi, Jobscan, Teal, Kickresume, Zety and friends — an AI resume builder + ATS checker, tailored cover letters, resignation letters, interview prep, a live remote-job board, an application tracker, and an always-on AI career copilot. One codebase, your infrastructure, your AI key.
Features · How it works · Self-hosting · Environment variables · Data model · Contributing
History: JobTools ran in production as the SaaS at
jobtools.cofrom 2024 to 2026. In July 2026 the hosted service was retired and the entire codebase was open-sourced under the MIT license. There is no hosted version anymore — this repo is now a self-host-first project.
Commercial AI resume tools (Rezi, Jobscan, Teal, Kickresume, Zety, Enhancv, resume.io, …) are subscription products: your documents live in their cloud, AI usage is metered by their plans, and the "ATS report" logic is a black box. JobTools is the same product category with the opposite posture:
| JobTools (this repo) | Typical AI resume SaaS | |
|---|---|---|
| Price | Free, MIT-licensed | Subscription / credit packs |
| Your data | Your own Firebase project (+ localStorage) | Vendor's cloud |
| AI | Bring your own OpenRouter key, server-side proxy | Bundled, metered |
| AI cost control | Built-in per-user rate limits you can tune | Plan limits |
| Scoring & prompts | All prompts and heuristics readable in src/services/ |
Black box |
| Extensible | Fork it, PR it | Feature requests |
The repo even ships the comparison research: 11 data-driven competitor pages (/compare/jobtools-vs-rezi, -vs-jobscan, …) live in src/data/comparisons.ts.
📄 Documents
- AI resume builder (
/resume-builder) — build from scratch or upload a PDF; AI extracts and structures your experience. - ATS checker & deep analysis (
/resume-analysis) — section/skill extraction, resume↔job-description match scoring, per-ATS-system compatibility checks, and concrete fixes (src/services/{advancedResumeService,atsOptimizationService,jobDescriptionService}.ts). - Resume tailor (
/resume-tailor) — rewrites your resume against a pasted job description, with one-click handoff from the job board. - Side-by-side resume editor (
/resume-editor) and 4 resume templates (/resume-templates). - AI cover letter generator (
/cover-letter) and resignation letter builder (/resignation-letter).
🔎 Job search suite
- Live job board (
/job-search) — aggregates four keyless public APIs in parallel (Remotive, Arbeitnow, Jobicy, The Muse), dedupes, and scores every job against your profile. No API keys needed. - Discover (
/discover) — swipe-style yes/no triage of scored roles. - Application tracker (
/applications) — kanban acrosssaved → applied → interviewing → offer → rejected. - Insider connections (
/connections) — AI suggests who to reach at a target company (recruiter / hiring manager / future teammate) and drafts the outreach. - Insights (
/insights) — response rate, match quality, and profile-strength funnel.
🎤 Interview prep
- AI interview coach (
/interview-coach) — mock interviews with written feedback. - Role-specific question banks (
/interview-questions/:role).
🤖 Career copilot
- Full-page chat (
/copilot) plus a floating widget on every page; shares state across tabs (src/services/jobtools/copilotChat.ts).
📈 Built-in programmatic SEO engine — everything is statically authored and data-driven, useful as a reference even outside job tools:
- 44 cover-letter role pages (
src/data/coverLetterRoles.ts) - 27 interview-question role pages (
src/data/interviewRoles.ts) - 12 resignation-situation pages (
src/data/resignationSituations.ts) - 11 competitor comparison pages + hub (
src/data/comparisons.ts) - 21 blog posts with AI-generated editorial illustrations (
src/blog/posts.ts) - JSON-LD builders (FAQ / SoftwareApplication / Breadcrumb / Organization) in
src/lib/seoSchema.ts, per-page meta viasrc/components/SEO.tsx,sitemap.xml+robots.txtinpublic/.
Signed-out users can use every tool with deterministic non-AI fallbacks; signing in (Google) unlocks the AI features and cloud sync.
flowchart LR
SPA["React 18 SPA<br/>(Vite, Tailwind)"] -- "Firebase ID token" --> FN["POST /api/ai<br/>Vercel serverless function"]
FN -- "verify token" --> AUTH[("Firebase Auth<br/>Google sign-in")]
FN -- "rate limit<br/>20/min · 300/day" --> RL[("Firestore<br/>rateLimits/{uid}")]
FN -- "chat completion" --> OR["OpenRouter<br/>openrouter/auto"]
SPA -- "owner-scoped CRUD" --> FS[("Firestore<br/>users/{uid}/…")]
SPA -- "file uploads" --> ST[("Firebase Storage")]
- The browser never holds an AI key. All AI calls go through a single auth-gated serverless function,
api/ai.ts: it verifies the caller's Firebase ID token with the Admin SDK, enforces per-user rate limits in a Firestore transaction, then proxies to OpenRouter'sopenrouter/automodel (override withOPENROUTER_MODEL). Guards: max 40 messages, 24,000 prompt chars, 2,000 output tokens per request. - localStorage-first storage. Your work is saved locally first and mirrored best-effort to Firestore under
users/{uid}/…; guest work is merged into your account on first sign-in (src/services/jobtools/storage.ts,src/context/AuthContext.tsx). - Owner-only security rules.
firestore.rulesandstorage.ruleslock every document and file to its owner; therateLimitscollection is server-only.
React 18 + TypeScript 5.5 · Vite 5 · Tailwind CSS 3.4 (+ shadcn-style primitives with Radix, CVA, tailwind-merge) · react-router 6 · framer-motion · lucide-react · pdfjs-dist / react-pdf for PDF parsing & preview · Firebase (Auth, Firestore, Storage) · one Vercel serverless function (@vercel/node) · OpenRouter for AI · Vercel Analytics. No Redux/state library — React context plus useSyncExternalStore over localStorage.
- Node.js 20+
- A Firebase project (free Spark tier is enough for auth + Firestore)
- An OpenRouter API key (the only thing that costs money — pay-per-token)
- Optional: Vercel CLI (
npm i -g vercel) to run the AI function locally and to deploy
git clone https://github.com/dineshxr/jobtools.git
cd jobtools
npm installIn the Firebase console:
- Create a project and add a Web app (Project settings → Your apps) — this gives you the
VITE_FIREBASE_*config values. - Authentication → Sign-in method → Google → Enable (set a support email). Under Settings → Authorized domains add
localhostand your production domain. - Firestore Database → Create database (production mode).
- Optional — Storage → Get started (needs the Blaze pay-as-you-go plan; only used for document uploads).
- Project settings → Service accounts → Generate new private key. From the JSON you need
project_id,client_email, andprivate_key(server-side vars below).
Then deploy the security rules from the repo root:
npx firebase-tools deploy --only firestore:rules,storage --project <your-project-id>cp .env.example .env # client config (VITE_FIREBASE_*) — safe-to-expose identifiers
cp .env.example .env.local # server config (OPENROUTER_API_KEY, FIREBASE_* admin vars)Fill .env with your Firebase web config and .env.local with the server-side values (used by vercel dev). No env file is committed; src/lib/firebase.ts refuses to start without your own config, so a fork can never silently point at someone else's backend.
npm run dev # frontend only — AI calls are disabled (tools fall back to non-AI mode)
vercel dev # frontend + /api/ai — full AI features locallynpm run build + npm run preview builds and serves the production bundle; npm run lint runs ESLint.
Any static host + serverless runtime works, but the repo is Vercel-shaped out of the box (vercel.json provides the SPA fallback rewrite; api/ai.ts is a Vercel function). Set all environment variables from the table below in your Vercel project — including the VITE_* ones, since env files aren't committed and Vite needs them at build time. Remember to add your production domain to Firebase's authorized domains.
| Variable | Side | Required | Purpose |
|---|---|---|---|
VITE_FIREBASE_API_KEY |
client | ✅ | Firebase web config (public identifier, not a secret) |
VITE_FIREBASE_AUTH_DOMAIN |
client | ✅ | Firebase web config |
VITE_FIREBASE_PROJECT_ID |
client | ✅ | Firebase web config |
VITE_FIREBASE_STORAGE_BUCKET |
client | ✅ | Firebase web config |
VITE_FIREBASE_MESSAGING_SENDER_ID |
client | ✅ | Firebase web config |
VITE_FIREBASE_APP_ID |
client | ✅ | Firebase web config |
OPENROUTER_API_KEY |
server | ✅ for AI | OpenRouter key used by /api/ai — the only billed secret |
FIREBASE_PROJECT_ID |
server | ✅ for AI | Admin SDK service account (token verification + rate limits) |
FIREBASE_CLIENT_EMAIL |
server | ✅ for AI | Admin SDK service account |
FIREBASE_PRIVATE_KEY |
server | ✅ for AI | Admin SDK private key — keep the quotes and \n escapes |
OPENROUTER_MODEL |
server | optional | Model override; defaults to openrouter/auto |
SITE_URL |
server | optional | Sent to OpenRouter as HTTP-Referer attribution |
Server variables must never be prefixed with VITE_ — that would bake them into the public bundle.
All user data is namespaced under the signed-in user:
| Firestore path | Used by |
|---|---|
users/{uid}/resumes/{id} |
resume builder / editor |
users/{uid}/coverLetters/{id} |
cover letter generator |
users/{uid}/resignationLetters/{id} |
resignation builder |
users/{uid}/documents/{id} |
document uploads (files in Storage at users/{uid}/documents/…) |
users/{uid}/applications/{id} |
application tracker |
users/{uid}/profile/main, users/{uid}/settings/main |
job-seeker profile & preferences |
rateLimits/{uid} |
AI rate limiting — server-only (clients denied by rules) |
Cost controls for self-hosters: /api/ai enforces 20 requests/min and 300/day per user (constants at the top of api/ai.ts), caps prompt size and output tokens, and requires sign-in — so anonymous traffic can never spend your OpenRouter credits. Note that the rate limiter fails open if Firestore is unreachable (a deliberate availability trade-off — tighten it if you need a hard guarantee).
api/ai.ts # the single serverless function: auth-gated AI proxy
src/
App.tsx # all routes
pages/ # 30+ route components (tools, SEO families, marketing)
components/ # shared UI (landing/, jobtools/ shell & copilot, ui/ primitives)
services/ # domain logic: resume/ATS/cover-letter/JD analysis + AI calls
jobtools/ # job board, tracker, profile, match scoring, copilot state
data/ # programmatic SEO content (roles, situations, comparisons)
blog/posts.ts # 21 posts, inline HTML
lib/ # firebase.ts, aiClient.ts, seoSchema.ts, utils.ts
context/AuthContext.tsx
public/ # static assets, sitemap.xml, robots.txt, templates, blog art
firestore.rules · storage.rules · firebase.json · vercel.json
GOAL.md # product philosophy: every page must produce a usable artifact
A few things are branded for the original deployment and worth changing in a fork:
src/lib/seoSchema.ts(SITE_URL),public/sitemap.xml,public/robots.txt, and the IndexNow key file inpublic/— regenerate for your domain.support@jobtools.coappears on the contact/privacy/terms pages.src/data/comparisons.tsand several blog posts make comparative claims about commercial products as of 2026 — review before republishing them on your own domain.
Honest list of what's missing: no test suite or CI yet; the resume editor's undo/redo/save toolbar has stub TODOs (src/components/ResumeEditor.tsx); src/services/openaiService.ts is legacy-named (it talks to OpenRouter); a couple of unrouted legacy pages linger in src/pages/. PRs for any of these are very welcome.
Issues and PRs are welcome. Before opening a PR:
- Read
GOAL.md— the product bar: every page must be reachable, produce a real artifact, persist work, and hand off to a next step. npm run lintmust pass.- Keep the security posture: AI keys stay server-side, user data stays owner-scoped.
Built by Dinesh Puppala as jobtools.co; open-sourced in July 2026. Blog and site illustrations were AI-generated via OpenRouter (prompts and generation records preserved in public/images/*/manifest.jsonl).
Rezi, Jobscan, Teal, Kickresume, Zety, Enhancv, resume.io and other product names are trademarks of their respective owners; JobTools is not affiliated with or endorsed by any of them.
MIT — do whatever helps someone land a job.



