Collects job postings from multiple sources, scores every one against your profile with AI, auto-writes cover letters, pings you on Telegram about the good ones — and gives you a clean dashboard to run the whole pipeline from "new" to "offer".
Job hunting is mostly grunt work: open five job boards, skim hundreds of postings, guess which ones are worth your time, rewrite the same cover letter over and over. AutoHQ automates the boring 90% so you spend your energy only on the postings that actually fit.
It's built as two halves that talk over a single webhook:
- The collector — n8n workflows run on a schedule (every weekday morning), pull fresh postings from job boards, ask OpenAI to rate each one 1–100 against your stack and preferences, generate a tailored cover letter, and forward everything to the app. High scorers also trigger a Telegram notification.
- The cockpit — a Nuxt 4 dashboard where every job lands in a Kanban-style
pipeline (
new → reviewing → applied → interviewing → offer). You read the AI score and reason, copy the generated cover letter, track applications, and tune the whole engine (keywords, score threshold, which sources are active) live — changes are picked up by n8n on the next run.
Note — this is a personal tool for one job seeker. Sign-in is GitHub OAuth, and since any GitHub account can complete OAuth, an owner-gate (
OWNER_GITHUBenv, enforced in the OAuth callback) restricts access to a single owner. Data lives in a Postgres database accessed only through the app's server API. If you fork it for a team, replace the owner-gate with per-user scoping (see Security notes).
| 🤖 AI scoring | Every posting rated 1–100 against your profile; only relevant jobs surface to the top. |
| ✍️ Auto cover letters | OpenAI writes a tailored, language-aware (EN/RU) cover letter per job — no clichés, references your real projects. |
| 📲 Telegram alerts | Get pinged only for jobs scoring above your threshold (default ≥ 70). No noise. |
| 🔌 Multiple sources | Remotive, Arbeitnow, HH.ru, rabota.by (Belarus), Djinni out of the box — each its own n8n workflow, toggleable from the UI. |
| 📊 Pipeline dashboard | Stats, a funnel view, top matches, and a Kanban board from new to offer. |
| 📄 Live resume | Your resume is rendered straight from a Markdown file in a GitHub repo (e.g. an Obsidian vault) — single source of truth. |
| 🎛️ Live control panel | Change search keywords, score threshold, and enable/disable sources without redeploying — n8n reads these at runtime. |
| 🌑 Polished dark UI | shadcn-vue + Tailwind v4, dark by default. |
flowchart TD
cron(["⏰ weekday cron"]) --> fetch
subgraph n8n["n8n · self-hosted"]
direction TB
fetch["Fetch postings<br/>Remotive · Arbeitnow · HH · rabota.by · Djinni"]
ai["OpenAI<br/>score 1–100 + cover letter"]
tg["Telegram alert<br/>score ≥ threshold"]
fetch --> ai
ai --> tg
end
subgraph app["Nuxt 4 app · Vercel"]
direction TB
ui["Dashboard · Jobs · Applications<br/>Resume · Control"]
api["server API<br/>+ GitHub OAuth session"]
db[("PostgreSQL<br/>jobs · app_config<br/>source_settings · profile")]
ui --> api
api --> db
end
ai -->|"POST /api/webhook/jobs<br/>x-autohq-token"| api
fetch -.->|"GET /api/n8n/hh · /api/resume<br/>live config + fresh HH token<br/>x-autohq-token"| api
repo["Private repo · Obsidian vault<br/>Resume EN.md / Resume RU.md"] -.->|"live resume<br/>GitHub raw API"| api
| Layer | Tech |
|---|---|
| Frontend / SSR | Nuxt 4, Vue 3, TypeScript |
| UI | shadcn-vue (Reka UI), Tailwind CSS v4, Lucide icons, @nuxtjs/color-mode |
| State / data | Pinia, VueUse, TanStack Vue Table |
| Database | PostgreSQL via postgres (direct driver) |
| Auth | GitHub OAuth + encrypted cookie sessions (nuxt-auth-utils) |
| Automation | n8n (self-hosted) |
| AI | OpenAI (scoring + cover letters) |
| Hosting | Vercel (app) + your own n8n instance |
- Node.js 20+ and npm
- A PostgreSQL database (any provider — Supabase, Neon, Railway, or local Postgres)
- A GitHub OAuth app (for login)
- A running n8n instance (self-hosted or cloud) — for the automated collection
- An OpenAI API key — for AI scoring and cover letters
- (optional) A Telegram bot token + chat id — for alerts
- (optional) A GitHub personal access token — only if you want the live-resume feature
You can run the app alone (just Postgres + GitHub login) and add jobs by hand. n8n, OpenAI and Telegram are what make it automatic.
git clone <your-fork-url> autohq
cd autohq
npm installCreate a PostgreSQL database and set DATABASE_URL (step 4), then apply the schema:
npm run db:migrateThis runs the SQL files in db/migrations/ in order, tracking what's been
applied in a schema_migrations table — so it's safe to re-run and safe against an existing
database (the migrations are idempotent). 0001_init.sql creates
all tables: jobs, app_config, source_settings, profile.
To evolve the schema later, add a new numbered file (db/migrations/0002_*.sql) and run
npm run db:migrate again. There's no Row Level Security — access is enforced by the app's auth
middleware, and the app connects with the database owner role.
- GitHub → Settings → Developer settings → OAuth Apps → New OAuth App.
- Authorization callback URL:
https://<your-app-domain>/auth/github(locally:http://localhost:3000/auth/github). - Copy the Client ID and generate a Client Secret — these go into
NUXT_OAUTH_GITHUB_CLIENT_ID/NUXT_OAUTH_GITHUB_CLIENT_SECRET(step 4). - (single-user lock) Any GitHub account can complete OAuth, so set
OWNER_GITHUBto your GitHub login. The OAuth callback (server/routes/auth/github.get.ts) rejects any other login.
Copy the example and fill it in:
cp .env.example .env# ── Database ───────────────────────────────────────────
# Postgres connection string. With Supabase use the "postgres" role string;
# on Vercel use the pooler (pgBouncer, port 6543).
DATABASE_URL=postgresql://postgres:password@host:5432/postgres
# ── Auth ───────────────────────────────────────────────
NUXT_OAUTH_GITHUB_CLIENT_ID=your-oauth-client-id
NUXT_OAUTH_GITHUB_CLIENT_SECRET=your-oauth-client-secret
# Encrypts the session cookie. At least 32 chars: openssl rand -base64 32
NUXT_SESSION_PASSWORD=change-me-to-a-random-string-32-chars-min
# GitHub login of the single owner. ANY GitHub account can pass OAuth,
# so the callback rejects anyone whose login != this value.
# Leave empty to disable the gate (local dev). REQUIRED in production.
OWNER_GITHUB=your-github-username
# ── Frontend (public, exposed to the browser) ──────────
# Show sample jobs on the Jobs page instead of querying the DB (dev/demo).
NUXT_PUBLIC_USE_SAMPLE_DATA=false
# Control-page links (all optional, each shown only if set). NUXT_PUBLIC_APP_URL
# builds the displayed webhook URL; if empty, the current origin is used.
NUXT_PUBLIC_N8N_URL=https://n8n.example.com
NUXT_PUBLIC_TELEGRAM_BOT=@your_bot
NUXT_PUBLIC_APP_URL=https://your-app.example.com
# ── Server-only ────────────────────────────────────────
# Shared secret n8n must send in the `x-autohq-token` header to reach the
# webhook, /api/n8n/* and /api/resume.
AUTOHQ_SECRET_TOKEN=choose-a-long-random-string
# Optional: live-resume feature (reads Resume *.md from a GitHub repo)
RESUME_REPO=your-username/your-private-repo
RESUME_PATH_EN=Personal/Resume EN.md
RESUME_PATH_RU=Personal/Resume RU.md
GITHUB_TOKEN=ghp_xxxLive resume is configured via
RESUME_REPO/RESUME_PATH_EN/RESUME_PATH_RU/GITHUB_TOKEN(seeserver/api/resume.get.ts). Point them at your own repo and Markdown files, or drop the Resume page if you don't need it.
npm run dev # http://localhost:3000Sign in with GitHub works locally as long as your OAuth app's callback URL includes
http://localhost:3000/auth/github. Set NUXT_PUBLIC_USE_SAMPLE_DATA=true to show a few sample
rows on the Jobs page instead of querying the DB, so the UI isn't empty before you add data.
Build for production with npm run build.
The repo is Vercel-ready (vercel.json):
- Import the repo into Vercel.
- Add all the env vars from step 4 in Project → Settings → Environment Variables.
- Deploy. Your webhook endpoint is now
https://<your-app>.vercel.app/api/webhook/jobs.
The n8n/ folder contains starter workflow exports and helper scripts.
-
Push the workflows via the Public API (recommended) — the JSON files are templates: they carry
{{APP_URL}}and{{AUTOHQ_SECRET_TOKEN}}placeholders thatn8n:syncrenders from your.envat push time, so each workflow already points at your app with the right webhook secret — no manual node editing.# set N8N_API_URL + N8N_API_KEY (Settings → n8n API), plus # NUXT_PUBLIC_APP_URL + AUTOHQ_SECRET_TOKEN + OPENAI_API_KEY in .env first npm run n8n:sync # create/update by name npm run n8n:sync -- --activate
n8n:sync(n8n/sync.cjs) matches workflows by name (safe to re-run) and fails loudly if a placeholder has no env value, rather than pushing a broken workflow. Each workflow fetches postings, maps them to the AutoHQ format, and POSTs to your webhook.(Alternatively, import
n8n/workflow-remotive.json/n8n/workflow-hh.jsonvia the UI's Import from File — but then you must fill the{{APP_URL}}/{{AUTOHQ_SECRET_TOKEN}}placeholders in the nodes by hand.) -
Live settings are already wired (HH) — the HH workflow reads keywords, the search period and a fresh HH OAuth token at runtime from a single Get Config node (
GET /api/n8n/hh, authenticated byx-autohq-token), so you change them on the Control page, not in n8n. The other workflows (Remotive, etc.) don't pull live config — they fetch and POST directly; the webhook still enforces the source toggle and Telegram threshold on ingest (see below). -
AI scoring + cover letters are already wired — each workflow fetches the live resume (
GET /api/resume), then runs two OpenAI nodes (@n8n/n8n-nodes-langchain.openAi, Message a Model):- an OpenAI Score node grades every posting 0–100 against your resume — a Parse Score code
node turns its JSON reply into
fit_score+score_reason; - an OpenAI Cover Letter node writes a language-aware
cover_letter. The OpenAI credential is provisioned for you:n8n:synccreates an n8n credential named AutoHQ OpenAI fromOPENAI_API_KEY/OPENAI_BASE_URLand wires its id into both workflows — no manual setup. By default the Docker stack runs these nodes against the bundled llama.cpp server (OPENAI_BASE_URL=http://llamacpp:8080/v1, modelgemma) — pointOPENAI_BASE_URLathttps://api.openai.com/v1with a real key to use hosted OpenAI instead. (The id is cached inn8n/.credentials.json, which is gitignored; the Public API can't list or patch credentials, so a rotated key is applied by delete + recreate on the next sync.) Both prompts live inn8n/prompts/(score.txt,cover-letter.txt) and are injected at push time via the{{>score}}/{{>cover-letter}}includes, so you tune them in one place for both workflows.
- an OpenAI Score node grades every posting 0–100 against your resume — a Parse Score code
node turns its JSON reply into
-
Telegram alerts are already wired (optional) — after posting to the webhook, a Telegram Gate (IF) checks the webhook's
telegram_notifyresponse and a Notify Telegram node pings you only for jobs at or above yourtelegram_min_score. The bot token lives in an n8n credential named AutoHQ Telegram you create once by hand; setTELEGRAM_CHAT_ID(and optionallyTELEGRAM_CREDENTIAL_ID) in.env, or leave them blank to skip Telegram entirely. -
Activate the workflows. They'll run on their cron schedule and your dashboard fills up.
The webhook respects your live settings: a posting is skipped if its source is disabled in the
Control page, and Telegram only fires when fit_score ≥ telegram_min_score (default 70) — both
enforced server-side in POST /api/webhook/jobs against app_config / source_settings on every
ingest, regardless of which workflow sent it. The HH workflow additionally reads the current keywords
and lookback up-front via GET /api/n8n/hh.
autohq/
├── app/
│ ├── pages/ # Dashboard, Jobs, Applications, Resume, Control, Login
│ │ ├── index.vue # Dashboard: stats, funnel, top matches
│ │ ├── jobs/ # list (filters + bulk ops), detail, manual add
│ │ ├── applications.vue # what you've applied to
│ │ ├── profile.vue # live-rendered resume (EN/RU)
│ │ └── automations.vue # "Control": keywords, threshold, source toggles
│ ├── components/
│ │ ├── app/AppSidebar.vue
│ │ └── ui/ # shadcn-vue components
│ ├── composables/useJobStatus.ts # single source of truth for statuses & score colors
│ └── layouts/ # default (app shell) + auth (login)
├── server/
│ ├── api/
│ │ ├── jobs/... # CRUD + bulk ops for the jobs table
│ │ ├── webhook/jobs.post.ts # ← n8n posts here; the heart of ingestion
│ │ ├── config.get.ts / .patch.ts # live keywords + telegram threshold
│ │ ├── sources/... # per-source enable/disable
│ │ └── resume.get.ts # pulls resume Markdown from GitHub
│ ├── routes/auth/github.get.ts # GitHub OAuth callback (owner-gate)
│ ├── middleware/auth.ts # requires a session for /api/* (except webhook/resume)
│ └── utils/db.ts # postgres connection pool (useDb())
├── db/migrations/ # numbered .sql migrations (npm run db:migrate)
├── scripts/migrate.mjs # forward-only migration runner
├── n8n/ # workflow exports + local prompt-testing helpers
└── nuxt.config.ts
new → reviewing → applied → interviewing → offer — plus rejected and archived.
Defined once in app/composables/useJobStatus.ts (labels, colors,
which statuses count as "applied", score-to-color thresholds), so the whole UI stays consistent.
- What gets searched — edit keywords on the Control page (stored in
app_config, read live by n8n). - Score threshold for alerts — the slider on Control (
telegram_min_score). - Sources — toggle existing ones on Control; add a new one by creating a new n8n workflow + a row
in
source_settings. - Cover-letter prompt — edit
n8n/prompts/cover-letter.txtand re-runnpm run n8n:sync. It's shared by both workflows. Tune it to your stack and tone.
This was built as a single-user personal tool, so a few things are deliberately loose — review before exposing it more widely:
- Access is owner-gated, not multi-tenant. Any GitHub account can pass OAuth, so the OAuth
callback (
server/routes/auth/github.get.ts) rejects any login other thanOWNER_GITHUB, and the server middleware (server/middleware/auth.ts) requires a valid session for every data endpoint. There is no per-user data isolation — for a team you'd adduser_id-scoped rows and filter every query by the logged-in user. - The webhook auth is a single shared secret (
x-autohq-token). KeepAUTOHQ_SECRET_TOKENlong and private; rotate if leaked. - Never commit real keys.
.envis gitignored. Keep OpenAI / n8n / Telegram secrets in n8n credentials or environment variables — never inline them in tracked files.
Built with Nuxt, PostgreSQL, n8n and a lot of "why am I doing this by hand".