ApplyOS
The local-first job hunt command center
Why this exists
Applying to jobs means re-typing the same 40 fields — name, links, notice period, "why us?" — hundreds of times. Then losing track of which company you applied to, which email you used on Naukri, and whether you already sent that cover letter to this recruiter three weeks ago.
The tools that exist are either SaaS platforms that want your data and a subscription, or spreadsheets that don’t help you fill the form. Neither solves the actual problem.
ApplyOS started as a personal itch: store every answer once, resolve template variables at copy-time, and have a browser extension that fills the form in one click. The local-first constraint came from the same instinct — job-hunt data is sensitive (where you’re interviewing, your salary expectations, your visa status), and I didn’t want to hand it to a server.
It ended up being a system I use every day. So I finished it properly.
Build timeline
Ideation
Applied to my 200th job. Typed my notice period for the 200th time. Opened a new file and wrote: 'what if I only had to type this once?' That was the brief.
Design & scoping
Decided on local-first (no backend, no accounts, no trust required). Mapped the core loop: store once → tailor fast → fill automatically → track everything. Designed the Vault schema and the field-scoring approach for autofill.
MVP
Basic Vault + snippet system working. Chrome extension filling Greenhouse and Lever forms. Template variables resolving at copy-time. It was janky, but it worked on my machine and saved me 20 minutes the same day.
The part that broke everything
React-controlled inputs silently ignored direct value writes. Three days debugging before I found the native prototype value setter trick — the same mechanism real browser autofill uses. Also discovered that 6 of the 9 target ATSs had some variation of this problem. The fix generalized to all of them.
AI layer + Tailor module
Added offline keyword extraction (100+ skill dictionary, deterministic, no API needed) and BYOK Anthropic integration. Designed the graceful degradation path so the app is 100% useful without an API key — AI is additive, not required.
v2: Multi-profile + encrypted passwords + auto-capture
Added multiple personas (Dev roles vs. Analyst roles) with per-profile overrides and conversion analytics. Built the AES-256-GCM credential vault with PBKDF2 key derivation and in-memory auto-lock. Extension now auto-captures company/role via JSON-LD → URL patterns → DOM fallback.
Polish & launch
Dexie v1→v2 lossless migration. Duplicate warning for re-applications. Passphrase rotation with full re-encryption. Docs, cleanup, and the README that actually explains how the extension sync works.
The interesting technical decisions
Heuristic autofill that generalizes
Instead of site-specific scrapers, every input is scored across 7 signal types (label, placeholder, name, id, aria-label, aria-describedby, autocomplete). Highest scorer wins. This means it works on ATSs it has never seen, and doesn't break when one changes its CSS class names.
React-proof form filling
React-controlled inputs ignore direct value writes. The extension writes through the native prototype's value descriptor and dispatches input + change + blur events — the same mechanism real browser autofill uses. Every framework treats it as a real keystroke.
Zero-config extension sync
No extension IDs, no native messaging, no backend. The web app broadcasts vault data via window.postMessage; a bridge content script on localhost:3000 copies it into chrome.storage. The extension reads from chrome.storage. Two moving parts, zero configuration.
Structured data–first job capture
Most ATS pages embed JSON-LD JobPosting markup for Google Jobs indexing. The extension parses that first (exact company/role), falls back to URL slug patterns, then falls back to the page's h1. Captures queue in chrome.storage and drain into IndexedDB the next time the app opens.
Crypto done properly
PBKDF2-SHA256 (310k iterations) → AES-256-GCM with a fresh random IV per secret. The derived key is non-extractable, in-memory only, auto-wiped after 5 idle minutes. No recovery path by design. A canary ciphertext confirms the passphrase on unlock so 'wrong passphrase' is a specific error.
Tech stack
Want to build something?
Have a feature idea, a collaboration in mind, or just want to talk shop? I’m easy to reach.