Skip to main content

RoleReady

nextjs typescript postgresql better-auth chrome-extension saas

I needed a better way to track job applications. Spreadsheets were tedious, and existing tools didn’t work across the different job boards I was using. So I built RoleReady — a job tracking platform that started as a weekend project and evolved into a production SaaS used by hundreds of developers.

Tech stack: Next.js 15 • TypeScript • PostgreSQL • Better-Auth • Zustand • Chrome Extension


The Beginning

It started simply: I was applying to jobs and losing track of where I was in each process. Some applications had multiple interview rounds, others required technical assessments, and managing this across LinkedIn, Indeed, Greenhouse, and Lever became overwhelming.

I tried building with familiar tools initially, but quickly ran into limitations. The real breakthrough came when I stopped thinking about it as a simple CRUD app and started solving the actual problems job seekers face.


Choosing the Stack

I went with Next.js 15 for its App Router and Server Components. The ability to eliminate client-side data fetching waterfalls was crucial — users needed to see their job information instantly, not wait for multiple API calls. Server Components by default meant faster initial page loads and better SEO performance.

For the database, I chose PostgreSQL with Drizzle ORM. After struggling with Prisma’s JSONB limitations and opaque migration system, Drizzle’s SQL-like syntax with full TypeScript support felt right. I could see the actual SQL being generated, and the migration files were transparent and reviewable.

Authentication needed to handle multiple providers without creating duplicate accounts. Users would naturally sign in with Google on desktop and GitHub on mobile. Better-Auth solved this with automatic email conflict detection and seamless account linking.

// Server-side enrichment pattern
export const enrichJob = async (job: Job) => {
  const enriched = { ...job };

  enriched._computed = {
    deadlineUrgency: calculateUrgency(job.deadline),
    displayTitle: formatTitle(job.title),
    interviewProgress: calculateProgress(job.interviews),
    applicationAge: daysSince(job.createdAt),
  };

  return enriched;
};

This pattern moved all business logic to the server. Components received ready-to-use data through a standardized _computed field, eliminating 865 lines of duplicate code across the frontend.


The Chrome Extension Challenge

The hardest problem was extracting job information from different job boards. My first attempt used site-specific parsers that read DOM selectors. They worked great — until LinkedIn updated their layout and broke everything.

The solution was to stop parsing entirely. Instead, the Chrome extension captures the entire page context and sends it to server-side AI for processing.

// universal-content.js - works on any job board
const pageContent = document.documentElement.outerHTML;
const metadata = {
  url: window.location.href,
  title: document.title,
  timestamp: Date.now()
};

chrome.runtime.sendMessage({
  type: 'extract_job',
  content: pageContent,
  metadata
});

This universal approach works on LinkedIn, Indeed, Greenhouse, Lever, and 11+ other platforms without any site-specific code. When job boards update their layouts, the extension keeps working. The maintenance overhead dropped from hours per week to zero.


State Management

Managing state across browser tabs and ensuring instant UI feedback led me to Zustand. The lightweight state management library handles optimistic updates with automatic rollback, creating a responsive user experience even on slower networks.

export const useJobStore = create<JobStore>()(
  persist(
    (set, get) => ({
      jobs: [],

      updateJobStatus: async (id, status) => {
        const originalJobs = get().jobs;
        // Update UI immediately
        set(state => ({
          jobs: state.jobs.map(job =>
            job.id === id ? { ...job, status } : job
          )
        }));

        try {
          await api.updateJob(id, { status });
          toast.success("Job updated");
        } catch (error) {
          // Rollback on error
          set({ jobs: originalJobs });
          toast.error("Failed to update job");
        }
      }
    }),
    { name: 'job-store' }
  )
);

The UI updates instantly, providing immediate feedback. If the API call fails, the state rolls back automatically. This pattern transformed perceived performance — actions feel instantaneous, even though they complete at the same speed.


Production Reality

The deployment is intentionally simple: Next.js on Vercel with PostgreSQL database. No microservices, no Kubernetes, no complex orchestration. Just reliable, maintainable infrastructure.

Performance improvements were measurable. Package installation went from 45 seconds to 3 seconds with Bun. Bundle size reduced by 40% through tree-shaking. Time to interactive improved from 2.1s to 1.2s. Server response time improved by 23%, and memory usage decreased by 35%.

The platform now supports document management, interview tracking, and provides detailed analytics about the job search process. Users can track which documents they used for specific applications, maintain interview feedback, and identify patterns in their application success rates.


Try it yourself: View Live DemoSource Code