[{"data":1,"prerenderedAt":418},["ShallowReactive",2],{"project-01-field":3,"projects-all":136},{"id":4,"title":5,"body":6,"category":114,"cover":115,"date":116,"description":47,"extension":117,"meta":118,"navigation":119,"path":120,"role":121,"screens":122,"seo":125,"stack":126,"stem":131,"subtitle":132,"team":133,"year":134,"__hash__":135},"projects\u002Fprojects\u002F01-field.md","Field",{"type":7,"value":8,"toc":109},"minimark",[9,14,18,21,25,28,31,34,38,41,95,98,105],[10,11,13],"h2",{"id":12},"brief","Brief",[15,16,17],"p",{},"Field Monitor is a real-time telemetry dashboard for agricultural IoT sensors deployed at the network edge. Farmers and agronomists use it to track soil moisture, temperature, and humidity across multiple zones from a single interface.",[15,19,20],{},"The system handles thousands of concurrent sensor connections, persists time-series data efficiently, and delivers live updates to browser clients without polling.",[10,22,24],{"id":23},"process","Process",[15,26,27],{},"The project started with a proof-of-concept using WebSockets and PostgreSQL. Early load tests showed that relational storage couldn't keep up with the write throughput from 500+ concurrent sensors.",[15,29,30],{},"We migrated to ClickHouse for time-series persistence and introduced NATS as the message broker between edge agents and the backend API. The frontend moved to Server-Sent Events, which simplified client reconnection logic and reduced server overhead.",[15,32,33],{},"Deployment runs on bare-metal nodes at the farm co-location, with a lightweight Go agent compiled for ARMv7 on each sensor gateway.",[10,35,37],{"id":36},"tech-decisions","Tech Decisions",[15,39,40],{},"The core architectural challenge was decoupling sensor ingestion from dashboard delivery without introducing complex distributed systems.",[42,43,48],"pre",{"className":44,"code":45,"language":46,"meta":47,"style":47},"language-mermaid shiki shiki-themes github-light github-dark","flowchart LR\n    Sensor[\"IoT Sensor (ARM)\"] -->|MQTT| Gateway\n    Gateway -->|NATS Publish| Broker[\"NATS Broker\"]\n    Broker -->|Subscribe| API[\"Go API Server\"]\n    API -->|INSERT| CH[\"ClickHouse\"]\n    API -->|SSE| Browser[\"Vue Dashboard\"]\n    CH -->|Query| API\n","mermaid","",[49,50,51,59,65,71,77,83,89],"code",{"__ignoreMap":47},[52,53,56],"span",{"class":54,"line":55},"line",1,[52,57,58],{},"flowchart LR\n",[52,60,62],{"class":54,"line":61},2,[52,63,64],{},"    Sensor[\"IoT Sensor (ARM)\"] -->|MQTT| Gateway\n",[52,66,68],{"class":54,"line":67},3,[52,69,70],{},"    Gateway -->|NATS Publish| Broker[\"NATS Broker\"]\n",[52,72,74],{"class":54,"line":73},4,[52,75,76],{},"    Broker -->|Subscribe| API[\"Go API Server\"]\n",[52,78,80],{"class":54,"line":79},5,[52,81,82],{},"    API -->|INSERT| CH[\"ClickHouse\"]\n",[52,84,86],{"class":54,"line":85},6,[52,87,88],{},"    API -->|SSE| Browser[\"Vue Dashboard\"]\n",[52,90,92],{"class":54,"line":91},7,[52,93,94],{},"    CH -->|Query| API\n",[15,96,97],{},"NATS was chosen over Kafka because the message volume didn't justify Kafka's operational overhead, and NATS's at-most-once delivery was acceptable for live telemetry (missing one reading is fine; duplicating one is not).",[15,99,100,101,104],{},"ClickHouse's columnar storage and built-in time-bucketing functions (",[49,102,103],{},"toStartOfInterval",") made rollup queries fast without a separate aggregation pipeline.",[106,107,108],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":47,"searchDepth":61,"depth":61,"links":110},[111,112,113],{"id":12,"depth":61,"text":13},{"id":23,"depth":61,"text":24},{"id":36,"depth":61,"text":37},"system","\u002Fimages\u002Fprojects\u002Ffield-cover.jpg","2024-06-01","md",{},true,"\u002Fprojects\u002F01-field","Backend Lead",[123,124],"\u002Fimages\u002Fprojects\u002Ffield-screen-1.jpg","\u002Fimages\u002Fprojects\u002Ffield-screen-2.jpg",{"title":5,"description":47},[127,128,129,130],"Go","NATS","ClickHouse","Vue 3","projects\u002F01-field","— design system","3 engineers",2024,"SF5d6pcMyy-L_fIPkBofcNbAKF-Awa5aSmJI5tNunDE",[137,167,272,296,366,393],{"id":138,"title":139,"body":140,"category":150,"cover":151,"date":152,"description":47,"extension":117,"meta":153,"navigation":119,"path":154,"role":155,"screens":156,"seo":157,"stack":158,"stem":162,"subtitle":163,"team":164,"year":165,"__hash__":166},"projects\u002Fprojects\u002F06-hako.md","Hako",{"type":7,"value":141,"toc":148},[142],[143,144,145],"blockquote",{},[15,146,147],{},"WIP — full case study coming.",{"title":47,"searchDepth":61,"depth":61,"links":149},[],"identity","\u002Fimages\u002Fprojects\u002Fhako-cover.jpg","2022-08-01",{},"\u002Fprojects\u002F06-hako","Designer",[],{"title":139,"description":47},[159,160,161],"Illustrator","After Effects","Blender","projects\u002F06-hako","— packaging studio","Solo",2022,"A-l3Qn-Zp0iTMnV-AN7aIKKweKTUdCOWpbt8IuX3rk0",{"id":168,"title":169,"body":170,"category":252,"cover":253,"date":254,"description":47,"extension":117,"meta":255,"navigation":119,"path":256,"role":257,"screens":258,"seo":261,"stack":262,"stem":267,"subtitle":268,"team":269,"year":270,"__hash__":271},"projects\u002Fprojects\u002F02-hina.md","Hina",{"type":7,"value":171,"toc":247},[172,174,177,180,182,185,188,191,193,196,231,242,245],[10,173,13],{"id":12},[15,175,176],{},"Hina is a multi-tenant SaaS platform that lets language learning studios manage courses, students, and assignments from a shared infrastructure while keeping tenant data strictly isolated.",[15,178,179],{},"Studio owners configure their subdomain, branding, and curriculum; students access their portal through the branded URL. The platform handles billing, seat limits, and content delivery.",[10,181,24],{"id":23},[15,183,184],{},"The initial architecture used row-level tenant isolation in a single PostgreSQL database. As the tenant count grew past 30, query plans started ignoring indexes due to bloated statistics.",[15,186,187],{},"We migrated to a schema-per-tenant model. Each new tenant gets a dedicated PostgreSQL schema provisioned automatically at signup via a Cloudflare Worker that runs Flyway migrations. This kept query plans predictable and simplified data residency requirements for European customers.",[15,189,190],{},"The frontend is a Nuxt 3 app deployed to Cloudflare Pages, with a shared component library published as a private npm package consumed by each tenant's customizable shell.",[10,192,37],{"id":36},[15,194,195],{},"The key design challenge was enabling tenant customization without forking the codebase.",[42,197,199],{"className":44,"code":198,"language":46,"meta":47,"style":47},"flowchart TD\n    DNS[\"*.studiodomain.com\"] -->|Wildcard DNS| CF[\"Cloudflare Pages\"]\n    CF -->|Request| Worker[\"Edge Worker (tenant resolution)\"]\n    Worker -->|Tenant config| App[\"Nuxt SSR App\"]\n    App -->|schema=tenant_id| PG[\"PostgreSQL (schema-per-tenant)\"]\n    App -->|Provision| MigWorker[\"Migration Worker (Flyway)\"]\n",[49,200,201,206,211,216,221,226],{"__ignoreMap":47},[52,202,203],{"class":54,"line":55},[52,204,205],{},"flowchart TD\n",[52,207,208],{"class":54,"line":61},[52,209,210],{},"    DNS[\"*.studiodomain.com\"] -->|Wildcard DNS| CF[\"Cloudflare Pages\"]\n",[52,212,213],{"class":54,"line":67},[52,214,215],{},"    CF -->|Request| Worker[\"Edge Worker (tenant resolution)\"]\n",[52,217,218],{"class":54,"line":73},[52,219,220],{},"    Worker -->|Tenant config| App[\"Nuxt SSR App\"]\n",[52,222,223],{"class":54,"line":79},[52,224,225],{},"    App -->|schema=tenant_id| PG[\"PostgreSQL (schema-per-tenant)\"]\n",[52,227,228],{"class":54,"line":85},[52,229,230],{},"    App -->|Provision| MigWorker[\"Migration Worker (Flyway)\"]\n",[15,232,233,234,237,238,241],{},"The edge worker resolves the tenant from the hostname, injects a config header (",[49,235,236],{},"X-Tenant-ID",", ",[49,239,240],{},"X-Tenant-Theme","), and forwards to the Nuxt app. The app reads the header to select the correct PostgreSQL schema and apply the tenant's design tokens at runtime.",[15,243,244],{},"This avoided a per-tenant deployment model while keeping strict data isolation — a deliberate trade-off between operational simplicity and isolation granularity.",[106,246,108],{},{"title":47,"searchDepth":61,"depth":61,"links":248},[249,250,251],{"id":12,"depth":61,"text":13},{"id":23,"depth":61,"text":24},{"id":36,"depth":61,"text":37},"product","\u002Fimages\u002Fprojects\u002Fhina-cover.jpg","2023-09-01",{},"\u002Fprojects\u002F02-hina","Fullstack Engineer",[259,260],"\u002Fimages\u002Fprojects\u002Fhina-screen-1.jpg","\u002Fimages\u002Fprojects\u002Fhina-screen-2.jpg",{"title":169,"description":47},[263,264,265,266],"TypeScript","Nuxt 3","PostgreSQL","Cloudflare Workers","projects\u002F02-hina","— payments console","5 engineers",2023,"tZlDVwK87s7WoKVojiW0iW7EnuUcJSrbRLrQ3-o7iks",{"id":273,"title":274,"body":275,"category":283,"cover":284,"date":285,"description":47,"extension":117,"meta":286,"navigation":119,"path":287,"role":155,"screens":288,"seo":289,"stack":290,"stem":293,"subtitle":294,"team":164,"year":134,"__hash__":295},"projects\u002Fprojects\u002F03-kata.md","Kata",{"type":7,"value":276,"toc":281},[277],[143,278,279],{},[15,280,147],{},{"title":47,"searchDepth":61,"depth":61,"links":282},[],"side","\u002Fimages\u002Fprojects\u002Fkata-cover.jpg","2024-03-01",{},"\u002Fprojects\u002F03-kata",[],{"title":274,"description":47},[291,292,263],"Figma","CSS","projects\u002F03-kata","— type specimen","oQ-K7hD_hsnEC-1mj_A3Ln2vjVvfRXx8RN1mIRS031o",{"id":4,"title":5,"body":297,"category":114,"cover":115,"date":116,"description":47,"extension":117,"meta":362,"navigation":119,"path":120,"role":121,"screens":363,"seo":364,"stack":365,"stem":131,"subtitle":132,"team":133,"year":134,"__hash__":135},{"type":7,"value":298,"toc":357},[299,301,303,305,307,309,311,313,315,317,349,351,355],[10,300,13],{"id":12},[15,302,17],{},[15,304,20],{},[10,306,24],{"id":23},[15,308,27],{},[15,310,30],{},[15,312,33],{},[10,314,37],{"id":36},[15,316,40],{},[42,318,319],{"className":44,"code":45,"language":46,"meta":47,"style":47},[49,320,321,325,329,333,337,341,345],{"__ignoreMap":47},[52,322,323],{"class":54,"line":55},[52,324,58],{},[52,326,327],{"class":54,"line":61},[52,328,64],{},[52,330,331],{"class":54,"line":67},[52,332,70],{},[52,334,335],{"class":54,"line":73},[52,336,76],{},[52,338,339],{"class":54,"line":79},[52,340,82],{},[52,342,343],{"class":54,"line":85},[52,344,88],{},[52,346,347],{"class":54,"line":91},[52,348,94],{},[15,350,97],{},[15,352,100,353,104],{},[49,354,103],{},[106,356,108],{},{"title":47,"searchDepth":61,"depth":61,"links":358},[359,360,361],{"id":12,"depth":61,"text":13},{"id":23,"depth":61,"text":24},{"id":36,"depth":61,"text":37},{},[123,124],{"title":5,"description":47},[127,128,129,130],{"id":367,"title":368,"body":369,"category":252,"cover":377,"date":378,"description":47,"extension":117,"meta":379,"navigation":119,"path":380,"role":381,"screens":382,"seo":383,"stack":384,"stem":388,"subtitle":389,"team":390,"year":391,"__hash__":392},"projects\u002Fprojects\u002F04-mori.md","Mori",{"type":7,"value":370,"toc":375},[371],[143,372,373],{},[15,374,147],{},{"title":47,"searchDepth":61,"depth":61,"links":376},[],"\u002Fimages\u002Fprojects\u002Fmori-cover.jpg","2025-01-01",{},"\u002Fprojects\u002F04-mori","Frontend Engineer",[],{"title":368,"description":47},[263,385,386,387],"Mapbox GL","React","Supabase","projects\u002F04-mori","— map editor","4 engineers",2025,"JVb6P1tYaPxu2ThY1yt3g8SCEyoQOfahj4wBY0l2Zms",{"id":394,"title":395,"body":396,"category":252,"cover":404,"date":405,"description":47,"extension":117,"meta":406,"navigation":119,"path":407,"role":408,"screens":409,"seo":410,"stack":411,"stem":415,"subtitle":416,"team":133,"year":391,"__hash__":417},"projects\u002Fprojects\u002F05-foglight.md","Foglight",{"type":7,"value":397,"toc":402},[398],[143,399,400],{},[15,401,147],{},{"title":47,"searchDepth":61,"depth":61,"links":403},[],"\u002Fimages\u002Fprojects\u002Ffoglight-cover.jpg","2025-05-01",{},"\u002Fprojects\u002F05-foglight","Platform Engineer",[],{"title":395,"description":47},[127,412,413,414],"Prometheus","Grafana","Kubernetes","projects\u002F05-foglight","— observability","eMl4j-uS1Z3VCswCpdYfg-efhBJBkUpbg1dQ2lVZk1Q",1780894152866]