Complexity often creeps in unnecessarily in eCommerce.
Let’s explore a straightforward approach to building multiple storefronts using a single codebase while maintaining separation of concerns and scalability.
Instead of sharing a database with complex tenant isolation, we instead share a single codebase with the store’s that is powered by themes, and configuration via environment variables.
This architecture provides several advantages:
This architecture is far simpler to work with locally too. One repository is now responsible for many stores, including different themes.
Let’s assume we have a frontend with the following structure:
/template-repo
├── src/
│ ├── components/
│ ├── app/
│ └── lib/
│ └── themes/
├── .env.example
└── package.json
The storefront works differently depending on the environment variables:
STORE_NAME="My Store"
STORE_THEME="default"
DATABASE_URL="libsql://..."
DATABASE_AUTH_TOKEN="..."
STRIPE_SECRET_KEY="..."
These environment variables alter the appearance of the store (with theme and name), the products it sells (with a separate database), and route payments to the right team (with dedicated Stripe accounts).
Deployments are super easy with this architecture.
For new tenants:
For existing tenants:
Each store gets its own database instance, containing tables for:
This approach for vendors means:
New databases can be created from an existing "seed database", or if you prefer a managed solution — Turso Multi-DB Schemas will automatically propagate changes to all databases when the parent schema is updated.
const database = await turso.databases.create("store-1", {
group: "default",
seed: {
type: “database”,
name: “template-db”
}
});
Connecting to each store database is now easier. Name your database after the Vercel Project’s production URL, and it’s made available via an environment variable:
import { createClient } from '@libsql/client';
import slugify from 'slugify';
const storeName = slugify(process.env.VERCEL_PROJECT_PRODUCTION_URL);
export const turso = createClient({
url: `libsql://${storeName}-org-name.turso.io`,
authToken: process.env.TURSO_GROUP_AUTH_TOKEN,
});
Using something like Drizzle is also a lot simpler. No need for WHERE tenant = ?
!
await db.query.products.findMany();
await db.insert(schema.carts).values({
items: [
{ product_id: 1, quantity: 2 },
{ product_id: 2, quantity: 1 },
],
});
await db
.update(schema.orders)
.set({ shipped: true })
.where(eq(schema.orders.id, id));
Learn more about Drizzle & Turso.
Most modern deployment platforms provide immutable deployments for each pull request, or commit. Since each store has its own Vercel project that is connected to the repository, Vercel will automatically create a new preview or production deployment every time code is changed.
The approach of creating isolated environments per store may traditionally be seen as an expensive route, but measuring costs on a per-store basis is now a lot simpler than before.
This simple multi-tenant e-commerce architecture offers a pragmatic approach to building and scaling tenant based storefronts. By using a single codebase with customizable themes and configuration, coupled with isolated databases and deployments, we achieve a balance of simplicity and scalability.
While this approach may not suit every use case, it provides a starting point for many platforms looking to build a multi-tenant architecture with low complexity.
A common question is how to aggregate data across all stores to see who are your most active and valuable customers — you can learn how I do that in this article.