Engineering deep-dive
Architecture
The interesting engineering problem in AI Job Agent was never "can a bot fill out forms." It was building a system with enough process isolation and typed boundaries that a crash, a bad model call, or a stale selector on some ATS's DOM can't cascade into something submitting on the applicant's behalf. This page covers the process topology, the privacy boundaries, and how it's tested.
Process topology
One rule governs everything below: the Electron main process never imports the backend. Only worker processes may; everything else reaches the backend by naming a procedure in a typed contract and sending it over IPC. The rule is enforced by a custom ESLint restriction, not convention — because every reason it exists is invisible at the call site.
main (Electron) — windows, menus, IPC bridge, CSP. Imports NO backend.
├─ agent host — long-lived. Every backend call goes through here.
├─ apply worker — ephemeral, one per application run.
├─ run worker — ephemeral, one per discover→score→tailor graph run.
└─ session workers — ephemeral, one per assisted browser session.
renderer — React. contextIsolation + sandbox + no nodeIntegration.
└─ preload (CJS) — one narrow bridge: call / hostStatus / onHostStatus.Every capability the app has grown since — a daily unattended scan, job-alert email ingestion, an assisted browser handoff — has been added as one more worker under this same rule, not an exception to it. Adding a capability means adding a procedure to a typed registry: deliberately more friction than adding an import, and the friction is the feature.
Why the boundary exists
The backend is a CLI at heart: its environment validation throws atimport time if a required key is missing, and its database client opens the SQLite file at import too. Both are correct behavior for a CLI — failing loudly at startup beats surfacingundefined three layers deep, mid-run. But in a desktop app, a first-run user with no API key yet would get a process that dies before a window exists — nowhere to even render the explanation. Routing every backend call through one isolated, dynamically-imported worker with an explicit ready/failed handshake keeps that failure visible instead of fatal.
Privacy boundaries
Contact data excluded from model prompts
Name, email, and phone never enter model context — they don't improve fit analysis and flow straight from the local profile into form fields instead.
OS-backed credential storage
Provider API keys are stored via Electron's safeStorage (macOS Keychain, Windows DPAPI, Linux libsecret). The renderer only ever sees a configured/not-configured flag and a masked suffix — never the key.
Session directories are a trust boundary, not a setting
A persistent browser-profile path is decided by exactly one function in the main process, from the OS user-data directory alone. It is never sent to, or accepted from, the renderer, an environment variable, or a Settings field.
Redacted logging, twice over
A path-based logger denylist redacts sensitive fields; a second, independent sanitizer strips credential-shaped strings, emails, phone numbers, and filesystem paths before anything is written to disk. Diagnostics exports get a third, independent redaction pass.
Explicit, manual retention
Nothing is deleted automatically. A dry-run/confirm purge command removes old artifacts and completed-run checkpoints; the main application database is deliberately kept to prevent duplicate applications.
Local-first is not the same claim as encrypted-everywhere: the data inventory still depends on device security and retention discipline, and filled-form screenshots are the densest PII artifact the app produces. That tradeoff is documented, not hidden.
Safety controls
- ▸Human review before submission — a durable pause, enabled by default, backed by a checkpoint on disk.
- ▸No automatic submission, ever. ATS adapters have no submit method; a separate, explicit action records that a human submitted.
- ▸Bounded blast radius — a hard cap on applications per run, and a retry limit for failures.
- ▸Destination allowlisting — only absolute HTTPS URLs on approved ATS hosts, validated before navigation and again after redirects settle.
- ▸Per-application browser isolation for standard ATS applications — a fresh context per application; cookies, storage, and cache are discarded afterward, including on failure paths. Naukri assisted handoff is the deliberate exception: it holds a single, leased persistent profile (a login session has to survive across applications), guarded by a single-holder lease rather than per-application isolation.
- ▸Fail closed on unknown platforms — an unrecognized ATS hands back to manual application rather than attempting a best-effort fill.
- ▸Post-fill verification — field values are re-read after the form settles, so a write that silently reverted is never reported as filled.
- ▸No CAPTCHA/MFA/anti-bot bypass — no login automation, stealth fingerprinting, or anti-bot evasion exists in the codebase.
Testing & release gates
290+ test files across the backend and desktop packages as of August 2026, run through a pull-request gate that includes:
- Build, typecheck, ESLint, and Prettier formatting
- Backend unit/integration suite
- Desktop unit/component/integration suite
- Electron end-to-end suite (renderer, preload, IPC, main, SQLite integration, hermetic session behavior)
- Axe accessibility checks
- Full-history secret scanning (Gitleaks)
- Packaged-executable smoke test, including first-run migration, on each native OS
Release testing has caught real problems this way — a packaging-only failure from a missing first-run migration entrypoint, a raw-key export path in diagnostics closed by a second independent redaction pass, and a logger stress test that caught catastrophic regex backtracking reachable through untrusted job description text.