Making a Job Search Measurable
Overview
A job search produces plenty of activity but very little memory. Applications disappear into company portals, follow-ups live in calendars or notes, contacts are scattered across messages, and the only reliable status update is often an automated rejection email.
I built Job Search Hub as an open-source workspace for keeping that work together. It tracks opportunities from discovery through a final outcome, connects applications to people and interviews, and turns status history into metrics that can answer a more useful question than “How many jobs have I applied to?”
That question is: What is working, and what needs attention next?
The application is deliberately private by default and simple to run locally. It uses Laravel for the domain and authorization layer, with a Vue and TypeScript interface delivered through Inertia.
Problem
Most job-search trackers are good at storing a row and poor at representing a process.
A spreadsheet can hold a company, role, date, and status. It becomes much less useful when the search needs to represent:
- Several contacts at the same company
- Multiple interviews for one application
- Tasks with priorities and due dates
- Compensation and workplace preferences
- Notes that should remain private
- A status that changes several times
- Follow-ups that become overdue
- The source that produced an interview rather than merely an application
The distinction between current state and history is especially important. A single status cell can say an application is at the interview stage, but it cannot say how long the employer took to respond, when the transition happened, or how frequently applications from that source progress that far.
Without that history, the tracker records activity without helping improve it.
Product Approach
One workflow, not a collection of utilities
The central object is a job application, but the product is built around the work surrounding it. Each application can carry role details, compensation, workplace type, the original posting, a description, and private notes. Contacts and scheduled interviews attach to that context instead of living in separate address-book and calendar silos.
Tasks provide the operational layer. They can be prioritized, assigned due dates, and surfaced alongside interviews in an upcoming-actions view. The result is a single place to answer both “Where does this opportunity stand?” and “What should I do next?”
A pipeline that preserves context
Applications move through a drag-and-drop Kanban board from saved opportunities to active applications, interviews, offers, rejections, or an archive.
The interaction is quick, but the move is not treated as a cosmetic rearrangement. A stage change is domain history. Preserving it makes later analysis possible and prevents the interface from reducing a months-long search to a snapshot of its current state.
Search and filters are designed to narrow that pipeline without making the user lose their place. The same filtering needs also led to a reusable package, Laravel Query Filters, which keeps request-driven Eloquent filters explicit and allowlisted.
Turning History Into Feedback
The analytics dashboard derives practical measures from the activity already recorded during normal use:
- Funnel conversion between application stages
- Application activity over time
- Employer response-time distribution
- Performance by application source
- Opportunities and follow-ups needing attention
This avoids asking the user to maintain a second analytics dataset. Moving an application, scheduling an interview, or recording an outcome creates the history needed to explain the search later.
There is an important product constraint here: the dashboard should support decisions without pretending the data is more conclusive than it is. A personal job search is a small dataset affected by role, market, timing, geography, and chance. The metrics are therefore presented as feedback about this search—not universal advice about how hiring works.
Security and Data Boundaries
Job-search data contains private notes, compensation expectations, contact information, and a detailed record of professional activity. “Users must be logged in” is not an adequate authorization model for it.
The application uses layered account and resource protection:
- Email verification and password confirmation for sensitive account actions
- Two-factor authentication and passkey support through Laravel Fortify
- Authorization policies around user-owned resources
- User-scoped route model binding so another user’s identifier cannot resolve into a controller action
- Server-side validation before filters or writes reach the domain layer
The application defaults to SQLite and can run on a user’s own machine. That makes local evaluation straightforward and gives users a deployment path that does not require sending their search history to a hosted service operated by the project.
Migrating Without Guessing
The first version of this workflow existed in a simpler CSV-based tracker. Replacing it created a practical requirement: existing history had to move into the new relational model without duplicate records or a leap of faith.
I built an Artisan import command with two safety properties:
- Dry-run first. The command can parse and preview the migration without writing anything.
- Idempotent execution. Running the import again does not duplicate data already migrated.
The importer accepts an explicit target user and only infers one when the database contains exactly one account. That keeps a convenient local workflow from becoming ambiguous in a multi-user installation.
This is a small feature from the interface’s perspective, but it is the difference between a demo that starts empty and a tool someone can actually adopt without abandoning their history.
Engineering Quality
The repository is intended to be read, run, and changed by people other than its author, so the development workflow is part of the product.
Backend checks combine formatting, PHPStan/Larastan static analysis, and a Pest test suite. The Vue and TypeScript frontend has separate lint, format, and type-check commands. Demo data can be seeded repeatedly without disturbing non-demo accounts, giving reviewers a representative search without requiring personal information.
The application also separates responsibilities along recognizable boundaries:
- Laravel actions and controllers coordinate use cases
- Models and policies own data relationships and authorization
- Query filters express reusable search behavior
- Inertia pages and Vue components own the interactive workflow
- Generated Wayfinder routes keep frontend navigation aligned with Laravel routes
The goal is not architecture for its own sake. It is to make changes to analytics, filtering, or workflow behavior possible without quietly weakening the data boundary around every user’s search.
Outcome
Job Search Hub is a working, installable application rather than a static portfolio demo. It combines the operational parts of a search—applications, relationships, interviews, and follow-ups—with enough history to evaluate the process afterward.
It also became a useful proving ground for two ideas I value in product engineering:
- The data created by doing the work should produce feedback without extra bookkeeping.
- A personal tool still deserves production-grade authorization, migration safety, and automated quality checks.
The project is open source under the MIT license. Read the source and run it locally.



