Designing a Multi-Store SaaS Platform with C4
I set out to make sense of a messy multi-store SaaS idea by forcing myself to model it properly with C4.
TL;DR
- I modeled a multi-store ecommerce SaaS where one user can run unlimited stores\
- I used C4 to clarify tenants, domains, and data boundaries\
- This is for product engineers and founders thinking about multi-tenant SaaS
Goal
My goal was to turn a vague product idea into a clear architectural understanding. I had a platform concept where a single user could spin up unlimited niche stores, each with its own branding, catalog, and orders, but all running on shared infrastructure. In my head it made sense, but when I tried to explain it to others, I noticed confusion around what was a "user," what was a "tenant," and what actually needed isolation.
Success for me meant being able to answer simple but critical questions. Where does data live? What is global versus store-scoped? What parts of the system are shared, and what parts behave as if they are isolated? If a new engineer could look at the diagrams and quickly understand these boundaries, I considered that a win.
I also wanted a tool for myself. When building fast, it's easy to make accidental architecture decisions that become expensive later. I wanted C4 modeling to act as a forcing function to think more deliberately.
Context
The stack I'm working with is fairly typical for a modern SaaS: Next.js on the frontend and server side, Firebase for auth and data, and Stripe for payments. Nothing exotic. The complexity didn't come from the tools, it came from the product model.
The tricky part is that this is not just multi-user SaaS, it's multi-store SaaS. One human can be associated with many stores. Each store is logically its own mini-business. That means per-store catalogs, per-store branding, and per-store orders. But from an infrastructure perspective, it's all one app.
I've seen prior art in Shopify, Gumroad, and various marketplace platforms. C4 itself, via c4model.com, gave me the modeling vocabulary. Structurizr and similar tools gave me examples of how to represent systems at different zoom levels.
Another constraint was time. I didn't want to over-engineer. I wasn't trying to design for 10 million stores on day one. I just wanted clarity and a direction that wouldn't paint me into a corner.
Approach
I decided to approach this from the top down. Instead of diving into code or schemas, I started with the System Context diagram. I asked: who interacts with this platform, and how do they see it? That forced me to define Store Owner versus Shopper versus internal roles.
Then I moved to the Container level. This is where I separated the Owner Console from the Storefront and from the Backend API. Even if they share a codebase, they serve different mental models and responsibilities.
Finally, I looked at domain boundaries inside the backend. Orders, payments, catalog, fulfillment, identity, and so on. I tried to make each domain responsible for a clear slice of the system.
Equally important was deciding what not to model. I didn't go into microservice-level detail. I didn't try to pre-split everything into deployable units. I kept it conceptual but concrete enough to guide decisions.
Steps
1) Setup
The setup was mostly mental. I listed all the real features the product needed: store creation, catalog management, checkout, and orders. Then I mapped them to actors.
I defined a core rule: every piece of business data must be either global or store-scoped. No ambiguous middle. That rule alone clarified many decisions.
I also picked a primary tenant key, storeId. Everything store-related hangs off that. That sounds obvious, but making it explicit early prevents weird hybrids later.
2) Implementation
My first attempt at diagrams was too detailed. I tried to capture every endpoint and every flow. It quickly became unreadable. I realized C4 works best when you intentionally leave things out.
So I simplified. At System Context, I only showed major actors and external systems. At Container level, I only showed deployable or conceptual containers like Web App, API, and Data Stores.
Then I layered in domain thinking. Orders owns order state. Payments owns payment status. Catalog owns products. Each can read from the same database but is conceptually separate.
I also thought about future fulfillment integration. Even if it's not built yet, modeling it as an external system helps avoid coupling the order model too tightly to internal assumptions.
3) Validation
Validation was simple but effective. I walked through the diagrams and narrated a scenario. A store owner creates a new store. A shopper visits it. They place an order. Money flows through Stripe. The order is recorded. Fulfillment is triggered.
If I could trace that story without confusion, the model was doing its job.
I also sanity-checked data flows. No domain should secretly own another domain's data. If that happened, I revised boundaries.
Results
What worked: - I got clarity on store versus platform concerns\
- Conversations with others became easier\
- I felt more confident about data modeling
What didn't: - My first versions were over-modeled\
- It's tempting to treat diagrams as specs instead of thinking tools
Gotchas / Notes
One big gotcha is confusing user identity with tenancy. A user is not a tenant here; a store is. That distinction matters everywhere from auth to billing.
Another note is that shared infrastructure doesn't mean shared data. Logical isolation still matters. Namespacing and access rules are critical.
There are also tradeoffs. A single database is simpler early but can become a bottleneck. Splitting later is easier if domains are clean.
Next
Next, I want to extend this with analytics and experimentation domains. I also want to model payouts and revenue sharing more explicitly. Fulfillment integrations are another big area.
Over time, I may revisit the container model and decide whether certain domains deserve their own services. For now, clarity beats perfection.