Journal

When the customer changes, the platform must follow

Kooko began by delivering dinner to individuals, then served groups of colleagues and finally their employers. Five years and four architectures taught us when to adapt a system—and when to stop extending it.

All postsArchitectureMigrationCommerce
Published
28 April 2026
Read
6 min read
Written by
Eric Chautems
Contents

On this page

Kooko delivered meals from local restaurants around Lausanne. The food was prepared on site, carried by salaried bicycle couriers and packed in compostable containers. What began as a dinner service for individuals gradually became a lunch service for teams and, eventually, a platform sold directly to their employers.

Kooko was also our own company. We co-founded it and built every version of the product, so this is not a conventional client case study. It is the story of five years spent watching a business change faster than the system underneath it—and learning that a good architecture can still become the wrong one when the customer changes.

First, we delivered dinner to one person

The original promise was deliberately narrow: Lausanne, in the evening, with food from the city’s own restaurants delivered by bike. At that stage, the priority was not to build a universal marketplace. It was to launch quickly, keep infrastructure costs low and make a small catalogue feel immediate on a phone.

We compiled the entire shop with Gatsby. Products, restaurants, categories and sales channels were read from Sylius during the build, then Capacitor packaged the result for the web, App Store and Play Store.

gatsby-config.js
{ resolve: "gatsby-source-custom-api",
options: { url: process.env.GATSBY_SYLIUS_CHANNEL_URL, rootKey: "SyliusChannel" } },
{ resolve: "gatsby-source-custom-api",
options: { url: process.env.GATSBY_SYLIUS_PRODUCT_URL, rootKey: "SyliusProductBeta" } },
{ resolve: "gatsby-source-custom-api",
options: { url: process.env.GATSBY_SYLIUS_VENDOR_LAUSANNE, rootKey: "SyliusVendor" } },

A static catalogue sounds like an odd choice for restaurants whose menus change regularly. The solution was to update the application without waiting for another store review: every build produced a downloadable web bundle, and the installed app checked for a new version as people moved through it. A menu change meant a build and a lightweight bundle update, not a new mobile release.

A restaurant should be able to change its menu without waiting for Apple.

Only stock, cart and payment remained live. For one city and one delivery period, this was fast, inexpensive and appropriately simple. Its limits were equally clear: the evening was hard-coded into the build, and one cart could contain food from only one restaurant. At the time, that matched the operation perfectly—one kitchen, one bag, one bicycle.

Then one order became a group decision

The delivery minimum changed the product. A person ordering dinner rarely reached it; three colleagues ordering together usually did. But colleagues do not necessarily want food from the same place, and a basket shared by several people cannot live inside one browser.

The second Kooko app was therefore built around a server-side, shareable order. One person started the basket, sent its link to the table, and everyone added their own meal—even from another restaurant. The order was confirmed once the group reached the minimum.

The second Kooko application in light mode, showing the delivery address, evening selection and a dish ready to order.The same Kooko ordering screen in dark mode, with an identical layout and adapted colours.

We also separated editorial content from commerce. Strapi managed restaurant pages and menus at the pace the team needed; Medusa managed products, prices, baskets and payments transactionally. Each restaurant received its own store and access, while a small relation joined the editorial menu to the corresponding commerce product.

A cart shared by a table is no longer browser state. It is part of the product.

Kooko’s restaurant discovery screen in light mode, with a map of Lausanne, cuisine filters and partner restaurants.The same restaurant discovery screen in dark mode, preserving the map, filters and partner grid.

The interface changed with the ordering model. The first application asked what someone wanted to eat that evening. The second helped a group choose among restaurants and cuisines. Technology followed a genuine shift in customer behaviour rather than an abstract desire to rebuild.

The customer became the employer

The next change came from delivery economics. Fifty meals delivered to fifty homes require fifty journeys. Fifty meals delivered to one office require one. That difference moved Kooko from selling dinner to individuals to selling lunch programmes to companies.

A corporate account added to the public marketplace would not have been enough. Each company needed its own restaurants, negotiated prices, payment methods and ordering rules. We built a dedicated portal whose context began in the URL: the company slug resolved its catalogue, customer group, subscription and checkout configuration.

src/utils/routing/navigation.ts
company: defineRoute('/[companySlug]', {
params: z.object({ companySlug: z.string() }),
}),
cart: defineRoute('/[companySlug]/cart', {
params: z.object({ companySlug: z.string() }),
search: z.object({ cart_id: z.string().optional() }).default({}),
}),

That gave employees a portal tailored to their workplace. One company paid by invoice; another offered card or Twint. One ordered from a small selection near its office; another had a broader catalogue. The shared-order concept from the consumer app became a quotation and pre-order flow suited to corporate approval.

The Kooko FAQ displayed as a list of collapsed questions on a phone.The Kooko FAQ displayed as a list of collapsed questions on a phone.

Phone390px

Kooko’s office lunch offer presented on a tablet.Kooko’s office lunch offer presented on a tablet.

Tablet834px

The Kooko restaurant catalogue displayed as a grid on a desktop browser.The Kooko restaurant catalogue displayed as a grid on a desktop browser.

Desktop1440px

Three pages, three widthsThe public site explained the service and presented its restaurants. The company portal carried the catalogue, prices and ordering rules specific to each employer.

The automation behind the portal was one of the project’s strengths. A company arriving through the public form could become a qualified prospect, receive its own record and portal, and start with a catalogue assembled from restaurants around its office.

The Kooko contact page asking for a company name, contact details and the number of employees to serve.
FigureThe public entry point for a company. Behind it, the workflow prepared the prospect record, portal and relevant restaurant catalogue.

But the system had become heavier with every successful answer. ERPNext handled subscriptions, pricing rules and stock. Medusa handled commerce. Strapi handled content. Directus handled operational data, logistics and internal views. n8n synchronized the four. This was a sensible way to prove that companies wanted the service, but it was not a sensible way to open city after city.

Geneva forced us to ask a better question

When Geneva became the next market, the architecture answered with another deployment, another commerce instance, another schema and another portal to maintain. We had designed tenancy as infrastructure: every new market meant another copy of the system.

Scroll sideways, or use the arrow keys.

  • A 2022 Kooko architecture proposal with a separate backend and PostgreSQL database for each client beside a shared core.
    In 2022, each client received its own backend and database.
  • A 2025 Kooko infrastructure proposal with a separate commerce deployment, schema and portal for each region.
    In 2025, each city still implied another commerce deployment, schema and portal.
GalleryTwo plans drawn three years apart reached the same limit: growth meant duplicating infrastructure.

The tooling reinforced that model. Medusa’s first major version assumed one store per installation. We had already removed a database constraint simply to support one store per restaurant—a clear indication that Kooko’s business model had outgrown the model offered by the platform.

A second market should add data, not infrastructure.

The fourth architecture began with that principle. A market, restaurant, company, courier, menu or quotation had to become a record inside one platform. Relations would replace synchronizations, and a new city would become configuration rather than a parallel deployment.

A module map connecting companies, menus, delivery, marketplace sellers and quotations to the core commerce modules.
FigureThe fourth architecture described Kooko through business modules and relations instead of separate services joined by synchronization jobs.

This time, we deliberately wrote less. Medusa’s second version provided modular commerce; Mercur already handled marketplace sellers, split orders and payouts; an existing B2B foundation covered companies, employees, approvals and quotations. Our task was to combine them and add only what was specific to food delivery: menus, couriers and delivery rounds.

The proposed Kooko platform connecting Next.js and Payload CMS to Medusa and Mercur modules for companies, restaurants, orders, sellers and payments.
FigureContent and commerce kept distinct responsibilities, but identifiers replaced copied records. The architecture removed entire synchronization flows rather than making them more sophisticated.
A navigation map where Lausanne, Geneva and Vevey lead to market-specific company, courier, administration and restaurant routes.
FigureCities, companies, couriers and restaurants all resolved to records within the same platform. Geneva would have been a market and a path—not a second copy of Kooko.

What Kooko changed in the way we build

Geneva was meant to prove the fourth model before Lausanne migrated onto it. Kooko stopped trading before that launch. The architecture remained a design, not a production success, but the reasoning behind it became one of the most useful outcomes of the company.

Five years taught us that architecture is not a permanent expression of technical taste. It is an answer to who the customer is, how money moves and what the business needs to repeat. The static application was right for one city and one evening offer. The shared cart was right when colleagues ordered together. The corporate portal was right while Kooko proved its B2B model. The marketplace design became necessary only when opening a second city exposed the cost of duplicating everything.

That experience still guides our choices. For the Cabinet des Alpettes, the right answer was the lightest CMS that gave the practice autonomy. For Autocorner, it was a shared data layer capable of reconciling brands, centres and live inventory. Kooko taught us to tell the difference early—and to build only as much platform as the next real step requires.

Journal

More from the journal

  • How the web became the default

    Every platform once demanded its own language, toolchain and answer to deployment. The web escaped that and became the default for almost everything, including the things it was worst at, and it never handed anyone an architecture.

    ArchitectureOpen source11 min read
  • How the web stack grew up

    The last decade did not produce a winning framework. It produced better runtimes, lighter delivery, shared standards and durable workflows, plus two answers nobody yet ships in the same box: opinionated frameworks and open standards.

    ArchitectureOpen sourceDeployment19 min read
  • When content management and business workflows meet

    Autocorner’s website became the foundation for something larger: reusable forms, real availability, notifications and internal tools that connect customer journeys with daily dealership work.

    ArchitectureContentCommerce5 min read

Next step

Recognise the problem?

If one of these sounds like your system, the first conversation costs nothing.