Origin story (from a spur-of-the-moment throwaway comment to a project) Link to heading

A few years ago I had the difficult duty of helping clear out the family home. One of the tasks was emptying a loft filled with generations of accumulated possessions, including my childhood belongings which I’d stashed away in the mid-late 1980s. One of the many boxes contained my old comic collection. I kept them because I had seen a news story about some old comics selling for high prices, and I still remember thinking, when I was boxing them up all those years ago, that I might be leaving a valuable treasure for my future self.

Fast forward to the present time. I checked online auction sites, and there were plenty of people trying to sell bundles of comics from that era, but they weren’t listed for the high prices I dreamed of when I was young. Nevertheless, I thought I’d join them, and listed all the bundles for sale, at a price that was pretty much the original purchase price adjusted for inflation1.

They sat there, languishing for month after month, with zero interest. One day I was telling this story to someone, and joked that “you’d have thought someone would be buying them to sell individually as quirky born-on-this-day birthday presents or something”. Although a spur-of-the-moment throwaway comment, I found myself thinking about this idea again over the following days.

I soon decided to see if any sites like that already existed. I couldn’t find any. It seemed that all the current ecommerce platforms allow you to search by product or category, but there didn’t seem to be any which has search and navigation centred on dates. Now that could be because there isn’t any interest in such a thing, but it could also mean an opportunity. I thought it would be fun to see if it was the latter. Worst case I’d learn all about building ecommerce sites. Little did I realise what a big undertaking it would be!

Requirements (turning an hours-long project into a months-long project) Link to heading

The first requirement was to be able to attach a date to each product, so that the search could return matching results. Unfortunately, it turned out to be not quite so simple. Some products would have a full date (i.e. day, month and year), e.g. a weekly comic, but other products would only have a year, e.g. a comic annual, and some might even have just a month and year, so the platform needed to be flexible enough to have at minimum a mandatory year, and also an optional month or full date. This was starting to sound like a days-long project rather than an hours-long project.

Self-hostable headless ecommerce platform Link to heading

It also soon became apparent that, because I needed a custom search and navigation, I wouldn’t be able to use any of the simple template-based Software as a Service (SaaS) platforms for building ecommerce sites, e.g. Shopify. I would need to be able to modify or write my own storefront, in which case I would be better off using a “headless ecommerce platform”, i.e. one where the storefront (front-end) and ecommerce platform (back-end) are decoupled. Some of the headless SaaS platforms ran into the £1000s a year for fees, and it didn’t make economic sense to spend that much money to sell £10s worth of comics, so I realised I would need a platform which would be relatively easy and inexpensive to self-host. This would now be a weeks-long project rather than a days-long project.

Multi-vendor marketplace Link to heading

Already it was starting to sound a fair bit of work, and I didn’t want to just do all the work, sell my things, and then shut everything down. So I wanted to try to build something that might be able to continue after I’d sold my comics. The obvious solution was to build a platform which other sellers could use to sell their things too - a “multi-vendor marketplace”. Needless to say that is a much bigger undertaking. I had successfully turned my weeks-long project into a months-long project!

Technology selection Link to heading

Ecommerce platform - Vendure Link to heading

I narrowed the search down to 3 open source and self-hostable headless ecommerce platforms for building a multi-vendor marketplace:

I performed small proof of concepts with each, trying basic installs with the year/date extensions, but not the more complex multi-vendor setups. Based on that, all seemed reasonable choices, but I selected Vendure, primarily due to the good levels of engagement in its developer community, its funding model, and well-thought-out architecture.

Front-end stack - React Router 7 framework mode Link to heading

I didn’t want to use a Single Page Application (SPA) for lots of reasons, the main one being that search engines typically don’t go to the expense of having their crawlers execute JavaScript, and it was important that my content be visible to search engines. I would have liked to use a full Static Site Generator (SSG), but that wasn’t ideal given some of the more dynamic functionality such as the shopping cart. So a good compromise seemed to be some form of Server-Side Rendering (SSR).

I looked at a few options, but settled on React Router 7 in framework mode, because I liked its philosophy - simplicity, web standards (primitives like fetch and forms) with progressive enhancement (works without JavaScript by default). Some of the other alternatives either didn’t have a clear philosophy, or had a philosophy that I wasn’t so sure about, e.g. in the context of JavaScript/Typescript I find that “best-in-class type safety” usually means more type wrangling and gymnastics rather than less.

There was also a sample Remix (earlier version of React Router 7) storefront provided with Vendure (more on that later).

Development (taking longer than expected) Link to heading

Locally hosted LLMs are actually quite good now Link to heading

You can’t really talk about software development nowadays without talking about LLM coding agents, so I’ll address that first.

I was mostly looking at self-hosted LLMs, because I generally prefer self-hosting for lots of reasons, e.g. tighter control of costs (fixed upfront vs open-ended ongoing). Also, in what seems like a rare case of fortune working in my favour, I built myself a new PC with 96GB RAM and a 2nd hand RTX3090 with 24GB VRAM in March 2024, before the AI bubble caused memory prices to go crazy2. And I’m generally quite cost conscious, so wasn’t keen on burning £1000s worth of tokens with the big AI companies just to boil the ocean to make some billionaires richerbuild a system to sell £10s worth of comics.

When I started the project in 2025, locally hosted LLM coding agents were a bit hit-and-miss, so pretty much all of the early code was hand-crafted. However, during the project things changed a lot. To use some slightly subjective and imprecise terminology, they became:

  • usable in Feb 2026 with Qwen3-Coder-Next.
  • quite good in May 2026 with Qwen3.6-27B-MTP.
  • actually quite good in Aug 2026 with Qwen3.8-27B.

My main issues became cooling during the unusually hot summer3.

A few thoughts on LLM generated code though:

  • Delegating specific tasks and carefully vetting any code they produce before accepting it is very different from “vibe coding”. I need code I can support and maintain, because I’m responsible for running it in a production environment. I can’t just “vibe code” it and hope it works.
  • I see it as like managing a junior developer - you don’t just give them vague instructions and hope they deliver, you need very specific instructions along with plenty of ongoing help and guidance. Plus, let’s be honest, there are some things you can learn from a junior developer. This makes LLMs a good pairing for me - I have in effect gone from being a solo dev back to a dev team lead again.
  • I have to admit that it is also useful for the stuff I don’t find so interesting, like writing test scripts, and stuff that I need help with, like debugging strange issues. In some ways it has made coding a bit more fun again, after it became a bit of a headache when everything got unnecessarily complicated.
  • To paraphrase Rick Deckard, LLMs “are like any other machine. They’re either a benefit or a hazard.” And like any other machine, it is the human operators who make the difference - they can be used well by someone who knows what they’re doing, or badly by someone who doesn’t. (Just to be clear, I hope I’m in the former group:-)

Simplifying the storefront (rewriting from Remix to React Router 7) Link to heading

When I started the project, there were a number of sample storefronts for Vendure, the main ones being the Next.js storefront starter and the Remix storefront starter (note that the main ones now are Next.js and TanStack Start).

I chose the Remix one for the reasons outlined above, thinking Remix and React Router 7 would be quite similar. Unfortunately I soon found out that a lot of things were simplified in the move from Remix to React Router 7. To take just one example, in Remix the cart state management was quite complex:

  • root.tsx had a shouldRevalidate function to manually trigger revalidation after login/logout/checkout.
  • root.tsx also had a useEffect to re-fetch the cart whenever loader data changes.
  • use-active-order.tsx had a fairly lengthy custom hook wrapping useFetcher with manual load/submit logic.

However, in React Router 7, revalidation is handled for you4, so all I needed was to put the activeOrder and activeCustomer data in the root, pass it down via <Outlet context={{ activeOrder, activeCustomer }} />;, and handle subsequent mutations with their own isolated useFetcher() calls, i.e. no shouldRevalidate, no useEffect, no custom hook, no fetcher management, and no other complex interdependencies. Although I did later find that it wasn’t possible to access data from a parent loader in a child loader (the workaround being to request the data in both places), so it wasn’t always so simple.

This is just one example though - there were many more. Unfortunately, this meant I pretty much ended up writing the storefront from scratch.

I also ended up changing the design in the process, so it now looks quite different from the Remix storefront starter.

Making it multi-vendor Link to heading

Vendure provide an example multi-vendor plugin. The core idea is that each seller has their own channel, but that the storefront shows products from all channels. There are also separate accounts for Customers and Sellers, perhaps because the storefront (used by Customers) is completely separate from the admin dashboard (used by Sellers).

The example plugin is a great start, but needs a lot of changes to make it production-ready.

One of the first challenges is creating sellers in such a way that their email is properly verified. Now I couldn’t find any way of integrating seller accounts with the customer email verification process, so what I ended up doing was a multi-step process where a user first has to create a Customer account (and verify their email), then creates a Seller account which uses the same (verified) email and also sets up the seller’s channel and permissions and so on. This has the advantage of allowing the process to start on the storefront, in my case on a new tab called Seller Account in the My Account screen.

Another challenge is the payment system. In my case I’m using Stripe Connect. When a seller’s account is created in Vendure, a connected account is created in Stripe. The seller then has to complete their onboarding process in Stripe, adding their bank details etc. Unfortunately the provided Stripe Plugin doesn’t capture some of the Stripe Connect webhook events such as transfer.created, so I ended up writing a custom Stripe Connect payment processor. Still, at least I know a lot more about payment processes now. Also, Stripe Connect is potentially quite expensive, with a monthly fee for every active seller account, which is why I’ve had to set the vintagepresents.com platform fee at 10%.

GraphQL - sounds great in theory, not in practice Link to heading

Vendure uses GraphQL as its API layer. GraphQL sounds great - “ask for what you need, get exactly that” according to https://graphql.org/.

I needed a custom date-based navigation, hierarchically:

  • year range - all years with products within range
  • single year - months with products + year-only products in year
  • single month - days with products + month-only products in year/month + year-only products in year
  • single day - full-date products + month-only products in year/month + year-only products in year

Unfortunately I wasn’t able to simply ask for what I needed because that information wasn’t available.

At first I simply requested all products and filtered these in the GraphQL client (which was run on the server given the SSR). This worked okay when I just had 83 products, but would fail when the system had more than the 100 item limit. So, before full rollout, I had to write my own queries for the navigation. Unfortunately there wasn’t a way of getting what I needed from the TypeScript API, so I had to:

  • write a “select distinct” SQL query to get e.g. a unique list of years with in-stock products
  • convert the “select distinct” SQL query to a TypeORM query
  • wrap up the TypeORM query as a service
  • setup a resolver for the service
  • make a schema extension containing the new GQL query
  • then write my GQL query

And in these transitions the automated type checking (not to mention the benefits of data abstraction) was lost. For the type checking, the recommended solution was to use codegen, but for me having to resort to using codegen feels like admitting you have failed to keep it simple.

Deployment Link to heading

Finding a domain Link to heading

I spent ages trying to find a domain.

I like old fashioned words which have fallen out of use, and thought one of those might be suitable for a site selling date based gifts, so wrote a script that searched the Oxford English Dictionary for “archaic” + “poetic and literary” + “obsolete” words and did a DNS lookup to see which didn’t have domains registered. I found loads of great available domains that might (at a push) have worked for other projects5, but unfortunately none which were suitable for this project.

I was even considering purchasing a domain from a “domain reseller”, until I realised I’d misunderstood the price and that it was actually 10 times more expensive than I thought.

In the end, I went with vintagepresents.com which was available.

Platform as a Service (PaaS) - Dokploy Link to heading

I had used the OpenShift Container Platform in a commercial setting, and found it helpful, so wanted to use an open-source self-hostable PaaS for this project. That came down to:

I would love to say that I did a full evaluation of each and picked the one which was a closest match for my needs, but the truth is that, by the time I had got to this stage of the project, I was just anxious to get something on production servers as soon as possible. So I went with Dokploy, simply because I’d seen examples of Vendure being deployed via Dokploy.

Difficulties finding a server, thanks to the AI bubble Link to heading

I use Hetzner for my other projects.

Unfortunately, thanks to the AI bubble, they have long periods with no availability whatsoever for their cost optimised cloud servers, despite a 40% price increase from mid-June 2026.

Fortunately, by shuffling some of my other projects and servers around, I was able to free up an existing ARM-based CAX11 with 40GB of storage, which is enough to run the full vintagepresents.com stack for the time being.

The server cost, post-AI-price-increase, is now €7.79 (about £6.67) a month.

The finished solution Link to heading

So it took a lot longer than I expected, but I got there in the end, and https://vintagepresents.com/ is now live!

Here’s the home page, including various date-based navigation options:

vintagepresents.com home page from 31 Aug 2026

And the search box, allowing a mandatory year and optional month and day, showing the year 1982 selected:

vintagepresents.com search box with 1982 selected

Searching for “1982” takes you to the year page for 1982:

vintagepresents.com year page for 1982

A couple of points about the year page:

  • The year page you get to by searching for a year is the same as if you had browsed by date, i.e. there is not a separate search result page. This makes the interface more familiar for users (and cuts down on the amount of code to maintain).
  • The introductory text is to try to make search engines see each year page as unique, so they will hopefully index multiple year pages rather than discarding other years as duplicates. The year facts are manually prepared, but the sentence about stock is auto-generated based on what stock is available, so should change over time.

Next steps - marketing to get buyers and sellers Link to heading

This post has been about building and deploying vintagepresents.com, i.e. the tech side. What I have to do next is to make the first sale and get a few more successful sales to get the confidence it is all working correctly (and/or fix any issues), then look to add one or more additional sellers to cover a much wider range of dates (i.e. make it more useful), then look to drive more potential buyers to the site.

This will require venturing into the marketing side, which is something of an undiscovered country for me (hopefully one from which I will return:-) Something that might help here is that, from a Search Engine Optimisation (SEO) perspective, vintagepresents.com is a little different. For many eCommerce sites, from an SEO perspective, they want search engines to drive traffic to the product pages. However, with vintagepresents.com, from an SEO perspective, I want to drive people to the date search or the actual date pages and from there they will select the products, i.e. in effect the product is the date. This may be an advantage because the date pages are evergreen content, i.e. should continue to be relevant over time.

Conclusion Link to heading

Anyway, I don’t want to spend too much more time writing about what I have done, or any time writing about what I could do with vintagepresents.com in future (although I do have lots of ideas), when I could be spending that time actually doing something new, so I’m going to wrap up here.

But thanks for reading, and if you have any questions, comments or suggestions, please reach out (particularly if you are interested in becoming a seller and/or have any ideas about how best to attract buyers).


  1. I had listed a bundle which worked out at £0.42 each, for cover prices which ranged from £0.09 in 1982 to £0.16 in 1986, which (using the Bank of England inflation calculator) works out at £0.32 to £0.48 respectively in Oct 2025 prices. ↩︎

  2. The 96GB was 2x48GB for a quite reasonable £275.48 inc VAT in March 2024. It also left 2 slots free for possible future expansion. I looked at how much it would cost to get another matching 96GB (2x48GB) pair to take my RAM to 192GB, and they’d gone from £275.48 to a crazy £1,071.98 by March 2026, and an even more crazy £1,226.99 by July 2026! Thanks AI! ↩︎

  3. The PC I originally built in March 2024 was passively cooled for near silence (I could still hear the chips twittering and whistling due to coil whine though), and summer 2026 in the UK was unusually hot. I could go into a lot more detail about some of the issues I had, but that would be a bit of a digression. ↩︎

  4. It’s a bit dense, i.e. a lot of information in a small amount of space so you really have to pay attention, but as per https://reactrouter.com/start/framework/route-module#action “Route actions allow server-side data mutations with automatic revalidation of all loader data on the page when called from <Form>, useFetcher, and useSubmit.” ↩︎

  5. I was going to provide some examples here, but curiously the ones I’ve just checked again have now been registered, parked and listed for sale at greatly inflated prices. ↩︎