Files
rubberquack/README.md

318 lines
9.3 KiB
Markdown
Raw Normal View History

# rubberquack
A showcase project demonstrating a multi-tenant storefront architecture built with Next.js.
The original implementation was developed for a commercial project and cannot be published. This repository recreates the architectural approach using fictional storefronts while preserving the core design decisions.
The storefronts deliberately sell rubber ducks.
The products are intentionally absurd because the products themselves are irrelevant. The purpose of this repository is to demonstrate how multiple brands can share one codebase while feeling like entirely different businesses.
Current storefronts:
- Executive Rubber Ducks
- Crypto Bros Ducks
- Rubber Dubber Supper Ducks
- Collector's Den
All storefronts share:
- one component library
- one application
- one product model
- one routing system
Only their presentation differs.
---
# How this showcase was built
This repository is not the original commercial implementation.
The original implementation belongs to a previous client and cannot be published.
Instead, I recreated the architectural idea as a fictional storefront showcase and used an AI coding agent to generate the surrounding demo application.
That distinction matters.
The core idea, the original architectural decision and the `@store` build-time filtering approach came from the real project.
The rubber duck storefronts, brand names, product copy and demo implementation were created for this repository so the approach could be reviewed without exposing client code.
I have used AI-assisted development for a while, but the workflow has changed significantly over time.
When I first started experimenting with these tools, they were useful for small isolated tasks but not reliable enough to trust with larger implementation flows. They often missed context, produced inconsistent code or needed so much correction that the benefit was limited.
That has changed.
I now use AI coding agents as a regular part of my development workflow.
Not as a replacement for engineering judgment.
As a way to turn a clearly defined technical direction into a working implementation faster.
For this project, my role was to define:
- the architecture being demonstrated
- the tenant model
- the boundaries of the showcase
- the brand concepts
- the expected code structure
- the technical constraints
- the final review and cleanup
The agent helped produce the demo code around those decisions.
That is also why this repository is useful as a sample.
It shows both the architectural idea and the way I currently work with AI: not by handing over responsibility, but by giving it a narrow, well-defined problem and keeping ownership of the product, structure and final result.
---
# Situation
The original project aimed to replace multiple independent storefronts with a single frontend connected to one Rails backend.
The backend became the single source of truth for pricing, business logic, inventory and checkout behavior.
The frontend intentionally remained "dumb". Its responsibility was simply to display information, collect user input and communicate with the API.
After this architecture was established, another problem appeared.
Multiple storefronts had to exist on top of the same backend.
Each storefront required its own visual identity, layout and branding while still sharing the same components and behaviour.
---
# The Problem
The obvious solution would have been to duplicate components or maintain separate component libraries for each tenant.
I rejected that approach.
It increases maintenance cost, creates unnecessary technical debt and makes long-term evolution progressively harder.
The challenge became:
> How can multiple brands share one component library while still feeling completely different?
I wanted developers joining the project years later to immediately understand where a component lived, where its styles belonged and how tenant-specific customisations worked.
The solution needed to scale from two storefronts to dozens without turning the project into conditional logic scattered throughout the codebase.
---
# The Ideal Solution
To me, the ideal architecture looked like this:
- one component library
- one codebase
- one styling location per component
- zero duplicated components
- zero runtime tenant switching
- zero global namespace hacks
The component itself should never know which tenant it belongs to.
The build process should.
If a storefront was built for Tenant A, Tenant B should simply not exist inside the final CSS output.
That became the design goal.
---
# The Actual Solution
The solution turned out to be surprisingly small.
I introduced a custom PostCSS plugin that adds a new directive:
```css
@store executive {
...
}
```
Every block inside that directive is evaluated before Tailwind processes the stylesheet.
During the build, the plugin compares the configured tenant against the directive.
If they match:
- keep the CSS
If they don't:
- remove it completely
By the time Tailwind generates the final stylesheet, it only sees styles belonging to the active storefront.
The other tenants simply never exist in that build output.
This allows components like:
```
Gallery
ProductCard
Navigation
Hero
Footer
```
to remain identical across every storefront while each tenant receives a completely different visual identity.
The HTML never changes.
The business logic never changes.
Only the presentation changes.
---
# Why This Approach
I like solutions that disappear.
The plugin itself is intentionally small.
Most of the value comes from what it removes rather than what it adds.
Without introducing:
- runtime switches
- CSS namespace hierarchies
- duplicated components
- duplicated styling systems
it allows every storefront to feel independent while preserving a single maintainable codebase.
New tenants also become inexpensive.
A new storefront can start with the shared baseline presentation.
Only the parts that actually need a different visual treatment receive tenant-specific overrides.
Everything else continues to work automatically.
---
# The exception to the rule
The general rule was that components should not contain tenant-specific branching.
That does not mean it was impossible.
In the original project, there were cases where checking the active tenant inside a component was the right decision. The important part was that this became an explicit exception rather than a normal pattern.
That distinction mattered.
If a component contained a hardcoded tenant check, it was immediately visible as a deliberate product decision. Not as a workaround. Not as "we did not know where else to put this."
The structure allowed us to stay flexible without turning flexibility into chaos.
Most tenant differences belonged in the build-time styling layer.
A few product differences belonged in the component.
The value was knowing which was which.
---
# Trade-offs
This architecture intentionally keeps a default presentation inside every component.
Tenant-specific styles override those defaults where necessary.
That means the final CSS still contains some shared styling that could theoretically be removed.
I considered eliminating it entirely.
In practice, keeping sensible defaults dramatically reduced the effort required to launch new storefronts and allowed incomplete tenants to remain functional while gradually receiving their own visual identity.
I considered that trade-off worthwhile.
---
# Storefronts
Four brands, one codebase, selling the same rubber duck catalogue:
| Store code | Brand | Character |
| ----------- | ------------------- | ---------------------------------------- |
| `executive` | Onyx | luxury, dark + gilt, Cormorant serif |
| `crypto` | DEGEN//DUCKS | neon-green on near-black, Space Grotesk |
| `supper` | Rubber Dubber | warm cream supper-club, Newsreader serif |
| `collector` | The Collector's Den | vintage paper + maroon, Spectral serif |
The same product can appear as "The Founder", "Lambo Duck", "Honey Butter", or "The Bullion" depending on the active storefront.
Same product model.
Same component.
Different brand context.
---
# How it works
- **Tokens:** `src/styles/globals.css` defines semantic design tokens such as `--color-bg`, `--color-accent`, `--font-display`, and `--radius-card`.
- **Components:** Components share one DOM structure and style themselves from those tokens. When a storefront needs a different layout, the component's `*.module.css` file can include an `@store <code> { ... }` block.
- **Plugin:** `plugins/postcss-storestyles` reads `NEXT_PUBLIC_STORE_CODE` and removes every non-matching `@store` block before Tailwind compiles.
- **Content:** The shared catalogue lives in `src/data/products.ts`. Per-store copy lives in `src/data/overrides/<code>.ts`. Per-store configuration lives in `config/store_<code>.ts`.
---
# Running a storefront
Requires [bun](https://bun.sh).
```sh
bun install
bun run dev:executive
bun run dev:crypto
bun run dev:supper
bun run dev:collector
```
Default:
```sh
bun run dev
```
`bun run dev` starts the executive storefront.
# Building
```sh
bun run build:executive
bun run build:crypto
bun run build:supper
bun run build:collector
```
# Project layout
```txt
config/
store_<code>.ts
store.ts
stores.ts
plugins/
postcss-storestyles/
src/
app/
components/
data/
styles/
```