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.
Boneappetitedk โ built from the ground up ๐พThe client runs Boneappetitedk, a Copenhagen-based company that sells customised dog food. The core feature is a calorie calculator โ owners enter their dog's breed, weight, and activity level, and the shop recommends a meal plan. Customers then subscribe to receive that plan on a recurring basis.
Simple concept. Surprisingly hard to execute with existing platforms.
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 would have meant fighting the platform constantly.
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.
In the end, the cleanest path was a custom build.
I chose what I knew well:
The decision to fully decouple frontend and backend was deliberate. The client's requirements would change โ they always do โ and keeping the UI layer independent meant we could redesign or replace it without touching the business logic.
I expected Stripe to be the easy part. It mostly was, but subscriptions have more surface area than one-off payments.
A few things that took longer than expected:
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. Getting this to feel natural in the UI โ showing the customer a clear preview of the charge before they confirm โ required careful orchestration between the frontend, the API, and Stripe's preview invoice endpoint.
Webhooks are the source of truth. Your payment intent might succeed on the frontend, but the subscription is not active until Stripe fires customer.subscription.created and your webhook handler processes it. I learned early to never update subscription state based on the API response alone. The webhook is what drives database state โ everything else is optimistic UI.
Failed payments need a recovery flow. Stripe retries failed payments automatically, but you need to handle the invoice.payment_failed event and communicate that to the customer gracefully. Building that email flow and the "update your payment method" screen was unglamorous work but important.
Partway through the project the client started requesting frequent colour scheme changes. Not huge redesigns โ just "can we try the buttons in a darker orange?" or "can the hero section feel warmer?". Each change meant hunting through components and updating values manually.
I had been using PrimeVue for the component layer. PrimeVue 4.0 was in beta at the time and introduced design tokens โ a theming system where you define your palette once and every component that uses those tokens updates automatically.
Migrating to it took less than half a day. From that point on, responding to a colour change request meant updating a handful of token values in one file. It sounds like a small thing but it fundamentally changed how the client relationship felt. Instead of change requests being a source of dread, they became trivial.
If you are building a long-running client project with a design system, design tokens are not optional. They are the minimum viable foundation.
Start with a proper domain model. Early on I let the database schema drift close to the UI shape โ tables that mapped to forms rather than to business concepts. Refactoring the subscription and meal plan models halfway through the project cost time that a clearer upfront design would have saved.
Write integration tests for the Stripe webhook handlers from day one. I added them later, but the period before I had them was genuinely stressful. Stripe's test event tooling makes this easy. There is no excuse not to have them.
Be more explicit with the client about scope. The calorie calculator logic evolved significantly during the project as the client better understood what their customers actually needed. That is normal and fine โ but some of those changes touched the database schema and required migration work. Better upfront documentation of what was in and out of scope would have made those conversations easier.
Honestly? The calorie calculation logic itself. Working through the nutritional science, talking with the client about how different breeds and activity levels translate into daily calorie needs, and then encoding that into a clean service layer โ that was the most intellectually satisfying part of the project.
Software is often just plumbing. Occasionally you get to build something where the domain itself is interesting, and the code becomes a direct expression of real knowledge. This was one of those times.
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, rolling your own is a legitimate choice โ provided you have the discipline to do it properly.
The Boneappetitedk project is still running and still evolving. That it has held up to changing requirements without major rewrites is the best validation I can ask for.