[{"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},"\u002Fes\u002Fprojects\u002Fjob-tracker","projects",false,"","Tracker de Aplicaciones Laborales","Un tracker para aplicaciones, entrevistas, contactos y follow-ups que efectivamente uso en mi propia búsqueda laboral. Node.js + TypeScript + Fastify + Prisma con JWT auth y una máquina de estados para el status de la aplicación.",[11,12,13,14,15,16],"Node.js","TypeScript","Fastify","Prisma","PostgreSQL","Vitest","https:\u002F\u002Fgithub.com\u002FFabrizio-Alvarez\u002Fjob-tracker",2025,"Autor",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},"por-qué-lo-construí",[30],{"type":31,"value":32},"text","Por qué lo construí",{"type":25,"tag":34,"props":35,"children":36},"p",{},[37],{"type":31,"value":38},"Estaba buscando trabajo y llevando mi pipeline en una planilla. Las planillas son\npésimas para recordatorios, transiciones de status y \"¿cuándo fue la última vez\nque me respondió esta empresa?\". Por eso construí la herramienta que quería —\nque también es el tipo de side project más defendible: uno que efectivamente\nuso (dogfooding).",{"type":25,"tag":26,"props":40,"children":42},{"id":41},"qué-hace",[43],{"type":31,"value":44},"Qué hace",{"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},"Trackea ",{"type":25,"tag":56,"props":57,"children":58},"strong",{},[59],{"type":31,"value":60},"aplicaciones",{"type":31,"value":62}," por empresa, con rol, fuente y notas de salario.",{"type":25,"tag":50,"props":64,"children":65},{},[66,68,73,75,82],{"type":31,"value":67},"Modela el status como una ",{"type":25,"tag":56,"props":69,"children":70},{},[71],{"type":31,"value":72},"máquina de estados",{"type":31,"value":74}," explícita: ",{"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},"Registra ",{"type":25,"tag":56,"props":90,"children":91},{},[92],{"type":31,"value":93},"follow-ups",{"type":31,"value":95}," y marca los que están vencidos.",{"type":25,"tag":50,"props":97,"children":98},{},[99,101,106],{"type":31,"value":100},"Mantiene una lista de ",{"type":25,"tag":56,"props":102,"children":103},{},[104],{"type":31,"value":105},"contactos",{"type":31,"value":107}," (recruiters, hiring managers) vinculados a las aplicaciones.",{"type":25,"tag":50,"props":109,"children":110},{},[111,113,118],{"type":31,"value":112},"Muestra ",{"type":25,"tag":56,"props":114,"children":115},{},[116],{"type":31,"value":117},"estadísticas de funnel",{"type":31,"value":119}," simples para ver dónde se pierden candidatos.",{"type":25,"tag":26,"props":121,"children":123},{"id":122},"decisiones-de-arquitectura",[124],{"type":31,"value":125},"Decisiones de arquitectura",{"type":25,"tag":34,"props":127,"children":128},{},[129],{"type":25,"tag":130,"props":131,"children":134},"img",{"alt":132,"src":133},"State machine de status de la aplicación","\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}," por sobre Express — validación basada en esquemas out of the box\ny una superficie tipada más ajustada, lo cual importa cuando el punto central\nes la corrección.",{"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}," para acceso a datos — queries tipadas y migraciones sin escribir SQL\na mano.",{"type":25,"tag":50,"props":157,"children":158},{},[159,164],{"type":25,"tag":56,"props":160,"children":161},{},[162],{"type":31,"value":163},"Status como máquina de estados",{"type":31,"value":165},", no un campo de texto libre — las transiciones\nilegales se rechazan en el límite, así las estadísticas de funnel son confiables.",{"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}," con refresh — single-user por ahora, pero estructurado para que\nel multi-tenant no sea un 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},"Los flujos centrales funcionan y están cubiertos por tests de Vitest, con CI en\nverde. La UI está intencionalmente fuera de scope por ahora — el valor está en\nla API y el modelo de datos.",{"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:es:projects:job-tracker.md","content","es\u002Fprojects\u002Fjob-tracker.md","es\u002Fprojects\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},"\u002Fes\u002Fprojects\u002Fmnemo","Mnemo","Flashcards con repetición espaciada para mi propio estudio — el contenido vive en Markdown versionado con git, el estado SRS (SM-2 de Anki) en Postgres. Live en su propio subdominio, full-serverless en el 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},"descripción-general",[221],{"type":31,"value":222},"Descripción general",{"type":25,"tag":34,"props":224,"children":225},{},[226],{"type":31,"value":227},"Una app personal de estudio con tarjetas, estilo Anki. El contenido — 8 mazos,\n256 tarjetas — vive en archivos Markdown versionados con git; el estado de\nrepetición espaciada (algoritmo SM-2 de Anki) vive en Postgres. Está en uso real\ndiario, y eso mantiene honesta cada decisión de diseño.",{"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},"arquitectura-monorepo-hexagonal",[249],{"type":31,"value":250},"Arquitectura: monorepo hexagonal",{"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}," — el parser de Markdown y el scheduler SM-2 como TypeScript\npuro: 67 tests, sin base de datos, sin framework. El paquete viaja intacto a\ncualquier 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},"Contenido ≠ estado",{"type":31,"value":277}," como decisión central: los ",{"type":25,"tag":76,"props":279,"children":281},{"className":280},[],[282],{"type":31,"value":283},".md",{"type":31,"value":285}," son la fuente de verdad;\nla base de datos es derivada e idempotente — el seed se puede re-correr en\ncualquier momento sin perder el estado de scheduling.",{"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 en el free tier",{"type":31,"value":295},": Cloudflare Workers (vía OpenNext) + Neon\nPostgres, dominio propio, CI\u002FCD en cada push (GitHub Actions).",{"type":25,"tag":26,"props":297,"children":299},{"id":298},"más-allá-de-las-flashcards-modo-quiz",[300],{"type":31,"value":301},"Más allá de las flashcards: modo quiz",{"type":25,"tag":34,"props":303,"children":304},{},[305],{"type":31,"value":306},"Un modo quiz didáctico donde cada pregunta trae distractores plausibles escritos a\nmano y una explicación conceptual del \"por qué\" — las respuestas incorrectas\nenseñan en lugar de solo fallar. Los modos son extensiones del mismo dominio, no\napps separadas: extensibilidad hecha carne.",{"type":25,"tag":26,"props":308,"children":310},{"id":309},"por-qué-este-proyecto",[311],{"type":31,"value":312},"Por qué este proyecto",{"type":25,"tag":34,"props":314,"children":315},{},[316],{"type":31,"value":317},"Es el proyecto que más uso y el que muestra el arco completo: modelado de dominio\n(SM-2, parsing de contenido), un monorepo real con límites estrictos, y operación\nen producción en mi propio dominio — deployado, monitoreado, y a US$0\u002Fmes.",{"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:es:projects:mnemo.md","es\u002Fprojects\u002Fmnemo.md","es\u002Fprojects\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},"\u002Fes\u002Fprojects\u002Fsupermarket-stock","Sistema de Stock de Supermercado","Backend de gestión de inventario para una cadena de supermercados, construido en Laravel con un núcleo framework-agnostic y domain-driven. El dominio se testea por completo sin Laravel — PHP puro.",[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},"Un servicio backend para gestionar el stock de un supermercado: productos, niveles\nde stock, movimientos (recepciones, ventas, ajustes) y las reglas asociadas. Es\nprimero un estudio de arquitectura y segundo un servicio funcional — construido\npara demostrar un núcleo PHP limpio, framework-agnostic, capaz de sobrevivir a un\ncambio de framework.",{"type":25,"tag":26,"props":350,"children":352},{"id":351},"arquitectura-ddd-en-capas",[353],{"type":31,"value":354},"Arquitectura: DDD en capas",{"type":25,"tag":34,"props":356,"children":357},{},[358],{"type":31,"value":359},"El código está estructurado de forma que la lógica de negocio nunca dependa de\nLaravel:",{"type":25,"tag":34,"props":361,"children":362},{},[363],{"type":25,"tag":130,"props":364,"children":367},{"alt":365,"src":366},"DDD en capas","\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},"Dominio",{"type":31,"value":380}," — PHP puro. Entidades, value objects y servicios de dominio. Sin\nimports del framework, sin Eloquent, sin HTTP. Esta es la parte que se testea\nde forma aislada.",{"type":25,"tag":50,"props":382,"children":383},{},[384,389],{"type":25,"tag":56,"props":385,"children":386},{},[387],{"type":31,"value":388},"Aplicación",{"type":31,"value":390}," — casos de uso (handlers) que orquestan el dominio y declaran\ntransacciones y side effects como interfaces.",{"type":25,"tag":50,"props":392,"children":393},{},[394,399],{"type":25,"tag":56,"props":395,"children":396},{},[397],{"type":31,"value":398},"Infraestructura",{"type":31,"value":400}," — repositorios Eloquent, adaptadores del framework y\nclientes de terceros que implementan las interfaces de la capa de Aplicación.",{"type":25,"tag":50,"props":402,"children":403},{},[404,409],{"type":25,"tag":56,"props":405,"children":406},{},[407],{"type":31,"value":408},"Presentación",{"type":31,"value":410}," — una REST API que expone los casos de uso.",{"type":25,"tag":34,"props":412,"children":413},{},[414,416,421],{"type":31,"value":415},"Lo más importante es el ",{"type":25,"tag":56,"props":417,"children":418},{},[419],{"type":31,"value":420},"dominio testeado sin el framework",{"type":31,"value":422},": si Laravel\ndesapareciera mañana, las reglas de negocio y sus tests pasarían en verde.",{"type":25,"tag":26,"props":424,"children":426},{"id":425},"qué-incluye",[427],{"type":31,"value":428},"Qué incluye",{"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},"Catálogo de productos y proyecciones de niveles de stock.",{"type":25,"tag":50,"props":438,"children":439},{},[440],{"type":31,"value":441},"Movimientos de stock como un ledger append-only con invariantes de dominio (sin\nstock negativo, totales consistentes).",{"type":25,"tag":50,"props":443,"children":444},{},[445],{"type":31,"value":446},"Endpoints REST para las operaciones comunes, validados en el límite.",{"type":25,"tag":50,"props":448,"children":449},{},[450],{"type":31,"value":451},"Un suite de tests en Pest que cubre las reglas del dominio y el comportamiento\nde la API.",{"type":25,"tag":50,"props":453,"children":454},{},[455],{"type":31,"value":456},"Entorno local dockerizado y un pipeline de GitHub Actions que ejecuta los tests\nen cada 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},"Trabajo día a día en PHP, así que quise tener un proyecto público que muestre cómo\npienso el diseño de backend en ese lenguaje: límites claros, invariantes enforced\nen el dominio, e infraestructura como detalle. DDD sobre un dominio real (aunque\nchico) es más honesto que otra demo CRUD más.",{"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:es:projects:supermarket-stock.md","es\u002Fprojects\u002Fsupermarket-stock.md","es\u002Fprojects\u002Fsupermarket-stock",1788209838604]