Skip to content
Case study · 12 / 15← All work

Tiendas

One online store core and a CLI that turns it into a new client store, now running Rodar MDQ and BiciTienda MDQ.

Role
Architecture and full build
Stack
Next.js 16, TypeScript, Tailwind 4, Drizzle, Neon Postgres / PGlite, React Email, Go
The starter, as Faro
The starter, as FaroThe template is generated from a live store
(01) The problem

Small shops in Mar del Plata kept asking for the same thing: a catalog, a cart, a checkout, orders and an admin they can run themselves. The first one, 3:23, was a frontend on mock data. Building each new store from scratch would cost weeks, and copying the last one by hand would carry the last client's brand, copy and photos into the next.

My roleI split the stores into a shared core and a small per-store layer, and wrote the CLI that generates a new store from it.
(02) ResultsCounted in the code
2client stores in production on the same core
11files a new store replaces. Everything else is core
6integrations with a local stand-in, so a store runs without credentials
(03) Architecture

Three decisions that shaped it.

  1. 01

    The template is generated from a live store

    Why. Rodar MDQ is the reference store. The CLI exports its last commit with git archive, strips Rodar's photos, copy and Cloudinary URLs, lays a placeholder brand called Faro on top, and checks that every migration and db script arrived intact. The starter is never edited by hand, so it can't drift from code that runs in production.

    Trade-off. A core fix has to land in Rodar first. A few Rodar leftovers in core files are swapped out by a list of string replacements, which only warns when a string goes missing.

  2. 02

    Copy the core, don't share a package

    Why. new-store copies the starter into its own git repo, with its own Neon database and Vercel project. Brand words, routes and modules live in config and a lexicon, so BiciTienda could add sizes and colors, appointments and customer accounts without touching Rodar.

    Trade-off. A fix doesn't reach stores that already exist. Each one gets it by cherry-picking the commit by hand.

  3. 03

    Every integration has a local stand-in

    Why. Without env vars the store uses PGlite instead of Neon, writes emails to a local outbox, pays through a sandbox page and opens the admin with a dev PIN. A new store runs with migrate, seed and dev, and I can show it to a client before anyone creates an account anywhere.

    Trade-off. Two code paths per integration. The sandbox payment page had to be locked out in production, and PGlite can hide a difference from Neon until deploy.

(04) GalleryKeep scrolling →
The starter, as Faro
The starter, as Faro · What new-store produces: the full core with a placeholder brand, demo products and two sample branches.
Rodar MDQ
Rodar MDQ · The reference store, for electric bikes and motorbikes. Same layout as the starter, with its own brand, routes and catalog.
BiciTienda MDQ
BiciTienda MDQ · Generated with the carbono identity, then grown on its own: repair shop bookings, quotes and sizes per bike.
Shared admin
Shared admin · Every store gets the same panel for products, stock, orders and content. Here running locally on PGlite with seed data.
(05) What I'd do next
01A CLI command that lists the core commits a store is missing since it was generated, so fixes stop depending on cherry-picks from memory.
02Move 3:23 from mock data onto the core, with its database, orders and admin.
03Run new-store in CI on every Rodar change and check that the result passes type checks, lint and build.
Next project · 13 / 15
El Rey Jesús →Website and content admin for a church community
Case study · 12 / 15

Tiendas

One online store core and a CLI that turns it into a new client store, now running Rodar MDQ and BiciTienda MDQ.

Role
Architecture and full build
Stack
Next.js 16, TypeScript, Tailwind 4, Drizzle, Neon Postgres / PGlite, React Email, Go
Tiendas
(01) The problem

Small shops in Mar del Plata kept asking for the same thing: a catalog, a cart, a checkout, orders and an admin they can run themselves. The first one, 3:23, was a frontend on mock data. Building each new store from scratch would cost weeks, and copying the last one by hand would carry the last client's brand, copy and photos into the next.

My roleI split the stores into a shared core and a small per-store layer, and wrote the CLI that generates a new store from it.
(02) Results
2client stores in production on the same core
11files a new store replaces. Everything else is core
6integrations with a local stand-in, so a store runs without credentials
(03) Architecture

Three decisions that shaped it.

01

The template is generated from a live store

Why. Rodar MDQ is the reference store. The CLI exports its last commit with git archive, strips Rodar's photos, copy and Cloudinary URLs, lays a placeholder brand called Faro on top, and checks that every migration and db script arrived intact. The starter is never edited by hand, so it can't drift from code that runs in production.

Trade-off. A core fix has to land in Rodar first. A few Rodar leftovers in core files are swapped out by a list of string replacements, which only warns when a string goes missing.

02

Copy the core, don't share a package

Why. new-store copies the starter into its own git repo, with its own Neon database and Vercel project. Brand words, routes and modules live in config and a lexicon, so BiciTienda could add sizes and colors, appointments and customer accounts without touching Rodar.

Trade-off. A fix doesn't reach stores that already exist. Each one gets it by cherry-picking the commit by hand.

03

Every integration has a local stand-in

Why. Without env vars the store uses PGlite instead of Neon, writes emails to a local outbox, pays through a sandbox page and opens the admin with a dev PIN. A new store runs with migrate, seed and dev, and I can show it to a client before anyone creates an account anywhere.

Trade-off. Two code paths per integration. The sandbox payment page had to be locked out in production, and PGlite can hide a difference from Neon until deploy.

(04) Gallery
The starter, as Faro
The starter, as Faro · What new-store produces: the full core with a placeholder brand, demo products and two sample branches.
Rodar MDQ
Rodar MDQ · The reference store, for electric bikes and motorbikes. Same layout as the starter, with its own brand, routes and catalog.
BiciTienda MDQ
BiciTienda MDQ · Generated with the carbono identity, then grown on its own: repair shop bookings, quotes and sizes per bike.
Shared admin
Shared admin · Every store gets the same panel for products, stock, orders and content. Here running locally on PGlite with seed data.
(05) What I'd do next
01A CLI command that lists the core commits a store is missing since it was generated, so fixes stop depending on cherry-picks from memory.
02Move 3:23 from mock data onto the core, with its database, orders and admin.
03Run new-store in CI on every Rodar change and check that the result passes type checks, lint and build.
Next projectEl Rey Jesús →
Nahuel SantillánEN