Skip to content
Matheus Prates

Time in Goiânia: Goiânia

PTRésumé
Back to the projects

01Busca Vagas

Tech jobs in Goiânia and remote ones, in one place.

A crawler reads nine job boards, merges the same job posted in different places and makes everything searchable, with filters and a link to the original posting.

No AI, on purpose: clear rules, explainable results and zero model cost.

  • Next.js
  • Fastify
  • PostgreSQL
  • GSAP
  • Vitest
See it live
Busca Vagas home page: the total number of jobs in huge type over a photo of Goiânia at night
Busca Vagas on a phone

The problem

Anyone looking for a tech job in Goiânia opens five sites a day and finds the same posting repeated on three of them. Remote jobs are scattered across a handful more.

What I built

  • Automatic collection from nine job boards, each with its own schedule and pace.
  • The same job posted in different places becomes a single entry.
  • Portuguese full-text search that handles accents, with filters for category, salary, work model, contract, seniority and city.
  • Coverage of Goiânia and the 21 cities of its metro area, plus remote jobs from all over Brazil.
  • User accounts with saved jobs, a résumé builder with six templates and a score for how applicant tracking systems read it, and an admin panel.

The screens

Screenshots of the real system. Click a screen to see it full size.

  • Search results for React: location and area filters on the left and the jobs as cards

    Search: filters by location and area, and every job with its source, date and a link to the original posting.

  • A DevOps job page with the details, a warning about an old posting and an estimated salary range

    Job page: every detail in one place, with a warning when a posting has been up for too long.

  • Market dashboard: open jobs, hiring companies, new jobs in seven days and median salary

    Market: open jobs, companies, new jobs this week and median salary, recalculated every 10 minutes.

  • Bar charts: jobs by area and the most requested technologies

    Where the jobs are and the most requested technologies, counted in the title and description.

  • Salary ranges by area, with the median marked on each one

    Pay by area: the middle range and the median, only where there are enough salaries.

  • Calculator comparing a CLT salary with a contractor rate

    Employee or contractor calculator: what you actually keep in each case, using this year's tax tables.

How it works

  1. Collect

    A worker reads each source on its own schedule and writes what it found to a queue inside PostgreSQL itself.

  2. Clean up

    Each job goes through classification rules editable in the admin panel and is compared with existing ones to avoid duplicates.

  3. Search

    The Fastify API answers with Portuguese full-text search, filters and pagination.

  4. Site

    The Next.js site shows everything, and the button takes you straight to the original posting.

Decisions that mattered

  1. No AI

    Why: Classifying a job is a rule, not an opinion. Written rules are predictable, testable and cost nothing per job.

  2. What to collect is data, not code

    Why: Source, search term, city and field live in tables. Taking the site to another city or field means adding rows, not writing code.

  3. A queue inside PostgreSQL

    Why: One less database to look after. The same pattern had already proven itself in another system I built.

  4. Site and API on the same origin

    Why: No CORS, and the session cookie stays protected. nginx splits the API path from the rest.

What went wrong

Every real system breaks somehow. These were the stumbles that taught the most, and what changed because of them.

  1. Collection kept running, and new jobs stopped coming in

    What happened

    In September, the intake of new jobs dropped to almost nothing without a single error. Every collection finished fine: the saved searches were just rereading the same set over and over.

    What changed

    I started watching what comes in, not just whether it ran. Today there is an alert when the day's new jobs fall below a quarter of the weekly average, and each search shows how much of what it reads it actually keeps.

  2. A page that didn't exist said it did

    What happened

    A removed job showed the not-found screen, but the server answered 200. To Google that is content, and on a job site, where postings expire all the time, search fills up with dead pages.

    What changed

    The loading screen that caused it now lives only where it's needed. And after touching a route, I check the status code, not just the screen.

  3. A new field broke every older résumé

    What happened

    I added certifications to the résumé and made the field required. Every résumé generated before that started returning a 500 error, and the tests passed because they only created new ones.

    What changed

    A new field in stored data gets filled in when it's read. And there is a test that writes the old format by hand and requires it to open.

Where it stands

Live since September 2026, updating itself.