[{"data":1,"prerenderedAt":475},["ShallowReactive",2],{"project-job-tracker":3,"project-job-tracker-more":200},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":7,"summary":9,"stack":10,"repo":17,"demo":7,"year":18,"role":19,"featured":6,"order":20,"body":21,"_type":194,"_id":195,"_source":196,"_file":197,"_stem":198,"_extension":199},"\u002Fprojects\u002Fjob-tracker","projects",false,"","Job-Application Tracker","A tracker for applications, interviews, contacts, and follow-ups that I actually use in my own job search. Node.js + TypeScript + Fastify + Prisma with JWT auth and a state machine for application status.",[11,12,13,14,15,16],"Node.js","TypeScript","Fastify","Prisma","PostgreSQL","Vitest","https:\u002F\u002Fgithub.com\u002FFabrizio-Alvarez\u002Fjob-tracker",2025,"Author",3,{"type":22,"children":23,"toc":187},"root",[24,33,39,45,120,126,135,176,182],{"type":25,"tag":26,"props":27,"children":29},"element","h2",{"id":28},"why-i-built-it",[30],{"type":31,"value":32},"text","Why I built it",{"type":25,"tag":34,"props":35,"children":36},"p",{},[37],{"type":31,"value":38},"I was looking for work and keeping my pipeline in a spreadsheet. Spreadsheets are\nterrible at reminders, status transitions, and \"when did I last hear back from\nthis company?\". So I built the tool I wanted — which is also the most defensible\nkind of side project: one I genuinely use (dogfooding).",{"type":25,"tag":26,"props":40,"children":42},{"id":41},"what-it-does",[43],{"type":31,"value":44},"What it does",{"type":25,"tag":46,"props":47,"children":48},"ul",{},[49,63,84,96,108],{"type":25,"tag":50,"props":51,"children":52},"li",{},[53,55,61],{"type":31,"value":54},"Tracks ",{"type":25,"tag":56,"props":57,"children":58},"strong",{},[59],{"type":31,"value":60},"applications",{"type":31,"value":62}," across companies, with role, source, and salary notes.",{"type":25,"tag":50,"props":64,"children":65},{},[66,68,73,75,82],{"type":31,"value":67},"Models status as an explicit ",{"type":25,"tag":56,"props":69,"children":70},{},[71],{"type":31,"value":72},"state machine",{"type":31,"value":74},": ",{"type":25,"tag":76,"props":77,"children":79},"code",{"className":78},[],[80],{"type":31,"value":81},"applied → screening → interview → offer | rejected",{"type":31,"value":83},".",{"type":25,"tag":50,"props":85,"children":86},{},[87,89,94],{"type":31,"value":88},"Logs ",{"type":25,"tag":56,"props":90,"children":91},{},[92],{"type":31,"value":93},"follow-ups",{"type":31,"value":95}," and surfaces the ones that are overdue.",{"type":25,"tag":50,"props":97,"children":98},{},[99,101,106],{"type":31,"value":100},"Keeps a ",{"type":25,"tag":56,"props":102,"children":103},{},[104],{"type":31,"value":105},"contacts",{"type":31,"value":107}," list (recruiters, hiring managers) linked to applications.",{"type":25,"tag":50,"props":109,"children":110},{},[111,113,118],{"type":31,"value":112},"Shows simple ",{"type":25,"tag":56,"props":114,"children":115},{},[116],{"type":31,"value":117},"funnel stats",{"type":31,"value":119}," so I can see where I'm losing candidates.",{"type":25,"tag":26,"props":121,"children":123},{"id":122},"architecture-decisions",[124],{"type":31,"value":125},"Architecture decisions",{"type":25,"tag":34,"props":127,"children":128},{},[129],{"type":25,"tag":130,"props":131,"children":134},"img",{"alt":132,"src":133},"Application status state machine","\u002Fdiagrams\u002Fjob-tracker-state.svg",[],{"type":25,"tag":46,"props":136,"children":137},{},[138,147,156,166],{"type":25,"tag":50,"props":139,"children":140},{},[141,145],{"type":25,"tag":56,"props":142,"children":143},{},[144],{"type":31,"value":13},{"type":31,"value":146}," over Express — schema-based validation out of the box and a tighter\ntyped surface, which matters when the whole point is correctness.",{"type":25,"tag":50,"props":148,"children":149},{},[150,154],{"type":25,"tag":56,"props":151,"children":152},{},[153],{"type":31,"value":14},{"type":31,"value":155}," for data access — typed queries and migrations without hand-rolling SQL.",{"type":25,"tag":50,"props":157,"children":158},{},[159,164],{"type":25,"tag":56,"props":160,"children":161},{},[162],{"type":31,"value":163},"Status as a state machine",{"type":31,"value":165},", not a free-text field — illegal transitions are\nrejected at the boundary, so the funnel stats are trustworthy.",{"type":25,"tag":50,"props":167,"children":168},{},[169,174],{"type":25,"tag":56,"props":170,"children":171},{},[172],{"type":31,"value":173},"JWT auth",{"type":31,"value":175}," with refresh — single-user today, but structured so multi-tenant\nisn't a rewrite.",{"type":25,"tag":26,"props":177,"children":179},{"id":178},"status",[180],{"type":31,"value":181},"Status",{"type":25,"tag":34,"props":183,"children":184},{},[185],{"type":31,"value":186},"Core flows are working and covered by Vitest tests, with CI green. The UI is\nintentionally out of scope for now — the value is the API and the data model.",{"title":7,"searchDepth":188,"depth":188,"links":189},2,[190,191,192,193],{"id":28,"depth":188,"text":32},{"id":41,"depth":188,"text":44},{"id":122,"depth":188,"text":125},{"id":178,"depth":188,"text":181},"markdown","content:projects:job-tracker.md","content","projects\u002Fjob-tracker.md","projects\u002Fjob-tracker","md",[201,327],{"_path":202,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":203,"description":7,"summary":204,"stack":205,"repo":210,"demo":211,"year":212,"role":19,"featured":213,"order":214,"body":215,"_type":194,"_id":324,"_source":196,"_file":325,"_stem":326,"_extension":199},"\u002Fprojects\u002Fmnemo","Mnemo","Spaced-repetition flashcards for my own studying — content lives in git-versioned Markdown, scheduling state (Anki SM-2) in Postgres. Live on its own subdomain, fully serverless on the free tier.",[12,206,207,14,208,16,209],"Next.js 15","Cloudflare Workers","PostgreSQL (Neon)","pnpm monorepo","https:\u002F\u002Fgithub.com\u002FFabrizio-Alvarez\u002Fmnemo","https:\u002F\u002Fmnemo.falvarez.dev",2026,true,1,{"type":22,"children":216,"toc":318},[217,223,228,245,251,296,302,307,313],{"type":25,"tag":26,"props":218,"children":220},{"id":219},"overview",[221],{"type":31,"value":222},"Overview",{"type":25,"tag":34,"props":224,"children":225},{},[226],{"type":31,"value":227},"A personal study app with flashcards, Anki-style. The content — 8 decks, 256 cards —\nlives in Markdown files versioned with git; the spaced-repetition state (Anki's SM-2\nalgorithm) lives in Postgres. It's in real daily use, which keeps every design\ndecision honest.",{"type":25,"tag":34,"props":229,"children":230},{},[231,236,238],{"type":25,"tag":56,"props":232,"children":233},{},[234],{"type":31,"value":235},"Live:",{"type":31,"value":237}," ",{"type":25,"tag":239,"props":240,"children":243},"a",{"href":211,"rel":241},[242],"nofollow",[244],{"type":31,"value":211},{"type":25,"tag":26,"props":246,"children":248},{"id":247},"architecture-hexagonal-monorepo",[249],{"type":31,"value":250},"Architecture: hexagonal monorepo",{"type":25,"tag":46,"props":252,"children":253},{},[254,268,286],{"type":25,"tag":50,"props":255,"children":256},{},[257,266],{"type":25,"tag":56,"props":258,"children":259},{},[260],{"type":25,"tag":76,"props":261,"children":263},{"className":262},[],[264],{"type":31,"value":265},"@mnemo\u002Fdomain",{"type":31,"value":267}," — the Markdown parser and the SM-2 scheduler as pure\nTypeScript: 67 tests, no database, no framework. The package travels intact to\nany runtime.",{"type":25,"tag":50,"props":269,"children":270},{},[271,276,278,284],{"type":25,"tag":56,"props":272,"children":273},{},[274],{"type":31,"value":275},"Content ≠ state",{"type":31,"value":277}," as the central decision: the ",{"type":25,"tag":76,"props":279,"children":281},{"className":280},[],[282],{"type":31,"value":283},".md",{"type":31,"value":285}," files are the source of\ntruth; the database is derived and idempotent — the seed can be re-run at any\ntime without losing scheduling state.",{"type":25,"tag":50,"props":287,"children":288},{},[289,294],{"type":25,"tag":56,"props":290,"children":291},{},[292],{"type":31,"value":293},"Full-serverless on the free tier",{"type":31,"value":295},": Cloudflare Workers (via OpenNext) + Neon\nPostgres, custom domain, CI\u002FCD on every push (GitHub Actions).",{"type":25,"tag":26,"props":297,"children":299},{"id":298},"beyond-flashcards-quiz-mode",[300],{"type":31,"value":301},"Beyond flashcards: quiz mode",{"type":25,"tag":34,"props":303,"children":304},{},[305],{"type":31,"value":306},"A didactic quiz mode where each question carries plausible, hand-written\ndistractors and a conceptual \"why\" explanation — wrong answers teach instead of\njust failing. Modes are extensions of the same domain, not separate apps: this is\nextensibility made flesh.",{"type":25,"tag":26,"props":308,"children":310},{"id":309},"why-this-project",[311],{"type":31,"value":312},"Why this project",{"type":25,"tag":34,"props":314,"children":315},{},[316],{"type":31,"value":317},"It's the project I use the most and the one that shows the full arc: domain\nmodeling (SM-2, content parsing), a real monorepo with strict boundaries, and\nproduction ops on my own domain — deployed, monitored, and paid for at US$0\u002Fmonth.",{"title":7,"searchDepth":188,"depth":188,"links":319},[320,321,322,323],{"id":219,"depth":188,"text":222},{"id":247,"depth":188,"text":250},{"id":298,"depth":188,"text":301},{"id":309,"depth":188,"text":312},"content:projects:mnemo.md","projects\u002Fmnemo.md","projects\u002Fmnemo",{"_path":328,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":329,"description":7,"summary":330,"stack":331,"repo":337,"demo":7,"year":18,"role":19,"featured":213,"order":188,"body":338,"_type":194,"_id":472,"_source":196,"_file":473,"_stem":474,"_extension":199},"\u002Fprojects\u002Fsupermarket-stock","Supermarket Stock System","Inventory management backend for a supermarket chain, built in Laravel with a framework-agnostic, domain-driven core. The domain is fully tested without Laravel — pure PHP.",[332,333,15,334,335,336],"Laravel 11","PHP 8.3","Pest","Docker","GitHub Actions","https:\u002F\u002Fgithub.com\u002FFabrizio-Alvarez\u002FEjercicio-Arquitectura",{"type":22,"children":339,"toc":466},[340,344,349,355,360,368,411,423,429,457,461],{"type":25,"tag":26,"props":341,"children":342},{"id":219},[343],{"type":31,"value":222},{"type":25,"tag":34,"props":345,"children":346},{},[347],{"type":31,"value":348},"A backend service for managing supermarket stock: products, stock levels, movements\n(receipts, sales, adjustments), and the rules around them. It's an architecture\nstudy first and a working service second — built to prove a clean, framework-agnostic\nPHP core that can survive a framework swap.",{"type":25,"tag":26,"props":350,"children":352},{"id":351},"architecture-ddd-in-layers",[353],{"type":31,"value":354},"Architecture: DDD in layers",{"type":25,"tag":34,"props":356,"children":357},{},[358],{"type":31,"value":359},"The code is split so the business logic never learns about Laravel:",{"type":25,"tag":34,"props":361,"children":362},{},[363],{"type":25,"tag":130,"props":364,"children":367},{"alt":365,"src":366},"DDD in layers","\u002Fdiagrams\u002Fsupermarket-stock-ddd.svg",[],{"type":25,"tag":46,"props":369,"children":370},{},[371,381,391,401],{"type":25,"tag":50,"props":372,"children":373},{},[374,379],{"type":25,"tag":56,"props":375,"children":376},{},[377],{"type":31,"value":378},"Domain",{"type":31,"value":380}," — pure PHP. Entities, value objects, and domain services. No framework\nimports, no Eloquent, no HTTP. This is the part tested in isolation.",{"type":25,"tag":50,"props":382,"children":383},{},[384,389],{"type":25,"tag":56,"props":385,"children":386},{},[387],{"type":31,"value":388},"Application",{"type":31,"value":390}," — use cases (handlers) that orchestrate the domain and declare\ntransactions and side effects as interfaces.",{"type":25,"tag":50,"props":392,"children":393},{},[394,399],{"type":25,"tag":56,"props":395,"children":396},{},[397],{"type":31,"value":398},"Infrastructure",{"type":31,"value":400}," — Eloquent repositories, framework adapters, and third-party\nclients implementing the Application-layer interfaces.",{"type":25,"tag":50,"props":402,"children":403},{},[404,409],{"type":25,"tag":56,"props":405,"children":406},{},[407],{"type":31,"value":408},"Presentation",{"type":31,"value":410}," — a REST API exposing the use cases.",{"type":25,"tag":34,"props":412,"children":413},{},[414,416,421],{"type":31,"value":415},"The headline is the ",{"type":25,"tag":56,"props":417,"children":418},{},[419],{"type":31,"value":420},"domain tested without the framework",{"type":31,"value":422},": if Laravel disappeared\ntomorrow, the business rules and their tests would compile green.",{"type":25,"tag":26,"props":424,"children":426},{"id":425},"whats-inside",[427],{"type":31,"value":428},"What's inside",{"type":25,"tag":46,"props":430,"children":431},{},[432,437,442,447,452],{"type":25,"tag":50,"props":433,"children":434},{},[435],{"type":31,"value":436},"Product catalogue and stock-level projections.",{"type":25,"tag":50,"props":438,"children":439},{},[440],{"type":31,"value":441},"Stock movements as an append-only ledger with domain invariants (no negative stock,\nconsistent totals).",{"type":25,"tag":50,"props":443,"children":444},{},[445],{"type":31,"value":446},"REST endpoints for the common operations, validated at the boundary.",{"type":25,"tag":50,"props":448,"children":449},{},[450],{"type":31,"value":451},"A Pest test suite covering domain rules and API behaviour.",{"type":25,"tag":50,"props":453,"children":454},{},[455],{"type":31,"value":456},"Dockerized local environment and a GitHub Actions pipeline running tests on every push.",{"type":25,"tag":26,"props":458,"children":459},{"id":309},[460],{"type":31,"value":312},{"type":25,"tag":34,"props":462,"children":463},{},[464],{"type":31,"value":465},"I spend my day job in PHP, so I wanted a public piece of work that shows how I think\nabout backend design in that language: clear boundaries, invariants enforced in the\ndomain, and infrastructure as a detail. DDD on a real (if small) domain is more\nhonest than yet another CRUD demo.",{"title":7,"searchDepth":188,"depth":188,"links":467},[468,469,470,471],{"id":219,"depth":188,"text":222},{"id":351,"depth":188,"text":354},{"id":425,"depth":188,"text":428},{"id":309,"depth":188,"text":312},"content:projects:supermarket-stock.md","projects\u002Fsupermarket-stock.md","projects\u002Fsupermarket-stock",1788209838301]