Stop manually tweaking your CV. Let AI do it.
This is an open-source, AI-powered workbench that helps you land your dream job by automating the tedious parts of the application process. It uses Google's Gemini AI to tailor your CV, write compelling cover letters, and draft personalized cold emails based on job descriptions.
Why use this?
- Tailored CVs: Automatically rewrites your CV to match specific job descriptions using LaTeX for professional formatting.
- Smart Cover Letters: Generates cover letters that actually make sense and reference your relevant experience.
- Cold Outreach: Drafts personalized emails to recruiters and hiring managers.
- Contact Finding: (Optional) Integrates with Apollo.io to find verified email addresses for key contacts.
- Privacy Focused: You host it yourself. Your data stays with you (and Google/Firebase/Apollo, but not a third-party SaaS).
- CV Customization: Upload your "Master CV" and a job description, and get a perfectly tailored PDF.
- Cover Letter Generator: Creates matching cover letters in seconds.
- Cold Email Drafts: Generates outreach emails based on the company and role.
- Research Briefs: AI analyzes the company and role to give you talking points for interviews.
- Quota System: Built-in usage limits to manage API costs.
Before you start, make sure you have the following installed on your computer:
- Docker: Download Here
- Git: Download Here
Follow these steps to get the project running on your local machine using Docker.
Open your terminal (Command Prompt, PowerShell, or Terminal) and run:
git clone https://github.com/ebenezer-isaac/job-hunt.email.git
cd job-hunt.emailThis project needs secrets (API keys) to work.
- Copy the example environment file:
cp .env.example .env.local # On Windows PowerShell: copy .env.example .env.local - Open
.env.localin a text editor (like VS Code or Notepad). You will need to fill in the blanks. Deployments: copy the same template to.env.buildso Cloud Run/build scripts read identical values.
This app uses Firebase for logging in and saving your data.
- Go to the Firebase Console and click "Add project". Give it a name.
- Enable Authentication:
- Go to Build > Authentication in the sidebar.
- Click "Get started".
- Select Google as a Sign-in provider, enable it, and save.
- Enable Firestore Database:
- Go to Build > Firestore Database.
- Click "Create database".
- Select Production mode (we have rules to secure it).
- Choose a location near you.
- Enable Storage:
- Go to Build > Storage.
- Click "Get started".
- Start in Production mode.
- Get Admin Keys (for the server):
- Click the Gear icon (Project Settings) > Service accounts.
- Click "Generate new private key". This downloads a JSON file.
- Open the JSON file. Copy the values to your
.env.local:FIREBASE_PROJECT_ID->project_idFIREBASE_CLIENT_EMAIL->client_emailFIREBASE_PRIVATE_KEY->private_key(Copy the whole string including-----BEGIN PRIVATE KEY...)
- Get Client Keys (for the browser):
- Go to Project Settings > General.
- Scroll down to "Your apps" and click the Web (</>) icon.
- Register the app (name it whatever you want).
- Copy the config values to your
.env.local:NEXT_PUBLIC_FIREBASE_API_KEY->apiKeyNEXT_PUBLIC_FIREBASE_AUTH_DOMAIN->authDomainNEXT_PUBLIC_FIREBASE_PROJECT_ID->projectIdFIREBASE_STORAGE_BUCKET->storageBucket
- Go to Google AI Studio.
- Click "Get API key".
- Click "Create API key in new project".
- Copy the key and paste it into
.env.localasGEMINI_API_KEY.
Required only if you want to find email addresses for cold outreach.
- Go to Apollo.io and sign up.
- Go to Settings > Integrations > API.
- Click "Create New Key".
- Paste it into
.env.localasAPOLLO_API_KEY. Note: CV customization and cover letters work fine without this.
You need a few secure random strings for auth + Server Actions:
- Internal token (48-byte base64) for
ACCESS_CONTROL_INTERNAL_TOKEN:
node -e "console.log(require('crypto').randomBytes(48).toString('base64'))"- Cookie signatures (generate twice, comma-separate both outputs) for
FIREBASE_AUTH_COOKIE_SIGNATURE_KEYS:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"- Deterministic Server Action key (32-byte base64) for
NEXT_SERVER_ACTIONS_ENCRYPTION_KEYso multi-instance deployments stop throwingfailed-to-find-server-action:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"To avoid manually editing the database to allow yourself in, set your email as the admin.
- In
.env.local, setADMIN_EMAIL=your.email@gmail.com. - This gives you instant access and a higher usage quota (1000 tokens).
Build the Docker image:
docker build -t job-hunt-app .Run the container:
docker run -p 8080:8080 --env-file .env.local job-hunt-appOpen http://localhost:8080 in your browser.
The app is "invite-only" by default. If you didn't set ADMIN_EMAIL, or want to invite others:
- Go to your Firebase Console > Firestore Database.
- Navigate to
app_config>security>accessControl>config. - Edit the document. Add the email to the
allowedEmailsarray field.
- Settings: Go to the Settings tab. Upload your "Original CV" (LaTeX format) and fill in your "Extensive CV" (a text dump of everything you've ever done).
- Need to create a LaTeX CV? Use Resumake.io - a free online tool to build professional resumes in LaTeX format.
- Want to edit your LaTeX CV? Use Overleaf for online LaTeX editing with real-time preview.
- Sample Template: Check out this Sample LaTeX CV on Overleaf to see a compatible structure.
- New Session: Go to the home page. Paste a Job Description URL or text.
- Generate: Click "Generate". The AI will research the company, rewrite your CV, and draft a cover letter.
- Download: Once done, you can download the PDF or copy the text.
For developers, architects, and contributors.
This project is a serverless, stateless Next.js application built with a hexagonal (ports & adapters) architecture. It runs on the Next.js App Router (v15) so the UI, Server Actions, and streaming responses share a single code path, while long-lived state lives in Firebase and heavy compute (AI inference) happens inside Google Gemini. External dependencies are decoupled behind port interfaces (src/core/ports/), with concrete adapters (src/adapters/) wired through a tsyringe DI container (src/di/).
- Framework: Next.js 15 App Router with Server Actions + streaming responses.
- Runtime: Node.js 20-compatible container (Docker/Cloud Run ready).
- Database: Firebase Firestore for sessions, quotas, and configuration.
- Object Storage: Firebase Storage for generated PDFs and structured artifacts.
- Authentication: Firebase Auth +
next-firebase-auth-edge, backed by an allowlist service. - AI Inference: Google Gemini 3 Pro (reasoning) + Gemini 2.5 Flash (fast path) via
@google/generative-ai. - Document Engine: Hardened LaTeX (
pdflatex) sandbox with PDF parsing + heuristics. - Observability: Structured logs mirrored to stdout and Firestore with enforced redaction.
flowchart LR
Client[Client Browser] <-->|Streaming RPC| Actions[Server Actions]
subgraph "Serverless Container (Cloud Run)"
Actions -->|1. Validate & Hold Quota| Security[Security Module]
Actions -->|2. Prompt Engineering| AI[AI Service]
Actions -->|4. Compile & Sanitize| Latex[LaTeX Sandbox]
end
subgraph "External Services"
AI <-->|Inference| Gemini[Google Gemini API]
AI <-->|Enrichment| Apollo[Apollo.io API]
end
subgraph "Persistence Layer"
Security <-->|Read/Write| Firestore[(Firestore)]
Latex -->|Upload| Storage[(Firebase Storage)]
end
Actions -->|3. Stream Updates| Client
Each PDF is rendered inside a throwaway workspace with explicit allow/deny lists.
- Sandboxed compilation: Every run writes sources to
/tmp/cv-latex-*, invokespdflatextwice, then recursively deletes the directory. - Exploit filtering: Inputs are scanned for
\write18,\openout, parent-directory includes, and suspicious packages before the compiler runs. - Failure hygiene: When LaTeX errors occur, the logs/stdout/stderr are hashed (not stored raw) so observability remains useful without leaking governed content.
- PDF verification:
pdf-parseenforces the configured page target. Mismatch? The workflow loops with corrective instructions to Gemini.
src/adapters/ai/gemini/prompts.json defines a layered contract so Gemini emits deterministic LaTeX/JSON.
- Level 1 (Format): Forces pure LaTeX/JSON (no Markdown fences, no chatter).
- Level 2 (Structure): Dictates sections, ordering, bullet limits, and relational data such as STAR stories.
- Level 3 (Content): Applies personas and factual constraints so each section references real experience.
- Payload ceilings:
normalizeFormDatameasures every submission byte-for-byte and emitsFormPayloadTooLargeErroronce the multi-field payload exceeds ~5×MAX_CONTENT_LENGTH. - Field-level limits:
formSchemacaps each textarea (job description, CVs, strategies, contact info) with descriptive Zod errors so users know what to trim. - Storage sanitation:
SessionRepositoryroutes everything throughsanitizeForStorage, clamps metadata strings toMAX_CONTENT_LENGTH, prunes undefined values, and redacts sensitive payloads before persisting chat history.
- Optimistic holds:
quotaService.placeHoldissues a request-scoped hold (1 token by default) before generation starts, keeping global usage fair. - Processing state: Sessions record
processingStartedAt, deadlines, andactiveHoldKey, enabling dashboards to show live progress and ensuring failed runs clean up after themselves. - Streaming UX:
generateDocumentsActionwires a readable stream to the client, emitting incremental status updates while the workflow orchestrates RAG, CV rewriting, cover letters, and cold emails.
- Context propagation:
registerRequestLogContext+runWithRequestIdContexttie every log line, Firestore entry, and streamed event back to a singlex-request-id. - Redaction first:
sanitizeForLoggingtrims/labels sensitive data before it ever reaches stdout or Firestore. - Audit trails:
scheduleChatLogandscheduleUsageLogappend structured events to each session so users (and support) can see what happened without combing raw logs.
- Task-aware models: Fast parsing/labeling tasks hit Gemini Flash, while long-form writing and multi-document reasoning go to Gemini 3 Pro.
- Retry policy: Automatic exponential backoff shields long sessions from transient 429/503 errors, and falls back to alternate models if necessary.
- Internal token required: Every POST must include the
x-internal-tokenheader that matchesACCESS_CONTROL_INTERNAL_TOKEN. Regular authenticated tenants can no longer hit this surface. - Payload guardrails: Requests larger than ~16KB are rejected upfront with
413, and the body is streamed/terminated without reaching Firestore. - Per-minute throttling: Each caller (keyed by
x-log-client, request id, or IP) can write up to 120 entries/minute before receiving a429. - Data hygiene:
message/scopelengths are validated, log data is sanitized viasanitizeForLogging(1KB string cap), nested structures are truncated, and only the scrubbed payload is handed to the shared stdout/Firestore writer. - Operational impact: Update any custom log forwarders or cron jobs to send
x-internal-token+ a stablex-log-clientvalue so the rate limiter buckets traffic predictably.
Each module has its own README inside the module folder. Miscellaneous docs live in docs/.
| Module | README | Description |
|---|---|---|
| AI (Gemini) | src/adapters/ai/gemini/README.md |
Gemini client, prompts, model routing, LlamaIndex RAG |
| Document (LaTeX) | src/adapters/document/latex/README.md |
LaTeX sandbox, PDF compilation, resume templates |
| Storage (Firebase) | src/adapters/storage/firebase/README.md |
Firebase Storage abstractions |
| Contact (Apollo) | src/adapters/contact/apollo/README.md |
Apollo.io integration, disambiguation |
| Module | README | Description |
|---|---|---|
| Port Interfaces | src/core/ports/README.md |
Hexagonal architecture boundary contracts |
| DI Container | src/di/README.md |
tsyringe container and injection tokens |
| Module | README | Description |
|---|---|---|
| Authentication | src/lib/auth/README.md |
Firebase Auth, session management |
| Logging | src/lib/logging/README.md |
Request-scoped logging, audit trails |
| Security | src/lib/security/README.md |
Quota management, allowlist, internal tokens |
| Module | README | Description |
|---|---|---|
| Server Actions | src/app/actions/README.md |
Domain-grouped server actions |
| Document | Description |
|---|---|
docs/IMPLEMENTATION.md |
Development standards, golden rules, workflow |
docs/COMMON_ERRORS.md |
Forbidden patterns and error codes |
src/
├── core/
│ └── ports/ # Port interfaces (hexagonal architecture boundaries)
├── di/ # tsyringe DI container + injection tokens
├── adapters/
│ ├── ai/gemini/ # Gemini AI adapter
│ ├── contact/apollo/ # Apollo.io contact adapter
│ ├── document/latex/ # LaTeX document adapter
│ └── storage/firebase/ # Firebase Storage adapter
├── app/
│ ├── actions/ # Server Actions grouped by domain
│ │ ├── generate/ # CV/cover letter/cold email generation
│ │ ├── session/ # Session CRUD
│ │ ├── upload/ # Resume/job ingestion
│ │ ├── content/ # Save/delete content
│ │ └── logging/ # Append logs
│ └── api/ # REST endpoints
├── lib/
│ ├── auth/ # Authentication & session
│ ├── firebase/ # Firebase admin & client SDKs
│ ├── logging/ # Structured logging & audit
│ └── security/ # Quota & allowlist
├── components/ # React UI components
├── hooks/ # Custom React hooks
├── store/ # Zustand state stores
└── scripts/ # DevOps & maintenance utilities
| File | Purpose |
|---|---|
.env.example |
Source of truth. Safe to commit; contains comments + placeholders. |
.env.local |
Local development + test scripts. Copy from the example and keep it private. |
.env.build |
Deployment/CI inputs. scripts/build-env-secrets.ts sanitizes this file and turns the values into secret payloads for Cloud Run/GCP. |
Tip: Keep
.env.localand.env.buildin sync. The only difference should be production URLs or stricter cookie flags.
| Command | What it does |
|---|---|
npm run dev |
Boots the tailored dev server (scripts/dev.ts): seeds the local vector store when persistence is enabled, then runs next dev. |
npm run dev:nodemon |
Starts nodemon with nodemon.json so backend code reloads without the dev bootstrap. |
npm run build |
Runs next build to produce the production bundle; requires all mandatory env vars (including NEXT_SERVER_ACTIONS_ENCRYPTION_KEY). |
npm run start |
Serves the already-built app with next start (use the same env that was present during the build). |
npm run lint |
Executes ESLint across the repo. |
npm run test |
Runs the Vitest suite in run mode. |
npm run seed:vector-store |
Manually invokes the vector-store seeding workflow. |
npm run deploy |
Runs scripts/deploy.ts, which syncs .env.build secrets, builds via Cloud Build, and deploys the Cloud Run service. |
npm run help |
Prints the curated command reference shown below directly to the terminal. |
Run npm run help whenever you need a quick, always-up-to-date reminder of the available scripts.
| Command | Purpose | Notes |
|---|---|---|
npx tsx scripts/build-env-secrets.ts |
Sanitizes .env.build, writes .env.build.clean, and produces tmp/secrets/*.txt plus tmp/upload-secrets.ps1. |
Run before rotating secrets so pwsh tmp/upload-secrets.ps1 can push them to Secret Manager. |
npx tsx scripts/delete-all-secrets.ts |
Deletes all secrets in the active GCP project. | Destructive; useful when resetting a sandbox. |
npx tsx scripts/seed-vector-store.ts |
Seeds the recon strategy document into the persisted LlamaIndex store. | LLAMAINDEX_ENABLE_PERSISTENCE must be true. |
npx tsx scripts/export-prompts.ts |
Regenerates src/prompts.json from src/lib/ai/prompts.ts with metadata. |
Keeps the prompt catalog in sync after edits. |
npx tsx scripts/expire-processing.ts |
Marks stuck sessions (past processingDeadline) as failed and releases their quota holds. |
Safe to run as a cron/Cloud Scheduler task. |
npx tsx scripts/clear-firestore-logs.ts --force |
Deletes Firestore log documents in batches. | Requires --force (or -y) to avoid accidental wipes. |
npx tsx scripts/dump-firestore-logs.ts |
Prints the most recent Firestore log entries to stdout. | Respects FIREBASE_LOG_COLLECTION and FIREBASE_LOG_FETCH_LIMIT. |
npx tsx scripts/render-firestore-log-viewer.ts --limit=500 |
Renders tmp/firebase-log-viewer.html, a searchable DataTables UI for logs. |
Adjust --limit to trade off size vs. detail. |
npx tsx scripts/test-firestore-logging.ts |
Writes an INFO log entry into the appLogs collection. |
Handy for verifying local credentials. |
npx tsx scripts/test-gcp-logging.ts |
Same payload as above, intended for Cloud Run/GCP smoke tests. | Run within the deployed environment to confirm IAM/routing. |
npx tsx scripts/test-gemini-generation.ts |
Exercises the configured Gemini model(s) with a sample prompt. | Requires GEMINI_API_KEY and model env vars in .env.local. |
npx tsx scripts/test-rag-generation.ts |
Validates the LlamaIndex runtime + embeddings by running a miniature RAG query. | Loads .env.local before importing repo code. |
node scripts/tools/test-fetch.js |
Sends a sample payload to /api/log using the internal token. |
Set ACCESS_CONTROL_INTERNAL_TOKEN in your shell first. |
- infra/docker/Dockerfile — Docker build context (referenced by Cloud Build).
- infra/cloud-build/cloudbuild.yaml — Cloud Build pipeline (used by
npm run deploy). - infra/cloud-run/service.json — Cloud Run service manifest snapshot.
- infra/firebase/firebase.json (+ rules/indexes) — Firebase config; run Firebase CLI with
--config infra/firebase/firebase.json. - config/eslint.config.mjs — ESLint flat config (script
npm run lintalready points here). - config/vitest.config.ts — Vitest config (script
npm run testalready points here). |pwsh scripts/remediate-logging.ps1| Restores Cloud Run logging IAM bindings and checks_Defaultsinks. | Requires the gcloud CLI with project access. | |pwsh tmp/upload-secrets.ps1| Uploads the sanitized secret payloads generated byscripts/build-env-secrets.tsto Secret Manager. | Re-run after rotating values inside.env.build. |
All scripts assume corepack enable/npm install has been run so tsx, next, and related CLIs are available.
We welcome contributions! Please fork the repository and submit a Pull Request.
MIT License