Skills
UX & Systems design
AI-assisted development
Full-stack build
Iterative testing
My Role
Solo designer, researcher, builder
Tools
Frontend: React, Vite, Tailwind CSS
Backend: Node.js, Express
Infrastructure: Supabase (Postgres), Render, GitHub Actions (CI/CD)
AI integration: Claude, Model Context Protocol (MCP), Gmail API
Industry
Personal productivity tool
Problem
After hundreds of job applications, my tracking spreadsheet had become its own part-time job.
Tools like Excel, Google Sheets, Teal, Huntr organize beautifully, but I still either enter or update everything by hand.
My motivation for this project
Personal frustration
No automation meant I had to manually track application data -> extremely time-consuming.
I wanted to maximize my application volume. Each day.
And learning how to build with AI.
So I made a betโฆ
ONE guiding principle, ONE thing I'd measure every decision against:
Terminal CLI triggers run
AppLog portal receives live updates
So, what is AppLog AI?
A smart, LLM-powered tracker I built to read the job-application emails that matter โ confirmations, interviews, rejections, or the silence of being ghosted. Instead of manually checking and updating each one, I type a single command and it reads my inbox and updates everything itself.
Almost like a personal assistant who watches my inbox and manages the tracking.
On the ambiguous cases โ a merge that isn't clear-cut โ it pauses and asks me to make the call.
One command
CLI
Reads my inbox
Gmail API
Extracts details
LLM parser
Asks me when unsure
human-in-the-loop review
Stores data
Supabase / Postgres
Updates live
React portal
These came before the process, because they were the lens on it.
Solo, by design
No engineer co-parts. I was designer, builder, and tester at once.
The ceiling
No system can extract a company name that appears in none of the emails. Some applications are genuinely unrecoverable by machine โ a limit I had to design around, not engineer away.
Real data == real stakes
A wrong merge wasn't a bug, it was a job I'd never know I lost.
The Process
Build a MVP.
Test with real data.
Then iterate.
Repeat.
I made a scrappy version fast.

Rough kanban portal, containing:
LocalStorage: application data on my computer only.
Columns to sort applications by where each one stood.
The email parser
Pulled company, role, and status out of my Gmail โ the piece meant to replace manual data entry.
Database setup via Supabase
The permanent home for every application row, so the data could live somewhere real instead of one browser.
Express API on Render
The middleman that let the portal and the parser read from and write to that database.
Backups + approval gates
A safety net so I could break things and roll back without fear โ built in first, before anything risky.
332 real application emails.
I was my own user: n=1, I learned by building.
It was the usability test.
Then it brokeโฆ
It "failed" in the most useful way.
120+ applications came back blank, and others came back confidently wrong
How it broke
What the parser saw
Input
Globalhr@myworkday.com
(No employer anywhere in email)
Sender display name: "System System"
The raw signals it tripped on
What it returned (wrong)
Output
Globalhr
Unspecified
System System
not the company
employer unidentified
sender scraps
The junk it produced
The parser I designed read the "envelope", not the "letter".
Email domains were parsed as company names.
Sender fragments (AI-generated) were also read as employers.
This informed my next step
Iteration #1
The reqID veto
Finding
My dedup logic saw 2 different roles at the same company as identical and silently merged them, with no flags raised.
After investigating, the only thing telling them apart was a requisition ID buried in the job listing.
Decision
For my parser moving forward, I made the reqID a deciding factor: different IDs = "separate applications, never merge".
The reqID now has vetoing power to override the previous parser logic.
Why this design
A tracker that quietly hides a real data point loses my trust.
I'd rather err toward keeping 2 rows that might be duplicates (which I can clean up later), than merging 2 that can cause invisible loss.
Before - merged into one row

After - all 3 rows preserved

Iteration #2
Hitting the parser ceiling
Finding
Some emails name no employer anywhere (not in subject, sender, or email body).
Parser cannot extract what isn't there. I'd hit a limit that more engineering couldn't cross.
Decision
Creating a watermark / an improved parser that runs a complete pipeline:
Have LLM flag the few ceiling cases ->
I hand-review and correct them ->
Only new mail syncs moving forward.
Why this design
The improved parser runs efficiently, with my approval on the calls that matter, and never re-fights corrections I've already made.
Before - parser hit the ceiling

There was nothing to extract - employer is named nowhere in the email
After - corrected by hand

The watermark keeps the parser from ever overwriting it
Reflections
This is an ongoing project (summer '26), but the payoff has been tremendous.
Learned how to build
Beyond UX design, but to think in layers. Frontend, backend, database, pipeline. How each depends on another and the way an engineer scopes a system before touching code.
Built a tool I'll actually use
It saves real time in my job search, and the systems thinking transfers to any team where I'm the designer working alongside engineers.
Learned to stop at "trustworthy," not "perfect"
Chasing the last few unrecoverable rows would've cost days for no real gain. Having the discipline to stop is its own skill.
Did the bet pay off?

