[{"data":1,"prerenderedAt":1112},["ShallowReactive",2],{"blog-\u002Fblog\u002Fseeing-the-world-as-who-we-are":3,"blog-nav":66},{"id":4,"title":5,"author":6,"body":7,"coverText":49,"date":50,"description":51,"excerpt":52,"extension":53,"image":54,"meta":55,"navigation":56,"path":57,"readingTime":58,"seo":59,"stem":60,"tags":61,"__hash__":65},"blog\u002Fblog\u002Fseeing-the-world-as-who-we-are.md","Seeing the World as Who We Are","Sandip Choudhary",{"type":8,"value":9,"toc":44},"minimark",[10,14,19,22,33,36],[11,12,13],"p",{},"I also love photography, not just because I wanted to capture the moments. Yes, of course I wanted to capture the beautiful moments, but I have read somewhere that the camera actually gives a perspective of how we can look at the same thing differently.",[15,16,18],"h2",{"id":17},"learning-about-exposure-triangle","Learning about Exposure Triangle",[11,20,21],{},"I recently learned that there are 3 different things in how a camera captures an image: Aperture, Shutter Speed and ISO. They named it the Exposure Triangle. We can know a lot about this topic on the internet, but the main thing I find interesting is that we can see the same thing differently with a different perspective or lens (not the actual camera lens). We see the world as who we are. We can change the perspective to see the good in bad times and hope when everything is chaos. I think that is our power, to see. And sometimes we don't see by our eyes but by heart.",[11,23,24,25,32],{},"If you want to learn about the Exposure Triangle, you can read more on ",[26,27,31],"a",{"href":28,"rel":29},"https:\u002F\u002Fphotographylife.com\u002Fwhat-is-exposure-triangle",[30],"nofollow","What is the Exposure Triangle?"," on Photography Life.",[11,34,35],{},"The best part of photography is you have a camera as a friend that makes you feel like you have a friend. I personally find myself very close to nature and people when I use a camera, and I don't feel the same way using a phone camera. I am not sure if it is my perspective, but when I have a camera in my hand, it feels adventurous.",[11,37,38,39,43],{},"I have clicked some photographs along the way, and they are over on ",[26,40,42],{"href":41},"\u002Faperture","Aperture"," if you want to have a look.",{"title":45,"searchDepth":46,"depth":46,"links":47},"",2,[48],{"id":17,"depth":46,"text":18},"seeing the world through a lens 📷","2026-08-03","I love photography not just to capture moments, but for the perspective a camera gives. On the exposure triangle, seeing the world as who we are, and why a camera feels like a friend.",null,"md","\u002Fimg\u002Fphotography\u002Fclicked_image_copenhagen.jpeg",{},true,"\u002Fblog\u002Fseeing-the-world-as-who-we-are",3,{"title":5,"description":51},"blog\u002Fseeing-the-world-as-who-we-are",[62,63,64],"photography","personal","perspective","nPkvmICo0IX1xsgH8O0aBVKHL04p9BuOBOToac02XAg",[67,93,157,590,1021],{"id":4,"title":5,"author":6,"body":68,"coverText":49,"date":50,"description":51,"excerpt":52,"extension":53,"image":54,"meta":90,"navigation":56,"path":57,"readingTime":58,"seo":91,"stem":60,"tags":92,"__hash__":65},{"type":8,"value":69,"toc":87},[70,72,74,76,81,83],[11,71,13],{},[15,73,18],{"id":17},[11,75,21],{},[11,77,24,78,32],{},[26,79,31],{"href":28,"rel":80},[30],[11,82,35],{},[11,84,38,85,43],{},[26,86,42],{"href":41},{"title":45,"searchDepth":46,"depth":46,"links":88},[89],{"id":17,"depth":46,"text":18},{},{"title":5,"description":51},[62,63,64],{"id":94,"title":95,"author":6,"body":96,"coverText":145,"date":50,"description":146,"excerpt":52,"extension":53,"image":147,"meta":148,"navigation":56,"path":149,"readingTime":150,"seo":151,"stem":152,"tags":153,"__hash__":156},"blog\u002Fblog\u002Fthinking-in-design.md","Thinking in Design, Not Just Code",{"type":8,"value":97,"toc":140},[98,101,105,108,112,115,118,127,131,134,137],[11,99,100],{},"I have never considered myself a raw coder, but I always considered myself a design person, maybe because I have come from a design mindset of learning design. Taking this philosophy, I have understood that everything can be connected with it. Here are a few things I have learned that shaped how I think.",[15,102,104],{"id":103},"a-payment-bug-that-was-really-a-design-problem","A payment bug that was really a design problem",[11,106,107],{},"I was building a webshop and building all the frontend and backend. After releasing the app for a very initial, crappy release, I needed to answer the following question: how does this app handle concurrent payment requests if there is an issue on the network and the user clicks the pay button multiple times? The solution was Idempotency, which fixes the problem. And though I had to implement it on critical endpoints on the API part, I describe it as a design problem.",[15,109,111],{"id":110},"choosing-a-language-with-intention","Choosing a language with intention",[11,113,114],{},"This is about not just blindly following the trends, but knowing what matters. For the technical side, there is not any single programming language or framework that fits for all. New programming languages are developed to overcome issues in existing languages, but come with another problem to be fixed. Microservices are not a silver bullet. There are different types of languages, dynamic and typed, and debating them based on social media hype, or some influence, or large organizations following them, would be biased, because neither is optimal for all types of application. So I learned that Rust is not good for every project, though we can code in it. Python is not the fastest language, though it is very popular among popular languages. JavaScript is not the best language. Every programming language is designed to solve a problem of their own.",[11,116,117],{},"For example, WhatsApp uses Erlang, which solves one of their greatest problems: concurrency. They chose the language to overcome the biggest problem, though it would have been very difficult to implement a UI in that language (I don't know if they ever used it for UI). But my point is that choosing a language should be intentional, for solving the biggest problem.",[11,119,120,121,126],{},"This has also happened when I was working as a software developer at ",[26,122,125],{"href":123,"rel":124},"https:\u002F\u002Fcrystalsolution.com.np\u002F",[30],"Crystal Solution",". We needed to be able to integrate a lot of modules of software, and each client wanted an app with a different set of modules. PHP was the primary language and we felt it very difficult to handle, because we did not have any method to know which part broke the app. So we moved to .NET Core, which helps to combine and take care of the app as it has type feedback. Most of the bugs are now patched by the IDE.",[15,128,130],{"id":129},"three-aspects-of-software","Three aspects of software",[11,132,133],{},"I also learned in an Advanced Software Engineering course that there are mostly 3 aspects to software: Technical, Security and Process. The technical one is what I already described above.",[11,135,136],{},"For the security aspect, software is not just concerned with developers, but with the people who use it daily, so security is very important. I also learned that there are different kinds of attacks other than cyber attacks in the high technical realm: the Social Engineering Attack. This type of attack involves a person manipulating people to extract identity information. I was very curious to know this dimension when learning about software development.",[11,138,139],{},"For the process aspect, software is developed in a process. It talks about how it is developed, how it will be tested, how it will be deployed, and how the client feedback is handled and injected into the software for software evolution.",{"title":45,"searchDepth":46,"depth":46,"links":141},[142,143,144],{"id":103,"depth":46,"text":104},{"id":110,"depth":46,"text":111},{"id":129,"depth":46,"text":130},"notes from how I think about software 🧩","Why I have never considered myself a raw coder, from treating a payment bug as a design problem, to choosing a language with intention, to the three aspects of software I learned to care about.","\u002Fimg\u002Fblog\u002Fdesign_thinking_blog_cover.jpeg",{},"\u002Fblog\u002Fthinking-in-design",6,{"title":95,"description":146},"blog\u002Fthinking-in-design",[154,155,63],"software engineering","design thinking","8jN9xxtVxBgu4addlttmdCdZ78C8lWqbWq57b4RdjkU",{"id":158,"title":159,"author":6,"body":160,"coverText":576,"date":577,"description":578,"excerpt":52,"extension":53,"image":522,"meta":579,"navigation":56,"path":580,"readingTime":394,"seo":581,"stem":582,"tags":583,"__hash__":589},"blog\u002Fblog\u002Fshipping-battery-notifier-cross-platform.md","From a WinForms Hack to a Cross-Platform Desktop App",{"type":8,"value":161,"toc":567},[162,167,170,173,187,198,202,205,208,215,218,222,225,237,240,247,251,254,263,266,275,279,282,291,299,517,526,530,533,537,545,548,551,563],[163,164,166],"h3",{"id":165},"origin-of-the-project","Origin of the project",[11,168,169],{},"Back in 2022 I had a simple problem. My ageing laptop would die without warning. The battery had degraded so badly that the default Windows alert at 20% was useless. The machine would shut off at 50% or higher, sometimes right in the middle of work.",[11,171,172],{},"So I did what any developer does and built a small WinForms utility app that checks the battery status and fires a toast notification when it crosses a threshold. It ran from the system tray and used that every day.",[11,174,175,176,180,181,186],{},"I ",[26,177,179],{"href":178},"\u002Fprojects\u002Fbattery-notifier","wrote about that original version"," a while back, and at the end I said I wanted to rebuild it with ",[26,182,185],{"href":183,"rel":184},"https:\u002F\u002Favaloniaui.net\u002F",[30],"AvaloniaUI"," so it could run on macOS and Linux too.",[11,188,189,194],{},[190,191],"img",{"alt":192,"src":193},"Battery Notifier evolution: 2022 first release, 2024 redesign, 2026 Avalonia cross-platform","\u002Fimg\u002Fprojects\u002FBatteryNotifierEvolution.webp",[195,196,197],"em",{},"From left to right: the original WinForms release (2022), the redesigned UI (2024), and the current Avalonia cross-platform version (2026).",[163,199,201],{"id":200},"why-i-port-my-utility-app-to-avalonia-after-4-years","Why i port my utility app to Avalonia after 4 years",[11,203,204],{},"I was not about waiting for Avalonia framework to mature but i thought it would be unnecessary to build for cross-platform.",[11,206,207],{},"When I first built Battery Notifier i was just a software developer working mostly on the backend with C# and .NET. I had built POS application using winforms at my work. My understanding of software development was limited to web application mostly. So the experience i gain fro building POS application using winforms, i use that to create a simple basic version of app.",[11,209,210,211,214],{},"After couple of years i joined IT University of Copenhagen for my master's in Computer Science. For the first time i studied concurrency properly. Before that the most unique and important problem we solve for concurrency was on the database level and not on the application level. We used 'UPDATE and GET' query to fix concurrency issue. For our application we needed to have a unique invoice number for each order and we faced concurrency issue when generating these numbers.\nAt ITU i learned more indepth about concurrency and learned differnt terminologies like : deadlock, race condition, synchronization. One of the lecture discussed about why Whatsapp uses Erlang to handle concurrency, which i have mentioned in my previous blog post ",[26,212,95],{"href":213},"\u002Fblog\u002Fthinking-in-design#choosing-a-language-with-intention",".",[11,216,217],{},"That foundation gave me the confidence to actually attempt this.",[163,219,221],{"id":220},"the-platform-reality","The Platform Reality",[11,223,224],{},"Confidence is one thing but each operating system fighting you is another.",[11,226,227,228,232,233,236],{},"Getting battery information on windows was simple until you need to implement it on three platforms. On Windows I call ",[229,230,231],"code",{},"GetSystemPowerStatus"," through P\u002FInvoke, On macOS there is no managed API at all, so I spawn a process ",[229,234,235],{},"pmset -g batt"," and parse text output with regex.",[11,238,239],{},"Detecting charger plug\u002Funplug events was trickier. Windows has WMI power management events. MacOS required registering for Darwin kernel notifications and blocking on a file descriptor in a background thread.",[11,241,242,243,246],{},"On implementing the features, this app should also care about the ",[195,244,245],{},"Do Not Disturb"," state. Windows uses an undocumented notification facility API. MacOS changed their DND implementation across Monterey, Ventura, and Tahoe.",[163,248,250],{"id":249},"not-just-a-port-a-rethink","Not Just a Port — a Rethink",[11,252,253],{},"I did not want to just rewrite the same app with a different UI toolkit. I wanted to raise the standard. If I was going to ship this, it had to feel like a product someone would actually want to install and keep.",[11,255,256,257,262],{},"The biggest influence on my thinking was Apple's design philosophy. Apple has always raised the bar for design and innovation so i wanted ",[26,258,261],{"href":259,"rel":260},"https:\u002F\u002Fbatterynotifier.com",[30],"Battery Notifier"," to feel considered as a intentially designed product and not like a developer's side project.",[11,264,265],{},"So, I opened Figma and started on redesign of the app. I had multiple design iterations and also refined the layout,the colour scheme, the iconography, the settings flow and many other aspects of the design. I have alwaysknown that if i need to make the design better then i had to choose a different framework and AvaloniaUI was the right choice for this. I couldnot have done it on the Winforms because it doesnot give that pixel perfect control over the UI elements.",[11,267,268,272],{},[190,269],{"alt":270,"src":271},"Battery Notifier Figma workspace — dashboard, settings, alerts, and sound picker screens","\u002Fimg\u002Fprojects\u002FBatteryNotifier-FigmaDesign.webp",[195,273,274],{},"The Figma file with all the screens — dashboard, settings, notification preferences, and the sound picker.",[163,276,278],{"id":277},"the-notification-problem","The Notification Problem",[11,280,281],{},"Here is the thing about battery notifications. If an app notifies you time to time then it is annoying. The problem of my app was it just used a loop to check for battery status and notifies it in a interval. It feels robotic and annoying when users gets a lot of notification alerts. I need to somehow fix that.",[11,283,284,285,290],{},"I had been reading a paper from Duolingo on how ",[26,286,289],{"href":287,"rel":288},"https:\u002F\u002Fresearch.duolingo.com\u002Fpapers\u002Fyancey.kdd20.pdf",[30],"Duolingo approaches push notifications",". Their research paper lays out the core insight clearly — every notification you send has a cost, which is the risk of the user opting out entirely. So instead of sending more, they send smarter. If a user ignores a notification, they back off. They treat the notification channel as a scarce resource to be protected, not a firehose to be maximised.",[11,292,293,294,298],{},"I took that same principle and built an escalating backoff system. The intervals increase progressively, and after 7 ignored notifications the app goes silent entirely, then auto-recovers after 2 hours. It burrows Duolingo's \"",[295,296,297],"strong",{},"recovering arm","\" concept:",[300,301,305],"pre",{"className":302,"code":303,"language":304,"meta":45,"style":45},"language-csharp shiki shiki-themes github-light github-dark","private static readonly TimeSpan[] BackoffIntervals =\n[\n    TimeSpan.Zero,\n    TimeSpan.FromMinutes(2),\n    TimeSpan.FromMinutes(5),\n    TimeSpan.FromMinutes(10),\n    TimeSpan.FromMinutes(15),\n    TimeSpan.FromMinutes(30),\n    TimeSpan.FromMinutes(45)\n];\n\nprivate const int MaxNotificationsBeforeSilence = 7;\n\n\u002F\u002F After two hours of silence the tracker resets,\n\u002F\u002F giving the user a fresh reminder cycle.\nprivate static readonly TimeSpan RecoveryInterval = TimeSpan.FromHours(2);\n","csharp",[229,306,307,336,341,346,365,379,392,406,420,435,441,447,470,475,482,488],{"__ignoreMap":45},[308,309,312,316,319,322,326,330,333],"span",{"class":310,"line":311},"line",1,[308,313,315],{"class":314},"szBVR","private",[308,317,318],{"class":314}," static",[308,320,321],{"class":314}," readonly",[308,323,325],{"class":324},"sScJk"," TimeSpan",[308,327,329],{"class":328},"sVt8B","[] ",[308,331,332],{"class":324},"BackoffIntervals",[308,334,335],{"class":314}," =\n",[308,337,338],{"class":310,"line":46},[308,339,340],{"class":328},"[\n",[308,342,343],{"class":310,"line":58},[308,344,345],{"class":328},"    TimeSpan.Zero,\n",[308,347,349,352,355,358,362],{"class":310,"line":348},4,[308,350,351],{"class":328},"    TimeSpan.",[308,353,354],{"class":324},"FromMinutes",[308,356,357],{"class":328},"(",[308,359,361],{"class":360},"sj4cs","2",[308,363,364],{"class":328},"),\n",[308,366,368,370,372,374,377],{"class":310,"line":367},5,[308,369,351],{"class":328},[308,371,354],{"class":324},[308,373,357],{"class":328},[308,375,376],{"class":360},"5",[308,378,364],{"class":328},[308,380,381,383,385,387,390],{"class":310,"line":150},[308,382,351],{"class":328},[308,384,354],{"class":324},[308,386,357],{"class":328},[308,388,389],{"class":360},"10",[308,391,364],{"class":328},[308,393,395,397,399,401,404],{"class":310,"line":394},7,[308,396,351],{"class":328},[308,398,354],{"class":324},[308,400,357],{"class":328},[308,402,403],{"class":360},"15",[308,405,364],{"class":328},[308,407,409,411,413,415,418],{"class":310,"line":408},8,[308,410,351],{"class":328},[308,412,354],{"class":324},[308,414,357],{"class":328},[308,416,417],{"class":360},"30",[308,419,364],{"class":328},[308,421,423,425,427,429,432],{"class":310,"line":422},9,[308,424,351],{"class":328},[308,426,354],{"class":324},[308,428,357],{"class":328},[308,430,431],{"class":360},"45",[308,433,434],{"class":328},")\n",[308,436,438],{"class":310,"line":437},10,[308,439,440],{"class":328},"];\n",[308,442,444],{"class":310,"line":443},11,[308,445,446],{"emptyLinePlaceholder":56},"\n",[308,448,450,452,455,458,461,464,467],{"class":310,"line":449},12,[308,451,315],{"class":314},[308,453,454],{"class":314}," const",[308,456,457],{"class":314}," int",[308,459,460],{"class":324}," MaxNotificationsBeforeSilence",[308,462,463],{"class":314}," =",[308,465,466],{"class":360}," 7",[308,468,469],{"class":328},";\n",[308,471,473],{"class":310,"line":472},13,[308,474,446],{"emptyLinePlaceholder":56},[308,476,478],{"class":310,"line":477},14,[308,479,481],{"class":480},"sJ8bj","\u002F\u002F After two hours of silence the tracker resets,\n",[308,483,485],{"class":310,"line":484},15,[308,486,487],{"class":480},"\u002F\u002F giving the user a fresh reminder cycle.\n",[308,489,491,493,495,497,499,502,504,507,510,512,514],{"class":310,"line":490},16,[308,492,315],{"class":314},[308,494,318],{"class":314},[308,496,321],{"class":314},[308,498,325],{"class":324},[308,500,501],{"class":324}," RecoveryInterval",[308,503,463],{"class":314},[308,505,506],{"class":328}," TimeSpan.",[308,508,509],{"class":324},"FromHours",[308,511,357],{"class":328},[308,513,361],{"class":360},[308,515,516],{"class":328},");\n",[11,518,519,523],{},[190,520],{"alt":521,"src":522},"Battery Notifier running on Windows and macOS in light and dark themes","\u002Fimg\u002Fprojects\u002FBatteryNotifierBannerUpdated.webp",[195,524,525],{},"The current version running on Windows and macOS — light and dark themes.",[163,527,529],{"id":528},"how-ai-helped-me-to-port-to-cross-platform","How AI helped me to port to cross-platform",[11,531,532],{},"AI helped to research and find platform-specific battery APIs and implement them. I then tested them in different operating systems to ensure compatibility. Without AI i would have needed a lot of time to find the right APIs and implement them. Also I have already mentioned that windows had undocumented battery APIs and macOS had no official battery API so AI helped me on researching and finding solutions.",[163,534,536],{"id":535},"from-repo-to-product","From Repo to Product",[11,538,539,540,544],{},"I have plenty of side projects that live as repos and nothing else. This time I wanted to go further. I designed in Figma first. I set up the website: ",[26,541,543],{"href":259,"rel":542},[30],"batterynotifier.com",". I built proper installers for each platform, and wrote release notes.",[11,546,547],{},"My first Figma mockup looked nothing like the final app. I went through more iterations than I imagined. Each one got closer to something that felt right.",[11,549,550],{},"I find that Cross-platform is still hard and there are many places where you need to do platform-specific things. Although Avalonia helps most of the part i had to write   platform-specific code. I also realized why some features of Avalonia does requre licensing because those are hard to get right on all platforms. The cost to maintain the compatibility between platforms was the reason for those pricing.",[11,552,553,554,559,560,214],{},"The source is on ",[26,555,558],{"href":556,"rel":557},"https:\u002F\u002Fgithub.com\u002Fsandip124\u002Fbatterynotifier",[30],"GitHub"," and the app is at ",[26,561,543],{"href":259,"rel":562},[30],[564,565,566],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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":45,"searchDepth":46,"depth":46,"links":568},[569,570,571,572,573,574,575],{"id":165,"depth":58,"text":166},{"id":200,"depth":58,"text":201},{"id":220,"depth":58,"text":221},{"id":249,"depth":58,"text":250},{"id":277,"depth":58,"text":278},{"id":528,"depth":58,"text":529},{"id":535,"depth":58,"text":536},"Battery Notifier v2 — now on macOS, Windows, and Linux","2026-03-21","In 2022 I built a small battery notifier for myself. In 2026 I finally shipped it as a proper cross-platform product — with Avalonia UI, Duolingo-inspired notifications, and a real design system.",{},"\u002Fblog\u002Fshipping-battery-notifier-cross-platform",{"title":159,"description":578},"blog\u002Fshipping-battery-notifier-cross-platform",[584,585,586,587,588],"dotnet","avalonia","desktop","open source","product design","pFFvzp4hbsn7Z4vzAQ-PFzeyf0p04tkHywRRPmQU2M0",{"id":591,"title":592,"author":6,"body":593,"coverText":1007,"date":1008,"description":1009,"excerpt":52,"extension":53,"image":1010,"meta":1011,"navigation":56,"path":1012,"readingTime":394,"seo":1013,"stem":1014,"tags":1015,"__hash__":1020},"blog\u002Fblog\u002Fbuilding-subscription-ecommerce.md","Building a Subscription eCommerce from Scratch",{"type":8,"value":594,"toc":999},[595,599,608,615,619,622,625,628,632,635,663,667,670,673,679,689,972,982,986,989,993,996],[163,596,598],{"id":597},"the-brief","The Brief",[11,600,601,602,607],{},"I had a chance to work with a Copenhagen-based company that sells customized dog food on ",[26,603,606],{"href":604,"rel":605},"https:\u002F\u002Fboneappetitedk.com\u002F",[30],"Boneappetitedk.com",". The core feature is a calorie calculator that recomends meal plans based on dog's breed, weight, body condition and activity level. Customers then subscribe to receive that plan on a recurring or one-time basis.",[11,609,610,611],{},"I had also written more descriptive information of the project ",[26,612,614],{"href":613},"\u002Fprojects\u002Fboneappetite","Boneappetitedk",[163,616,618],{"id":617},"why-platforms-failed-us","Why Platforms Failed Us",[11,620,621],{},"My first instinct was Shopify. I had used it before, it handles payments well, and the ecosystem is enormous. But the subscription model required plugins, and the plugins that came close to what we needed were either too expensive, too generic, or both. Customising the calorie calculation logic on top of a plugin layer was very difficult to implement.",[11,623,624],{},"I looked at MedusaJS as a self-hosted alternative. At the time the available version was 1.x, and the documentation for subscriptions was sparse. The project timeline did not allow for that much exploration.",[11,626,627],{},"In the end, the cleanest path was a custom build but i knew that i have to do all the fullstack work and deployment by myself. Also i knew that it is hard to get right on the security part becasue the system that involves payment processing need to have hight security standards.",[163,629,631],{"id":630},"the-stack-decision","The Stack Decision",[11,633,634],{},"I chose what I knew well:",[636,637,638,645,651,657],"ul",{},[639,640,641,644],"li",{},[295,642,643],{},".NET Core"," for the backend and API : I had years of production experience with it and trusted it for the codebase i produce.",[639,646,647,650],{},[295,648,649],{},"Vue.js"," for the frontend app : Client required a customizable app with their own design tweaks so i choose Vue.js for its simplicity.",[639,652,653,656],{},[295,654,655],{},"PostgreSQL"," for the database : open source, reliable, excellent JSON support for the dynamic meal plan data",[639,658,659,662],{},[295,660,661],{},"Stripe"," for payments : I had integrated it before and its subscription primitives are genuinely good",[163,664,666],{"id":665},"stripe-subscriptions-are-surprisingly-deep","Stripe Subscriptions Are Surprisingly Deep",[11,668,669],{},"I expected Stripe to be the easy part. It mostly was, but handling subscription was a bit tricky compared to one-time payments.",[11,671,672],{},"A few things that took longer than expected:",[11,674,675,678],{},[195,676,677],{},"Proration"," : When a customer changes their meal plan mid-cycle, Stripe can automatically calculate what they owe or are owed for the remainder of the billing period. It required careful orchestration between the app and Stripe's API to show the charge clearly.",[11,680,681,684,685,688],{},[195,682,683],{},"Webhooks are the source of truth"," : Your payment intent might succeed on the frontend, but the subscription is not active until Stripe fires ",[229,686,687],{},"customer.subscription.created",". I learned early to never update subscription state based on the API response alone but to consider the webhook as the source of truth.",[300,690,692],{"className":302,"code":691,"language":304,"meta":45,"style":45},"[HttpPost(\"webhook\")]\npublic async Task\u003CIActionResult> HandleStripeWebhook()\n{\n    var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();\n    var stripeEvent = EventUtility.ConstructEvent(\n        json,\n        Request.Headers[\"Stripe-Signature\"],\n        _webhookSecret\n    );\n\n    switch (stripeEvent.Type)\n    {\n        case Events.CustomerSubscriptionCreated:\n            var subscription = (Subscription)stripeEvent.Data.Object;\n            await _subscriptionService.ActivateAsync(subscription.Id);\n            break;\n\n        case Events.InvoicePaymentFailed:\n            var invoice = (Invoice)stripeEvent.Data.Object;\n            await _notificationService.SendPaymentFailedEmail(invoice.CustomerEmail);\n            break;\n    }\n\n    return Ok();\n}\n",[229,693,694,711,737,742,770,788,793,804,809,814,818,826,831,847,866,880,887,892,906,923,937,944,950,955,966],{"__ignoreMap":45},[308,695,696,699,702,704,708],{"class":310,"line":311},[308,697,698],{"class":328},"[",[308,700,701],{"class":324},"HttpPost",[308,703,357],{"class":328},[308,705,707],{"class":706},"sZZnC","\"webhook\"",[308,709,710],{"class":328},")]\n",[308,712,713,716,719,722,725,728,731,734],{"class":310,"line":46},[308,714,715],{"class":314},"public",[308,717,718],{"class":314}," async",[308,720,721],{"class":324}," Task",[308,723,724],{"class":328},"\u003C",[308,726,727],{"class":324},"IActionResult",[308,729,730],{"class":328},"> ",[308,732,733],{"class":324},"HandleStripeWebhook",[308,735,736],{"class":328},"()\n",[308,738,739],{"class":310,"line":58},[308,740,741],{"class":328},"{\n",[308,743,744,747,750,752,755,758,761,764,767],{"class":310,"line":348},[308,745,746],{"class":314},"    var",[308,748,749],{"class":324}," json",[308,751,463],{"class":314},[308,753,754],{"class":314}," await",[308,756,757],{"class":314}," new",[308,759,760],{"class":324}," StreamReader",[308,762,763],{"class":328},"(HttpContext.Request.Body).",[308,765,766],{"class":324},"ReadToEndAsync",[308,768,769],{"class":328},"();\n",[308,771,772,774,777,779,782,785],{"class":310,"line":367},[308,773,746],{"class":314},[308,775,776],{"class":324}," stripeEvent",[308,778,463],{"class":314},[308,780,781],{"class":328}," EventUtility.",[308,783,784],{"class":324},"ConstructEvent",[308,786,787],{"class":328},"(\n",[308,789,790],{"class":310,"line":150},[308,791,792],{"class":328},"        json,\n",[308,794,795,798,801],{"class":310,"line":394},[308,796,797],{"class":328},"        Request.Headers[",[308,799,800],{"class":706},"\"Stripe-Signature\"",[308,802,803],{"class":328},"],\n",[308,805,806],{"class":310,"line":408},[308,807,808],{"class":328},"        _webhookSecret\n",[308,810,811],{"class":310,"line":422},[308,812,813],{"class":328},"    );\n",[308,815,816],{"class":310,"line":437},[308,817,446],{"emptyLinePlaceholder":56},[308,819,820,823],{"class":310,"line":443},[308,821,822],{"class":314},"    switch",[308,824,825],{"class":328}," (stripeEvent.Type)\n",[308,827,828],{"class":310,"line":449},[308,829,830],{"class":328},"    {\n",[308,832,833,836,839,841,844],{"class":310,"line":472},[308,834,835],{"class":314},"        case",[308,837,838],{"class":324}," Events",[308,840,214],{"class":328},[308,842,843],{"class":324},"CustomerSubscriptionCreated",[308,845,846],{"class":328},":\n",[308,848,849,852,855,857,860,863],{"class":310,"line":477},[308,850,851],{"class":314},"            var",[308,853,854],{"class":324}," subscription",[308,856,463],{"class":314},[308,858,859],{"class":328}," (",[308,861,862],{"class":324},"Subscription",[308,864,865],{"class":328},")stripeEvent.Data.Object;\n",[308,867,868,871,874,877],{"class":310,"line":484},[308,869,870],{"class":314},"            await",[308,872,873],{"class":328}," _subscriptionService.",[308,875,876],{"class":324},"ActivateAsync",[308,878,879],{"class":328},"(subscription.Id);\n",[308,881,882,885],{"class":310,"line":490},[308,883,884],{"class":314},"            break",[308,886,469],{"class":328},[308,888,890],{"class":310,"line":889},17,[308,891,446],{"emptyLinePlaceholder":56},[308,893,895,897,899,901,904],{"class":310,"line":894},18,[308,896,835],{"class":314},[308,898,838],{"class":324},[308,900,214],{"class":328},[308,902,903],{"class":324},"InvoicePaymentFailed",[308,905,846],{"class":328},[308,907,909,911,914,916,918,921],{"class":310,"line":908},19,[308,910,851],{"class":314},[308,912,913],{"class":324}," invoice",[308,915,463],{"class":314},[308,917,859],{"class":328},[308,919,920],{"class":324},"Invoice",[308,922,865],{"class":328},[308,924,926,928,931,934],{"class":310,"line":925},20,[308,927,870],{"class":314},[308,929,930],{"class":328}," _notificationService.",[308,932,933],{"class":324},"SendPaymentFailedEmail",[308,935,936],{"class":328},"(invoice.CustomerEmail);\n",[308,938,940,942],{"class":310,"line":939},21,[308,941,884],{"class":314},[308,943,469],{"class":328},[308,945,947],{"class":310,"line":946},22,[308,948,949],{"class":328},"    }\n",[308,951,953],{"class":310,"line":952},23,[308,954,446],{"emptyLinePlaceholder":56},[308,956,958,961,964],{"class":310,"line":957},24,[308,959,960],{"class":314},"    return",[308,962,963],{"class":324}," Ok",[308,965,769],{"class":328},[308,967,969],{"class":310,"line":968},25,[308,970,971],{"class":328},"}\n",[11,973,974,977,978,981],{},[195,975,976],{},"Failed payments need a recovery flow"," : Stripe retries failed payments automatically, but you need to handle the ",[229,979,980],{},"invoice.payment_failed"," event and communicate that to the customer gracefully.",[163,983,985],{"id":984},"the-part-i-enjoyed-most","The Part I Enjoyed Most",[11,987,988],{},"Working on the calorie calculation logic itself was most rewarding where i closely worked with the vet experts and nutritionists on their nutrition formula. I then mapped the formula on the app to calculate daily calorie needs to their custom made dog food. This project expands my knowledge on custom ecommerce development and experience working with an international team.",[163,990,992],{"id":991},"wrapping-up","Wrapping Up",[11,994,995],{},"Building from scratch is not always the right answer. Platforms exist for good reasons and most eCommerce projects are better served by them. But when the core feature is genuinely bespoke and the platform fight would cost more than a custom build then rolling your own is a legitimate choice - provided you have the discipline to do it properly.",[564,997,998],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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":45,"searchDepth":46,"depth":46,"links":1000},[1001,1002,1003,1004,1005,1006],{"id":597,"depth":58,"text":598},{"id":617,"depth":58,"text":618},{"id":630,"depth":58,"text":631},{"id":665,"depth":58,"text":666},{"id":984,"depth":58,"text":985},{"id":991,"depth":58,"text":992},"snapshot of Boneappetitedk","2026-03-10","No Shopify. No WooCommerce. Just .NET Core, Vue.js, PostgreSQL, and a lot of decisions I had to make myself. Here is what I learned building Boneappetitedk.","\u002Fimg\u002Fprojects\u002FBoneappetiteBannerWide.png",{},"\u002Fblog\u002Fbuilding-subscription-ecommerce",{"title":592,"description":1009},"blog\u002Fbuilding-subscription-ecommerce",[584,1016,1017,1018,1019],"vue","ecommerce","stripe","lessons learned","sZZHAXa4rBjBbjugnMhV-QSLesoixSL9AS7h8JTDrd0",{"id":1022,"title":1023,"author":6,"body":1024,"coverText":1100,"date":1101,"description":1102,"excerpt":52,"extension":53,"image":1103,"meta":1104,"navigation":56,"path":1105,"readingTime":348,"seo":1106,"stem":1107,"tags":1108,"__hash__":1111},"blog\u002Fblog\u002Fwhy-i-write.md","Why I Finally Started Writing Online",{"type":8,"value":1025,"toc":1095},[1026,1030,1033,1036,1039,1042,1046,1053,1056,1062,1065,1069,1072,1075,1078,1089,1092],[163,1027,1029],{"id":1028},"the-notebooks","The Notebooks",[11,1031,1032],{},"I have been keeping physical notebooks since 2018. Small pocket-sized ones that I carry everywhere. When I run into a tricky bug, I sketch it out on paper. When I learn something new — a pattern, a concept, a gotcha — I write it down by hand.",[11,1034,1035],{},"I also have written digital notes on my computer note app, Notion and Google Keep but there is something about the act of writing by hand that forces you to slow down and actually understand what you are putting on the page. You cannot paste a Stack Overflow answer into a notebook. You have to distil it into your own words.",[11,1037,1038],{},"Over the years I have filled more notebooks than I can count. Ideas for projects, architecture diagrams, notes from conference talks, half-finished essays about things I was thinking about.",[11,1040,1041],{},"I have always wanted to write a physical note because that would be a single piece of reference of what i write and i felt it very important becasue i believed that if i had notes on digital form then it would not probably mine because i could have pasted some texts from internet or generate by AI.",[163,1043,1045],{"id":1044},"the-moment-i-changed-my-mind","The Moment I Changed My Mind",[11,1047,1048,1049,1052],{},"I was working on the ",[26,1050,614],{"href":604,"rel":1051},[30]," project, trying to figure out why PrimeVue's design tokens were not being applied in the right order during SSR hydration. I spent the better part of a day on it, eventually solved it, and wrote three pages of notes about what I found.",[11,1054,1055],{},"Six months later a friend asked me about a similar issue with a different UI library. I remembered solving something like it. I dug out the notebook, found the entry, and read it back to him over a voice call — squinting at my own handwriting.",[11,1057,1058,1059,214],{},"That was the moment I thought: ",[195,1060,1061],{},"this should be online",[11,1063,1064],{},"Not because the internet needed another developer blog. But because future-me and the people I work with would benefit from it.",[163,1066,1068],{"id":1067},"how-i-plan-to-write-here","How I Plan to Write Here",[11,1070,1071],{},"Short. Honest. Close to the original notebook entry in spirit.",[11,1073,1074],{},"I am not trying to build an audience or a brand. I am trying to create a searchable, shareable version of the notebooks I have already been keeping.",[11,1076,1077],{},"Posts will mostly be:",[636,1079,1080,1083,1086],{},[639,1081,1082],{},"Things I learned while building something",[639,1084,1085],{},"Opinions I formed over time that I want to articulate properly",[639,1087,1088],{},"Notes on tools, patterns, or decisions I keep revisiting",[11,1090,1091],{},"I will not be posting on a schedule. I will post when something is worth writing down.",[11,1093,1094],{},"If something I write here saves you an afternoon of debugging, or makes you think about a problem differently then that is more than enough reason for it to exist.",{"title":45,"searchDepth":46,"depth":46,"links":1096},[1097,1098,1099],{"id":1028,"depth":58,"text":1029},{"id":1044,"depth":58,"text":1045},{"id":1067,"depth":58,"text":1068},"Crafting my thoughts into words ✍️","2025-01-15","I have kept physical notebooks for years. Here is what pushed me to start publishing my thoughts online, and why I think every developer should too.","\u002Fimg\u002Fblog\u002Fwhy_i_write_cover.jpeg",{},"\u002Fblog\u002Fwhy-i-write",{"title":1023,"description":1102},"blog\u002Fwhy-i-write",[1109,63,1110],"writing","developer life","icYFCKLgCEewEMSKfNoPMxjiDE_SGRIVwvEYYjGtGKU",1785813117453]