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