- Published on
How Job Assistant is built - Backend & Agents
- Authors

- Name
- Zee Lu
Introduction
In the frontend dev note, we explored how Job Assistant handles the chaotic DOM of job boards. But extracting the text is just step one. The real value—the "AI" in AI Assistant—happens in the backend.
To keep the extension lightweight and secure, I moved all intelligence to the server. Specifically, I built a coordinated suite of Edge Functions running on Deno (via Supabase).
This post is a detailed walkthrough of every single function in the plugin-functions repository, explaining exactly how each one works.
The Edge Architecture
Before diving into specific functions, let's look at the shared DNA across the backend. Every function shares three traits:
- Deno Runtime: We use Deno for its native TypeScript support and web-standard APIs (fetch, Request, Response).
- Hybrid Auth:
- User Requests: JWTs verified against Supabase Auth (verifySupabaseToken).
- Internal/Admin Requests: Self-signed hashed tokens (verifySelfSignedToken) for secure service-to-service communication.
- Database Traceability: Every single request—successful or failed—is logged to a dedicated Postgres table (e.g., extraction_requests, cover_letter_requests). We log the prompt usage, processing time, and the model used.
1. Company Research: The "Analyst Swarm"
The company-research function is the most complex. It doesn't just "ask ChatGPT". It mimics a team of human analysts working in parallel.
The Challenge
Research takes time. Sequentially searching for news, then financials, then culture would take 60+ seconds.
The Solution: Promise.all Orchestration
We spin up 4 specialized agents simultaneously.
// Simplified "Swarm" Pattern
const [financial, news, culture, industry] = await Promise.all([
financialAgent.research(company), // Looks for stock, revenue, funding
newsAgent.research(company), // Scrapes recent PR and headlines
cultureAgent.research(company), // Checks Glassdoor, values, mission
industryAgent.research(company) // Analyzes competitors
]);
Streaming Updates (SSE)
Because even parallel agents take ~10-15 seconds, we can't leave the user hanging. We use Server-Sent Events (SSE) to stream "thoughts" to the UI.
- data: { type: "status", message: "Searching for Q3 Financial Reports..." }
- data: { type: "status", message: "Analyzing Competitors..." }
This "Optimistic UI" approach makes the wait feel negligible.
2. Resume Alignment: The "Hiring Manager" Simulator
The align-resume function is designed to be the "bad cop". Its job is to objectively score a candidate (0-100) against a Job Description (JD).
The "Assessment" Prompt
We don't ask for a generic opinion. We force the LLM into a persona:
"You are a Senior Talent Acquisition Executive... Conduct a rigorous, professional assessment."
Structured JSON Output
The critical part is the output schema. We define a strict interface:
interface AlignmentResult {
score: number;
summary: string;
strengths: string[];
gaps: string[];
recommendations: string[];
}
The prompt explicitly forbids markdown or chatter. We strip fences (```json) and aggressively parse the output. If the model is feeling "chatty", the parser cleans it up before the UI ever sees it.
3. Resume Tailoring: The "Fact-Preserving" Rewriter
tailor-resume is dangerous. AI likes to hallucinate. If an AI invents a job you never had, you're lying to an employer.
The "Permutation" Constraint
To solve this, we use a technique I call "Permutation Prompting". In the prompt for tailorResumeWithLLM, we explicitly instruct:
"You MUST NOT add, remove, merge, split, or rename any skills. The output skills array MUST contain exactly the same items as the input array, only reordered."
We treat the user's experience as immutable blocks. The AI is only allowed to:
- Reorder skills to match the JD keywords.
- Rephrase bullet points to emphasize relevant tech.
- NEVER invent dates or titles.
PDF Service Handoff
Once the JSON is tailored, this function doesn't generate the PDF (Edge functions have 10MB limits and limited binary support). Instead, it acts as a coordinator, handing the confirmed JSON to a dedicated resume-svc-cf (Cloudflare Worker) that runs a heavy PDF generation library.
4. Resume Parsing: extract-info
Users upload PDFs, but code needs JSON. extract-info turns raw text into the StructuredResume schema.
Parallel Model Fallback
Parsing is critical. If one model fails, the user is blocked. In extract-info/index.ts, we see a robust pattern: the code is set up to potentially call multiple models (defined in MODELS). It uses Promise.race (or Promise.all depending on config) strategies to ensure we get some valid JSON back, even if the primary provider is slow.
5. Cover Letter Generator: The Anti-Robot
Most AI cover letters sound like: "I am writing to express my enthusiastic interest in the [Role] at [Company]. I am a hardworking..." Gross.
In generate-cover-letter, we engineer the prompt to be Anti-Robotic.
The "Banned Words" List
We explicitly ban the "ChatGPT Dictionary":
"BANNED WORDS: Do NOT use: delve, showcase, testament, landscape, tapestry, unwavering, spearhead..."
The "Hook" Strategy
We force the AI to skip the "I am writing" intro.
"THE HOOK: Never start with 'I am writing to express my interest.' Start with a direct connection between the candidate's specific past achievement and a problem listed in the JD."
This produces letters that sound confident and senior, rather than generated and junior.
6. Job Summarizer: The "Skim Reader"
summarize-job is a utility function for the "Job Board" view. When you have a list of 50 jobs, you don't want to read 50 JDs.
This function takes a flexible config:
interface SummarizeConfig {
length: "short" | "medium" | "long";
fields: Array<"title" | "company" | "salary" | ...>;
}
It condenses 2000-word corporate fluff into a 30-word "What they actually want" summary.
7. Check Usage: The Accountant
check-usage is simple but vital. It proxies requests to the OpenRouter API to fetch credit balances. Why a backend function? Security. We never want to expose our master API keys (which have billing access) to the client-side extension code. The extension asks the backend "How much money do I have?", and the backend—using the secure server-side env vars—checks the bank.
Conclusion
Building "AI" features is easy. Building reliable AI features is hard. By breaking the backend into these atomic, single-purpose Edge Functions, I've built a system that is:
- Resilient: If "Cover Letter" goes down, "Research" still works.
- Specific: Each function has a prompt engineered for exactly one task.
- Fast: Parallel execution patterns reduce wait times from minutes to seconds.