AI-powered job search and application automation. Upload a resume, discover roles across major ATS platforms, and apply manually or with agent-assisted form filling.
Stack: Next.js 16 (App Router) · Clerk auth · Neon Postgres + Drizzle ORM + pgvector · RetroUI · Tailwind v4 · Inngest workflows
Not using: Supabase · LangChain · Vercel Blob · Neo4j
- Resume upload & parsing — PDF/DOCX stored in Postgres (
BYTEA); structured profile fields extracted via OpenRouter (google/gemini-3.1-flash-lite) - Vector embeddings — Resume text embedded with Google AI Studio (
gemini-embedding-2, 768-dim) and stored in pgvector for future semantic matching - Profile management — Editable profile with completeness tracking (Personal, Summary, Skills, Experience, Education, Projects, Certifications)
- Job discovery — Tavily-powered search across Greenhouse, Lever, Workable, Wellfound, LinkedIn, and Google Careers with match scoring and 6-hour cache
- Save & track jobs — Bookmark listings; saved jobs are never pruned on refresh
- Application automation — Two-phase Inngest workflow: prepare (form discovery + RAG-grounded answers) → submit (Playwright fill + submit). Supported ATS: Greenhouse, Lever, Workable
- Authenticated browser handoff — Remote Browserbase sessions for login/MFA/CAPTCHA; seven-day encrypted context reuse per platform
- Fail-closed safety — Unknown, ambiguous, or unsupported form fields abort the run to human review (
needs_review); real submission gated by feature flag
cp .env.example .env
# Fill in all required keys (see Environment variables below)
npm install
# First-time DB setup (new Neon branch)
npm run db:setup-pgvector # CREATE EXTENSION vector
npm run db:push # Push Drizzle schema
npm run db:setup-embedding-index # HNSW index on resumes.embedding
# Terminal 1 — Next.js
npm run dev
# Terminal 2 — Inngest (required for application automation)
npx inngest-cli@latest dev -u http://localhost:3000/api/inngestOpen http://localhost:3000. Inngest dashboard: http://localhost:8288.
See .env.example for the full list. Required groups:
| Group | Variables | Source |
|---|---|---|
| Auth | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY, CLERK_WEBHOOK_SECRET |
Clerk Dashboard |
| Database | DATABASE_URL |
Neon Console |
| LLM (parse + RAG + Stagehand) | OPENROUTER_API_KEY, OPENROUTER_MODEL=google/gemini-3.1-flash-lite |
OpenRouter |
| Embeddings | GOOGLE_GENERATIVE_AI_API_KEY, GEMINI_EMBEDDING_MODEL=gemini-embedding-2, GEMINI_EMBEDDING_DIMENSIONS=768 |
Google AI Studio |
| Job search | TAVILY_API_KEY |
Tavily |
| Browser automation | BROWSERBASE_API_KEY, APPLICATION_CONTEXT_ENCRYPTION_KEY |
Browserbase |
| Inngest (local) | INNGEST_DEV=1, INNGEST_EVENT_KEY=local, INNGEST_SIGNING_KEY=local |
— |
| Release gate | AUTOMATIC_APPLICATION_SUBMISSION_ENABLED=false |
Keep false until a controlled end-to-end submit passes |
Generate the encryption key once:
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"Optional: TAVILY_PROJECT (dashboard grouping), JOBS_DEBUG=1 (search logging).
Each step uses a dedicated provider — do not mix them:
| Step | Provider | Model |
|---|---|---|
| Resume parse | OpenRouter | google/gemini-3.1-flash-lite |
| Application answer RAG | OpenRouter | OPENROUTER_MODEL |
| Vector embedding | Google AI Studio | gemini-embedding-2 |
| Job discovery | Tavily | Search API (search_depth: basic) |
| Form discovery / automation | Browserbase + OpenRouter | Remote browser + Stagehand LLM |
npm run db:setup-pgvector # Enable pgvector extension (once per Neon branch)
npm run db:push # Push schema to Neon (dev)
npm run db:setup-embedding-index # HNSW index on resumes.embedding
npm run db:generate # Generate SQL migrations
npm run db:migrate # Apply migrations (production)
npm run db:studio # Open Drizzle StudioFirst-time setup order: db:setup-pgvector → db:push → db:setup-embedding-index.
If a database was built with db:push and later db:migrate fails with type ... already exists, baseline the migration journal instead of dropping tables. See memory/memory.md for details.
npm run test:jobs # Job search, cache, URL filter, match score (13 tests)
npm run test:applications # State machine, adapters, URL allowlist (18 tests)
npm run test:browser-discovery # Live Browserbase discovery (requires keys)| Route | Access | Description |
|---|---|---|
/ |
Public | Landing page |
/sign-in, /sign-up |
Public | Auth (signed-in users redirect to /dashboard) |
/onboarding |
Protected | Resume upload gate |
/dashboard |
Protected | Job search & listings |
/dashboard/profile |
Protected | Profile editor |
/dashboard/resume |
Protected | Resume upload |
/dashboard/application-status |
Protected | Application list |
/dashboard/applications/[id] |
Protected | Application detail + auth handoff |
/dashboard/hunt |
Protected | Hunt control (placeholder) |
/dashboard/settings |
Protected | Settings |
/api/webhooks/clerk |
Public | Clerk webhook (signature verified) |
/api/inngest |
Public | Inngest function handler |
/api/applications/[id]/authentication |
Protected | Browserbase sign-in redirect |
/api/applications/[id]/screenshot |
Protected | Application screenshot (ownership check) |
/api/resumes/[id]/download |
Protected | Resume file download |
User clicks Apply
→ startJobApplication (server action)
→ Inngest: prepare-application
→ Browserbase/Stagehand form discovery (Greenhouse / Lever / Workable)
→ OpenRouter RAG answers from resume chunks
→ State: ready_to_submit (or awaiting_authentication / needs_review / job_unavailable)
→ Inngest: submit-application (when enabled + user continues)
→ Playwright locators fill + submit
→ State: submitted / needs_review / submission_uncertain
AUTOMATIC_APPLICATION_SUBMISSION_ENABLED=false(default) — workflow pauses atready_to_submit; discovery and answers still run, but nothing is submitted.- Authentication — if a platform requires login, the app enters
awaiting_authenticationand opens a remote Browserbase session for the user to sign in manually. JobClaw never stores third-party credentials. - Delete —
deleteJobApplicationcancels queued work and hard-deletes the application and all related artifacts.
- Email + Password — User & Authentication → enable Email + Password.
- Google OAuth — User & Authentication → Social connections → enable Google.
- Redirect URLs — add
http://localhost:3000(and production domain). - Webhook — endpoint
https://your-domain/api/webhooks/clerk. Subscribe touser.createdanduser.updated. Copy signing secret toCLERK_WEBHOOK_SECRET.
Local tip: without a webhook, the first visit to /dashboard or /profile upserts a profiles row via ensureProfileSynced().
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ RetroUI │────▶│ Next.js 16 │────▶│ Neon Postgres │
│ (React 19) │ │ App Router │ │ + Drizzle ORM │
└─────────────┘ │ + Clerk │ │ + pgvector │
└──────┬───────┘ └─────────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ OpenRouter│ │ Tavily │ │ Browserbase │
│ (LLM/RAG)│ │ (search) │ │ + Stagehand │
└──────────┘ └──────────┘ └──────────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ Google │ │ Inngest │
│ (embed) │ │ (workflows) │
└──────────┘ └──────────────┘
| Area | Path |
|---|---|
| Resume parse & upload | lib/resume/, lib/actions/upload-resume.ts |
| Embeddings | lib/embeddings/ |
| Job search | lib/jobs/, lib/actions/search-jobs.ts |
| Applications | lib/applications/, inngest/functions/ |
| Database schema | lib/db/schema.ts |
| UI components | components/ |
| Agent memory log | memory/memory.md |
- Sign up at
/sign-up, sign in, confirm redirect to/dashboard. - Upload a resume at
/onboardingor/dashboard/resume; verify profile fields populate at/dashboard/profile. - Search for jobs on
/dashboard; confirm listings appear and platform filters work. - Save a job; refresh search; confirm saved job persists.
- Start a manual or automatic application on a Greenhouse/Lever/Workable posting.
- With
AUTOMATIC_APPLICATION_SUBMISSION_ENABLED=false, confirm workflow stops atready_to_submit. - Sign out; confirm protected routes redirect to
/sign-in.
For AI agent conventions and pitfalls, see AGENTS.md. For a detailed development log, see memory/memory.md.