Tech stack & architecture

ResumeGo is a small applicant tracking system for recruiters and agencies. You upload CVs, it reads them, finds contact details and skills, flags files that are not CVs, and ranks every CV against your open jobs. It is one Go program and one database file on a small server. This page explains, in plain words, how it works and what was decided on purpose.

What happens to a CV

  1. Upload. Up to 20 PDF or Word files at once. The real file type is checked from its content, not its name, and the same file uploaded twice is recognised, so nothing is stored twice.
  2. Reading. In the background, two at a time: the text of a PDF or Word file is read directly; a scanned PDF is turned into page images and read with OCR (text recognition), in English or Romanian.
  3. Understanding. Name, email and phone are taken from the top of the CV. Skills are found with a dictionary of 348 skills and 752 ways of writing them, in English and Romanian ("k8s" is Kubernetes, "contabilitate" is Accounting).
  4. Checking. A file that does not look like a CV (an invoice, a contract, a cover letter, a blank scan) is flagged with the reason. The recruiter keeps it or deletes it.
  5. Matching. Every read CV is ranked against every open job straight away: a score from 0 to 100 built from skills, years in that kind of job (from the job titles and dates on the CV) and spoken languages with their level, with the reasons written next to it.
  6. Pipeline. The recruiter adds candidates to a job and moves them through New, Screening, Interview, Offer, Hired or Rejected. Every step is kept in the candidate's activity.

Stack

LanguageGo 1.26, standard library first. Direct modules: chi (router), the SQLite driver, x/crypto (passwords), x/sync, x/time (rate limits), uuid
DatabaseSQLite with the pure Go driver modernc.org/sqlite (no C compiler needed), WAL mode, one writer connection and a read-only pool. Tables in 8 plain SQL files, created on start
SearchSQLite full-text search (FTS5): Romanian letters with or without diacritics match the same, every word is matched as a prefix
Reading filespoppler (pdftotext, pdftoppm) and Tesseract OCR as child processes, each with a time limit and one CPU thread; Word files read with Go's own zip and XML packages
Pageshtml/template rendered on the server and embedded in the program; plain CSS with colour tokens; a few small JavaScript files, no framework and no build step
Sign-inbcrypt passwords, sessions stored on the server (only a hash of the cookie token), login rate limits, protection against cross-site form posts (http.CrossOriginProtection)
Outside servicesNone. CVs never leave the server
HostingOne static binary on a VPS, run by pm2 on localhost, nginx in front with HTTPS from certbot (Let's Encrypt). Database backups are planned

Architecture

browser ─► nginx (HTTPS) ─► one Go program (pm2)
                                │
             router ─► sign-in checks ─► handlers ─► store (SQL only)
                                │                         │
             upload: type check, save file,               ▼
             one short database transaction ─► SQLite (WAL): accounts, candidates,
                                │              CVs = the job queue, skills, jobs,
                                ▼              pipeline, activity, search index
             2 background workers ◄── claim the oldest waiting CV
                │
                ├─ PDF  ─► pdftotext ─► too little text? ─► pdftoppm ─► tesseract
                ├─ Word ─► text from word/document.xml
                ▼
             contact details · skills · "is it a CV?" · search index · match scores

Every account is separate: each query filters on the signed-in account, and the database itself refuses links between records of two accounts. Opening another account's candidate answers "not found".

Notable engineering

  • A queue without a queue server. The CV table is the job queue. A worker claims the oldest waiting CV in one database statement, so two workers never take the same one. If the server stops while a CV is being read, it is read again after the restart.
  • Limits that cannot be raced. The monthly CV limit and the open-jobs limit are checked in the same short transaction as the insert, on the single writer connection, so 20 uploads at the same moment cannot pass the limit together.
  • Is it a CV? A simple, explainable check: contact details, CV headings (Experience, Education, Skills … in English and Romanian), known skills and a "Curriculum Vitae" title add points; an invoice, contract or cover-letter title takes points away. Below the bar the file is flagged with the reasons, never deleted.
  • Explainable match score. The CV is split into sections and dated jobs. A skill used in a job of the last five years counts fully, one only listed counts less, one only implied (Django means Python) counts half, and one under Interests or after "no experience with" does not count. Job titles in English and Romanian give years per kind of job (testing, helpdesk, chat support …), counted once when jobs overlap, and languages are read with their level (C1, "avansat", native). The score combines skills (65%), experience (20%) and languages (15%); a missing must-have or language caps it below 50. Each match says why: "7 of 9 skills · 3 years in Customer & chat support (needs 1) · English C1". A person always makes the decision; nothing is rejected automatically.
  • Search that is safe and forgiving. What the recruiter types is turned into a safe search query (never passed through raw), matches are highlighted in a short extract, and "bucuresti" finds "București".
  • Careful with files. Files are stored under generated names, never the uploaded name; large uploads go to temporary files instead of memory; tools run with time limits; a CV opens in a preview window, PDFs as themselves and Word files as their text.
  • Logs and audit trail. Every request is recorded (route, status, time, IP), and the important actions (sign-ins, uploads, CV views, stage moves, deletes, every admin view) also go to an audit trail with their outcome. A request never waits for this: entries go through a buffer to one background writer that saves them in batches, and rows older than 90 days are deleted.
  • Works without JavaScript. Every page and form works without it; scripts only add the preview window, the sidebar on phones, auto-submitting filters and the refresh while CVs are being read.

Design decisions

  • Simple to run. One program and one database file. No separate queue, search engine or cloud service to pay for or keep running.
  • Rules, not AI, for ranking people. Ranking candidates is "high-risk" under the EU AI Act. A rule-based score that shows its reasons is easier to trust, check and explain than a black box.
  • Privacy by default. CVs are personal data: they are read on the server, never sent to a third party, and deleting a candidate deletes the file too.
  • No migration tool. The tables are plain SQL files run on every start; a change to an existing table is a deliberate, backed-up step.

Project structure

cmd/server/        starts everything: config, database, workers, web server
cmd/seed/          demo data: 20 open jobs in fictional companies
internal/
  config/          settings from environment variables, plans and limits
  database/        opens SQLite, schema/*.sql (tables and the skills dictionary)
  auth/            registration, login, sessions, admin from the settings, rate limits
  upload/          file type check, save, register the CV
  processor/       text extraction, OCR, contact details, skills, "is it a CV?"
  queue/           the background workers
  store/           all SQL: candidates, jobs, clients, pipeline, matching, activity
  handlers/        the web pages and forms
  views/           templates, CSS, JavaScript (embedded in the program)
docs/              the plan and its checklist

Areas of the site

AreaPathsAccess
Dashboard/Signed in
Candidates and CVs/candidates, /candidates/upload, /resumes/…Signed in, own account only
Jobs and pipeline/positions, /applicationsSigned in, own account only
Clients/clientsSigned in, own account only
Admin/admin/files, /admin/logsPlatform admin only (404 for others)
Public/login, /register, /techAnyone

Known limits

  • Years of experience come from date ranges and job titles the dictionary knows; an unusual title or a CV without dates gives no years for that kind of job.
  • Some words are too ambiguous to match on their own ("Go", "Excel", "Word"), so a CV must write "Golang" or "MS Excel" to be counted.
  • Billing, team invites and the public "send us your CV" link per recruiter are planned, not built.
  • There is no automated test suite yet; tests are written when asked for.