One Codebase, Fifteen Restaurant Brands: Architecting the Sipo Website Builder
How I designed a multi-tenant platform where a small team ships and maintains 15+ live restaurant websites from a shared component system and a single typed API — the architecture, the trade-offs, and what a design system really buys you at scale.
Namith K P
· 25 min read
At my day job I build products on the Sipo Cloud POS — the platform restaurants and retailers across New Zealand and Australia run their businesses on. One of the things I'm most proud of architecting is the Sipo Restaurant Website Builder: a system where a small team ships and maintains more than a dozen live, distinct restaurant brand websites — Coffee Hub, El Greko, Tarka, Royal Tandoor, The Bicycle Thief, and more — from a single shared codebase, without a developer touching the site every time a restaurant changes its menu.
This post is the architecture story behind that. It's about multi-tenancy, design systems, and the specific trade-offs that let a handful of engineers support a growing roster of brands without drowning. It's the kind of problem I find most rewarding — not a single hard algorithm, but a system that has to stay simple to reason about while doing more and more, for more and more brands, over time. And because the earlier version of this post was a short love letter to building UI components, I've kept that spirit — but grown it into what components actually become when you take them seriously at scale: a system, not a folder.
The problem: many brands, one team
Picture the naive version first, because it's how most agencies do it and it's a trap. A restaurant wants a website. You build them a website. Another restaurant wants one; you copy the first project, find-and-replace the branding, tweak the layout, deploy. Repeat fifteen times. Six months later you have fifteen codebases, fifteen sets of dependencies drifting apart, fifteen places a bug lives, and a security patch that takes fifteen deployments. Every new brand makes the whole thing heavier. The marginal cost of the sixteenth site is as high as the first. That doesn't scale — it anti-scales.
The insight that breaks the trap is that these sites are not fifteen different things. They're fifteen configurations of the same thing. A restaurant website is a menu, a brand identity, some content, hours, a location, and an ordering integration — arranged in one of a handful of proven layouts. The differences between Coffee Hub and Royal Tandoor are real and important to their owners, but they are almost entirely data and styling, not structure. Once you see that, the architecture writes itself: build the structure once, make the data and styling a per-tenant configuration, and let the sites be instances rather than copies.
Multi-tenancy: the core decision
The heart of the system is multi-tenancy — one application serving many tenants (restaurants), each seeing their own branded, content-isolated site. Getting this boundary right is the whole game, so it's worth being precise about what's shared and what's isolated.
Shared across all tenants: the component library, the page templates, the API client, the rendering logic, the deployment pipeline, the performance and accessibility work. Every improvement here lifts every brand at once. Fix a bug in the menu component, and fifteen restaurants get the fix simultaneously. Improve the Core Web Vitals of the layout, and every site gets faster. This shared core is where the leverage lives, and it's why the marginal cost of a new brand drops toward zero instead of staying flat.
Isolated per tenant: the brand identity (colors, typography, logo, imagery), the content (menu, story, hours, location), and the choice of template. A restaurant edits these from the Sipo admin, and their site — and only their site — reflects the change. No tenant can see or affect another's data. That isolation is not a nice-to-have; it's the contract that makes the whole thing trustworthy.
The architectural discipline is keeping that line clean. Every time you're tempted to special-case one brand in the shared code — "just this once, Royal Tandoor needs a custom header" — you're drilling a hole in the boundary that separates shared from isolated. Do it enough times and you're back to fifteen codebases wearing one repo's clothes. So the rule I hold is strict: brand-specific needs are expressed through configuration and theming, never through conditional branches in shared components. If a brand needs something the system can't express, the answer is to make the system able to express it — as a new configurable capability every brand can use — not to carve out an exception for one.
The design system underneath
Here's where my old affection for UI components grows up. At the scale of one site, a "component" is a convenience — a button you don't want to restyle every time. At the scale of fifteen brands sharing one codebase, the component library becomes the substrate the entire business runs on, and it demands a completely different level of rigor.
Every component has to be themeable without being forked. A menu card, a hero section, a navigation bar — each must render correctly and beautifully across a coffee shop's warm minimalism, a Greek taverna's bold Mediterranean palette, and an Indian restaurant's rich, ornate identity, purely through design tokens. The moment a component can only look right for one brand, it's not a shared component anymore; it's a liability that will get copied and diverged. So theming isn't a layer on top of the components — it's designed into them from the first line. Colors, typography, spacing, and imagery flow from per-brand tokens, and the components are built to wear whatever identity the tokens carry.
Every component also has to be content-agnostic. It can't assume the menu has three categories or the hero has a background video, because across fifteen restaurants it won't. Components take content as data and render whatever they're given, gracefully. This sounds obvious and is constantly violated in practice — the fastest way to break a multi-tenant system is a component that quietly assumes the shape of one tenant's data.
And every component has to hold a quality bar that's now multiplied by fifteen. An accessibility bug isn't one bug; it's a bug on every site. A slow component isn't one slow page; it's fifteen. That multiplication is intimidating, but it's also the point: it means investment in the shared components pays back fifteen-fold, which justifies a level of care you could never afford on a one-off. The design system is where I get to do my best, most careful component work, precisely because it matters so much more than it would on a single site.
How theming actually works
Let me make the theming concrete, because "design tokens" can sound like hand-waving until you see the mechanism. Every brand is, at its core, a set of tokens — the values that define its identity — and every shared component reads from those tokens rather than from hardcoded values. A simplified version of the idea looks like this:
// A brand is just a configuration.
interface BrandTheme {
colors: { primary: string; surface: string; ink: string; accent: string };
fonts: { display: string; body: string };
radius: string;
logo: string;
}
// Components consume the theme, never hardcoded values.
function MenuCard({ item, theme }: { item: MenuItem; theme: BrandTheme }) {
return (
<article
style={{
background: theme.colors.surface,
color: theme.colors.ink,
borderRadius: theme.radius,
fontFamily: theme.fonts.body,
}}
>
<h3 style={{ fontFamily: theme.fonts.display }}>{item.name}</h3>
{item.price != null && <span>{formatPrice(item.price)}</span>}
</article>
);
}The real system is more sophisticated — tokens flow through CSS custom properties and
Tailwind configuration rather than inline styles, so there's no per-render cost and the
cascade does the work — but the principle is exactly this. The component describes
structure and behavior; the brand supplies appearance. Coffee Hub and Royal Tandoor
render through the identical MenuCard, and they look nothing alike, because the tokens
they feed it are worlds apart. Notice too the item.price != null check: that's the
content-agnostic discipline in miniature — the component never assumes a menu item has a
price, because across fifteen brands, some won't.
The discipline this demands is that no shared component may ever contain a brand value. Not a color, not a font name, not a magic number that happens to look right for one restaurant. The moment a hardcoded value sneaks into a shared component, it's a lie that will render wrong for fourteen other brands. Every value that varies by brand comes from the theme; every value in the component is either structural or derived from a token. Hold that line and the components stay universal. Break it and they quietly rot into brand-specific forks wearing shared-component names.
The typed API client: one contract, every site
Sitting between the templates and the Sipo Cloud POS backend is a shared, typed API client, and it's one of the most important pieces of the whole architecture even though users never see it.
Every site needs the same categories of data from the POS: the menu, hours, store details, ordering configuration. Without a shared client, each template would talk to the backend in its own slightly different way, and those small differences would compound into fifteen subtly incompatible integrations — the exact fragmentation multi-tenancy is supposed to prevent, sneaking in through the back door. With one typed client, there's a single, well-defined contract between the sites and the backend. The types mean that when the API changes, the compiler tells me every place that needs updating, across every template, before anything ships broken. That's not a convenience; at this scale it's the difference between a change being safe and a change being a gamble.
TypeScript earns its keep hardest here. The types are the enforcement mechanism for the contract — they turn "I hope every site handles a menu item without a price correctly" into "the code will not compile until every site handles a menu item without a price." When your blast radius is fifteen live businesses, you want as much of your correctness as possible enforced by the compiler rather than by hope and manual testing.
Templates: structure as a product
On top of the components and the API client sit the templates — a handful of complete, proven page structures. A restaurant picks a template, and the template arranges the shared components into a full site, hydrated with that brand's content and styling.
The number of templates is itself a deliberate design decision. Too few and brands feel generic and constrained; too many and you're back toward the maintenance burden you were fleeing, because every template is more shared structure to keep working across every brand and every change. The sweet spot is a small, curated set that covers the real range of what restaurants need — a menu-forward layout, a story-forward layout, a booking-forward layout — each polished to a level no one-off build would justify. The constraint is a feature: because there are few templates, each one gets disproportionate care, and every brand benefits from that care.
I think of the templates as products in their own right, with their own quality bar and their own evolution. When I improve a template, every brand on it improves. When a new brand needs something no template offers, that's a signal to consider whether it's worth adding to a template — a capability the whole roster can share — rather than a reason to go off-book for one client. The templates are how "structure" itself becomes something you ship and maintain deliberately, not something you re-improvise per project.
Onboarding a new brand
The truest test of a multi-tenant architecture is how it feels to add the next tenant. If onboarding a new restaurant means a developer spinning up a project, wiring integrations, and hand-building pages, you haven't built a platform — you've built a template you copy, and you're back in the anti-scaling trap. The goal is that a new brand is configuration, not construction.
In the Sipo builder, adding a brand looks roughly like this: the restaurant is set up in the Sipo POS, so its menu, hours, and store data already exist and flow through the same typed client every other site uses. A brand theme is defined — the colors, fonts, logo, and imagery that make it them. A template is chosen. And the site comes to life, hydrated from real POS data, styled by the brand's tokens, structured by the chosen template. No new components. No new integration code. No bespoke build. The new brand inherits every improvement, every bug fix, every performance gain the shared core has ever received, for free, on day one.
That last part is the quiet magic of the model and it compounds over time. The fifteenth brand launches on a system that's been hardened by fourteen brands' worth of real-world use. Every edge case a previous restaurant hit, every accessibility issue someone reported, every performance problem that got solved — the new brand benefits from all of it without anyone doing anything special. In a copy-paste world, the fifteenth site starts from the same shaky baseline as the first. Here, it starts from the accumulated hardening of everything that came before. The platform gets better at onboarding brands as it onboards more of them, which is the exact opposite of the anti-scaling trap.
Performance and SEO across many brands
A restaurant website lives or dies on two things the owner cares about deeply: it has to be fast, and it has to be findable. Both are areas where the shared-core model turns into a decisive advantage, and it's worth explaining why.
On performance, because every brand renders through the same components and the same rendering strategy, performance work is done once and inherited everywhere. I can invest seriously in the loading behavior, the image handling, the Core Web Vitals of the shared layout — and every brand, present and future, gets a fast site as a birthright rather than as a per-project effort. Contrast that with fifteen hand-built sites, where each one has its own performance profile and its own set of problems, and improving all of them means fifteen separate investigations. Here, one investigation lifts the whole roster. The static-first rendering model matters here too: a restaurant's menu and content are rendered ahead of time and served fast, revalidating when the owner makes a change, so visitors get near-instant pages without the sites hammering the POS on every request.
On SEO, the same logic holds. Structured data for restaurants, correct metadata, semantic markup, clean URLs, sensible sitemaps — these are the kind of technical SEO foundations that are tedious to get right on one site and maddening to maintain across fifteen. Built into the shared templates, they're done once and correct everywhere. Every brand gets a technically sound foundation for being found by the customers searching for them, and when search best practices shift, I update the shared templates and the whole roster stays current. For local restaurants, being findable is close to existential, and the platform gives every one of them a solid baseline they'd struggle to afford individually.
This is the recurring theme in one more guise: do the hard work once, in the shared layer, and let every brand inherit it. Performance, SEO, accessibility, correctness — each of these is a place where the multi-tenant model converts a small team's focused effort into an outcome that would take a large team to replicate across fifteen separate codebases.
The payoff: zero-developer content editing
The measure of whether the architecture worked is a human one: restaurant owners edit their own sites, and developers stay out of the loop entirely. A restaurant changes a price, adds a seasonal special, updates their hours, swaps a photo — all from the Sipo admin, and it propagates to their live site in real time. No ticket, no deploy, no developer.
This is the whole point, and it's worth dwelling on why it matters beyond convenience. When content editing requires a developer, the developer is a bottleneck on every trivial change across every brand, and the restaurant is frustrated waiting on a price update that should take ten seconds. When content editing is self-service, the developer's time is freed for the things only a developer can do — building new capabilities, improving the shared core, onboarding new brands — and the restaurant has direct control over its own presence. The architecture doesn't just make the team more efficient; it puts control where it belongs, with the people who own the business.
That separation — developers own the system, restaurants own their content — is the clean seam the entire design is organized around. Everything I described, the multi-tenancy, the themeable components, the typed client, the curated templates, exists to make that seam real and durable.
The stack, and why each piece
I've mentioned the tools in passing, but since the choices were deliberate, here's the reasoning — because in a multi-tenant system, the stack decisions compound across every brand, so getting them right matters more than usual.
Next.js for the framework, because its rendering model is exactly what a roster of content-driven sites needs: static generation for speed, incremental revalidation for freshness, and a component model that makes the shared-core architecture natural rather than forced. The routing, the image optimization, the data-fetching patterns — they all line up with what a multi-tenant content platform actually requires, so I'm working with the framework rather than around it.
TypeScript everywhere, non-negotiably, for the reason I keep returning to: when a change can break fifteen live businesses, you want the compiler to be your first line of defense. Types turn a whole class of "this works on most brands but breaks on the one with unusual data" bugs into compile errors you fix before anyone sees them. The stricter the types, the more the compiler carries, and the less rests on hoping I remembered every case. At this blast radius, that trade is obvious.
Tailwind for styling, because a utility-first, token-driven approach is a remarkably good fit for a themeable system. Design tokens map cleanly onto Tailwind's configuration, the styling stays colocated with the components that use it, and there's no separate stylesheet architecture drifting out of sync with the markup. It makes the "components read appearance from tokens" discipline easy to hold in practice, which matters, because a discipline that's hard to follow won't be followed.
Vercel for deployment, because the static-first, edge-served model it's built around is precisely the delivery model the sites want, and because a smooth, reliable deployment pipeline is not a luxury when you're shipping to a growing roster of live businesses. The less friction between "the change is ready" and "the change is live and safe," the more often I can improve the shared core with confidence.
None of these choices is exotic, and that's the point. Good architecture doesn't come from exotic tools; it comes from choosing solid, well-matched tools and then organizing them around the right boundaries. The stack is the easy part once the architecture is right — and it's almost interchangeable if the architecture is wrong.
The scary part: a bug is a bug times fifteen
Everything I've praised about the shared-core model has a shadow, and honesty requires naming it clearly. The same leverage that makes a good change lift every brand at once makes a bad change break every brand at once. When your components are shared across fifteen live businesses, a regression isn't an incident on one site — it's an incident on all of them, simultaneously, in production, in front of real customers trying to order dinner.
That reality forces a discipline that a one-off site never demands, and I've come to see it as one of the most valuable things the project taught me. You cannot ship to fifteen businesses on vibes. Every change to the shared core has to be treated as what it is: a change to a platform, not to a page. That means the compiler carries as much weight as possible — the typed API client and strict TypeScript catch entire categories of "this breaks on brands where the data looks slightly different" before anything deploys. It means changes to shared components get reviewed with the whole roster in mind, not just the brand that prompted the change. And it means verifying against the real variety of brands, because a change that looks perfect on Coffee Hub's clean minimalism might break on Royal Tandoor's dense, ornate layout, and the only way to know is to actually look.
The mindset shift is from "does this work?" to "does this work for every brand, present and future?" That's a genuinely harder question, and holding yourself to it is tiring in the way that all real engineering discipline is tiring. But it's also where the craft is. Building something that has to stay correct across many contexts, under real load, with a blast radius that concentrates the mind — that's the kind of work that makes you better, precisely because it doesn't let you be sloppy. The stakes are the teacher.
The rendering strategy, and why it matters
A quick word on how the pages actually get to the browser, because it's a load-bearing decision that's easy to gloss over. Restaurant content changes, but not constantly — a menu updates when the restaurant changes it, not on every page view. That profile is a perfect fit for static generation with revalidation: pages are pre-rendered and served from the edge, fast and cheap, and they regenerate when the underlying content actually changes rather than on every request.
The alternative — rendering every page fresh on every visit — would mean every customer load hits the POS backend, which is slower for the visitor, heavier on the backend, and more expensive to run, all for content that didn't change between one visitor and the next. By rendering ahead of time and revalidating on real changes, every brand gets pages that feel instant while the backend stays calm even when a site gets a rush of traffic at dinner time. It's the right shape for the domain, and getting the rendering model matched to the actual behavior of the data is one of those architectural choices that pays off invisibly, every single day, on every single brand.
Trade-offs I made consciously
No architecture is free, and pretending otherwise is how you mislead the next engineer. Here are the trade-offs I took on purpose.
Less per-brand flexibility, deliberately. A fully bespoke site can do anything; a template-based brand site can do what the system supports. I traded unlimited flexibility for maintainability and consistency, and for this business it's overwhelmingly the right trade — the value is in supporting many brands well, not one brand infinitely. But it's a real constraint, and I'm honest with stakeholders that the system has an expressive range, wide as it is.
More upfront complexity for less ongoing cost. A multi-tenant, themeable, API-driven system is more complex to build than one hardcoded site. The bet is that the upfront complexity buys dramatically lower marginal cost per brand and per change forever after — and across fifteen-plus brands, that bet has paid off many times over. But it does mean the first brand cost far more than a one-off would have. You have to be building for the fifteenth site, not the first, for the math to work.
A stricter discipline than one-off work. The rule against special-casing brands in shared code is a real ongoing tax on developer freedom. It's occasionally faster, in the moment, to just add the conditional. Holding the line costs discipline. But every hole you don't drill in the shared/isolated boundary is a hole you don't have to maintain forever, so the discipline pays a dividend that compounds quietly with every brand added.
Where it sits in the bigger picture
The website builder doesn't exist in isolation — it's one piece of a larger Sipo Cloud POS ecosystem, and understanding that context is part of why the architecture is shaped the way it is. The same POS backend that powers a restaurant's in-store ordering, its online ordering, its QR-code dine-in flow, its kiosks, and its table bookings is the backend the website builder draws from. A restaurant's menu isn't entered separately for its website; it is the POS menu, the same data that drives every other channel. When the restaurant updates a price, it updates everywhere at once — the till, the online order page, the website — because there's one source of truth underneath all of it.
That's the deeper design principle the website builder is an expression of: one system of record, many channels. The website is a view onto the restaurant's data, the same way the online ordering app is, the same way the kiosk is. Building the website system to draw from the shared POS backend through the same typed contracts as everything else is what keeps the whole ecosystem coherent instead of fragmenting into a dozen tools that each hold their own slightly-stale copy of the truth. Architecting the website builder well meant thinking about it not as a standalone product but as a citizen of a larger system — which is exactly the kind of systems thinking that separates building a feature from building a platform.
What it means for the restaurants
It's easy, deep in the architecture, to lose sight of who this is actually for, so let me end the technical part with them. The restaurants on this platform are, for the most part, small and independent — a coffee shop, a family Indian kitchen, a neighborhood Italian place. These are businesses for whom a custom website from an agency is a serious expense, and a good one is often out of reach entirely. What the platform gives them is a genuinely professional web presence — fast, findable, on-brand, and fully theirs to edit — at a fraction of what a bespoke build would cost, because the expensive engineering was done once and is shared across all of them.
That's the part I find most satisfying, and it connects back to everything I care about as an engineer. The architecture isn't clever for its own sake; it's clever in service of making something valuable accessible to people who'd otherwise be priced out. A small restaurant getting a site as fast and polished as a big chain's, and being able to run it themselves without a developer on retainer — that's real leverage pointed at real people. The multi-tenancy, the design system, the typed contracts: all of it exists so that a family running a restaurant can have a great website and get back to cooking.
What this taught me about scale
The Sipo Website Builder crystallized something I now believe generally: scale is a design problem before it's a technology problem. The tools — Next.js, React, TypeScript, Tailwind, Vercel — matter, but they're not what makes fifteen brands tractable for a small team. What makes it tractable is the architecture: the clean line between shared and isolated, the design system rigorous enough to wear any brand, the typed contract that makes change safe, the curated templates that turn structure into a product. Get those right and the technology is almost interchangeable. Get them wrong and no framework saves you.
It also taught me that a design system's real value isn't consistency for its own sake — it's leverage. Every hour I put into a shared component returns fifteen-fold across the roster, and that multiplier is what lets a small team punch far above its size. That reframing changed how I value component work entirely. It's not the fun-but-minor part of building for the web; done right, at the right layer, it's the highest-leverage engineering on the team.
Why I still love building components
So, to bring it back to where the earlier version of this post started: yes, I still love building UI components. But I've come to love them for a bigger reason than the craft satisfaction of a well-made button. A component, taken seriously and placed at the right architectural layer, is a lever. Build it once, build it well, and it lifts every brand, every page, every user, every time — long after you've moved on to the next thing.
There's a broader lesson in here about how I've come to think about seniority as an engineer. Early on, you measure yourself by what you can build — can I make this button, this page, this feature. Later, the measure changes to what you can build that other people build on top of, and that stays correct and pleasant to use as it grows. The Sipo Website Builder is the clearest example I've worked on of that second kind of value. Its worth isn't in any one site; it's in being the foundation a growing number of sites stand on, and in staying trustworthy as that number climbs. Building things other things depend on — and taking the responsibility that comes with that dependency seriously — is the work I most want to be doing, and this project is where I learned to love it.
That's the version of front-end work that stops being "just front-end" and becomes architecture. It's where design systems, type safety, API design, performance, and product thinking all meet, and it's some of the most satisfying engineering I do — precisely because the care compounds. One codebase, fifteen brands, one small team, and a system built so that each of those numbers can keep growing without the others buckling. That's what components become when you let them grow up.
If you're building multi-tenant systems or design systems at scale and want to compare notes on where to draw the shared-versus-isolated line, get in touch — it's one of my favorite things to talk about.