Tvashtr β The divine craftsman of Hindu beliefs. A modular, zero-cost framework for building production-ready multi-tenant e-commerce storefronts.
Tvashtr Suite is a "Store-in-a-Box" monorepo framework. It decouples a generic, reusable e-commerce engine (@tvashtr/* packages) from individual store applications so you can spin up a fully configured, white-label storefront in seconds β not days.
Every store built on this suite runs at $0/month by leveraging free tiers:
| Service | Free Tier Limit |
|---|---|
| Cloudflare Pages | Unlimited sites, 500 builds/month |
| Cloudflare Workers | 100,000 requests/day |
| Cloudflare D1 | 5 GB storage, 25M row reads/day |
| Google Sheets | Unlimited (used as headless CMS) |
See it in production: patel-flour β a real organic flour store built entirely on this framework.
βββββββββββββββββββββββββββββββββββ
β Google Sheets CMS β
β (Products, Recipes, Blog posts)β
ββββββββββββββββββ¬βββββββββββββββββ
gviz API (real-time)
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β Nuxt 4 SPA β Cloudflare Pages β
β @tvashtr/ui Β· @tvashtr/core Β· store theme β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββββ
HTTPS POST (Turnstile-protected)
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β Cloudflare Worker (Edge API) β
β Rate limiting Β· Bot protection Β· Validation β
ββββββββββββ¬ββββββββββββββββββββ¬ββββββββββββββββββ
β β
ββββββββββββΌβββββββ ββββββββββΌβββββββββββββββ
β Cloudflare D1 β β Notifications β
β (Orders DB) β β Telegram + Brevo SMTPβ
βββββββββββββββββββ βββββββββββββββββββββββββ
tvashtr-suite/
βββ apps/
β βββ patel-flour/ β Store: brand, theme, locale overrides
β βββ my-store/ β Your new store (created by CLI)
β
βββ packages/
βββ @tvashtr/ui β Shared Vue components (NavBar, ProductCard, Cartβ¦)
βββ @tvashtr/checkout β Order/enquiry services, bot protection, notifiers
βββ @tvashtr/core β Types, utilities, config patterns
βββ @tvashtr/google-sheets β Google Sheets gviz API parser
βββ @tvashtr/notifications β Telegram + Brevo email builders
βββ @tvashtr/cli β Scaffolding tool (`npx tvashtr create`)
| Feature | Description |
|---|---|
| π§± Feature Flags | Toggle Checkout, Recipes, Blog, or Service Mode via .env β zero code changes |
| π¨ Dynamic Theming | Tailwind v4 @theme tokens + CSS custom properties for complete brand control |
| ποΈ No-Code CMS | Add/edit products in Google Sheets; changes appear in real-time |
| π i18n Ready | Built-in multi-language routing (English + Danish out of the box) |
| π€ Bot Protection | Cloudflare Turnstile + honeypot fields on all forms |
| π¦ Rate Limiting | 5 requests per 10 min per IP on all Worker endpoints |
| π± Instant Notifications | Telegram + Brevo email on every order and enquiry |
| π Persistent Cart | localStorage-backed with variant pricing |
| π³ Variant Pricing | Different prices per size/weight/flavour |
| 𧬠Product Attributes | Generic per-product data table driven by Google Sheets β Nutrition, Specs, Features, or any custom labels |
| π¦ Cash on Delivery | Full COD payment flow |
| π Zero Secrets in Repo | All sensitive keys stored as Cloudflare Worker secrets |
| π©π° Geo-restriction | Optionally hide checkout for visitors outside your target country |
| π Recipes Module | Google Sheets-driven recipes with "buy the ingredients" product links |
| π° Blog Module | Markdown-based blog via Google Docs / Sheets content |
| Layer | Technology | Purpose |
|---|---|---|
| Framework | Nuxt 4 (SPA mode) | Full-stack Vue framework |
| UI | Nuxt UI + TailwindCSS v4 | Component library + styling |
| i18n | @nuxtjs/i18n | Multi-language routing |
| CMS | Google Sheets (gviz API) | Products, recipes, blog content |
| Hosting | Cloudflare Pages | Static site + CI/CD |
| API | Cloudflare Workers | Serverless order/enquiry endpoints |
| Database | Cloudflare D1 | SQLite at the edge |
| Bot Protection | Cloudflare Turnstile | CAPTCHA alternative |
| Notifications | Telegram Bot API + Brevo SMTP | Real-time alerts |
- Node.js 20+
- Cloudflare account (free)
- Google account (for Google Sheets CMS)
git clone https://github.com/gunjanpatel/tvashtr-suite.git
cd tvashtr-suite
npm installnpx tvashtr create my-storeThe interactive wizard will ask you for your store name, colors, phone format, and which modules you want. It writes everything directly into your config files.
Create a Google Sheet (see Step 1 below), share it as Viewer, then add the ID to apps/my-store/.env:
NUXT_PUBLIC_SHEET_ID=your_sheet_id_herenpm run dev -w my-store
# β http://localhost:3000Your store is running! Now follow the full deployment guide below to go live.
Create a new Google Sheet with these exact headers in Row 1:
| Column | Type | Description | Example |
|---|---|---|---|
sku |
string | Unique product ID (no spaces) | organic-wheat-1kg |
name |
string | Display name | Organic Whole Wheat Flour |
categories |
string | Comma-separated list of category IDs | baking,whole-wheat |
price |
number | Default price | 45 |
image |
string | Image path from /public |
/images/wheat.webp |
short |
string | Short tagline | Stone-ground, locally sourced |
desc |
string | Full description (Markdown ok) | Rich in fibre... |
variants |
string | Comma-separated sizes | 1 kg, 5 kg, 10 kg |
variant_prices |
string | variant:price pairs |
1 kg:45, 5 kg:200 |
qtyDefault |
number | Default cart quantity | 1 |
is_popular |
boolean | Show on homepage | TRUE |
active |
boolean | Show in catalogue | TRUE |
Share settings: Google Sheets β Share β General Access β Anyone with the link β Viewer
Get your Sheet ID: Copy the long string from your sheet URL between /d/ and /edit:
https://docs.google.com/spreadsheets/d/THIS_IS_YOUR_SHEET_ID/edit
Add to apps/my-store/.env:
NUXT_PUBLIC_SHEET_ID=your_sheet_id_hereEnable with NUXT_PUBLIC_ENABLE_RECIPES=true. Create a second sheet with these columns:
| Column | Type | Description |
|---|---|---|
slug |
string | URL slug (e.g. flatbread-recipe) |
title |
string | Recipe title |
image |
string | Image path |
description |
string | Short intro |
category |
string | e.g. Baking, Breakfast |
prepTime |
string | e.g. 15 mins |
cookTime |
string | e.g. 30 mins |
ingredients |
string | Comma-separated list |
content |
string | Markdown cooking steps |
recipeProducts |
string | sku:variant:qty pairs for "buy ingredients" |
isPopular |
boolean | Show on homepage |
active |
boolean | TRUE or FALSE |
NUXT_PUBLIC_RECIPE_SHEET_ID=your_recipe_sheet_id
NUXT_PUBLIC_ENABLE_RECIPES=trueA generic per-product data table. Patel Flour uses it for Nutrition Information. A lighting store might use it for electrical specs. A service company for service inclusions. The column names become the attribute labels β fully dynamic.
Sheet layout:
sku |
Calories | Protein | Carbohydrates | Fat | Fibre |
|---|---|---|---|---|---|
| organic-wheat-flour | 340 kcal | 13 g | 62 g | 2 g | 11 g |
- Column A header must be
skuβ this is the lookup key matched against the product SKU - Columns B onwards β any label you want, they become the attribute names as-is
- Empty cells are automatically ignored β only populated values are shown
- SKUs with no row in this sheet simply show no section on their product page
- Add as many columns as needed β the UI adapts automatically
NUXT_PUBLIC_PRODUCT_ATTRIBUTES_SHEET_ID=your_sheet_id_hereCreate a separate Google Sheet tab or file to manage your dynamic categories and automatic navigation row splits.
Sheet layout β Row 1 must be headers, freeze it via View β Freeze β 1 row:
category |
name |
type |
description |
|---|---|---|---|
daily-staples |
Daily Staples | usage | Essential everyday flours used for making standard rotis chapatis and flatbreads |
gluten-free |
Gluten-Free | dietary | Naturally gluten-free flour options suitable for dietary restrictions |
- Column A (
category): The unique text slug used in your product data mapping (e.g.,daily-staples). - Column B (
name): The clean, customer-facing label displayed on the storefront filter pills. - Column C (
type): Must be eitherdietaryorusage. This value automatically assigns the category to its respective layout row without changing backend code. - Column D (
description): Optional context describing the target category grouping. - Share the sheet as Anyone with the link β Viewer
Add to .env:
NUXT_PUBLIC_CATEGORIES_SHEET_ID=your_categories_sheet_id
In your store's product page, use the useProductAttributes(sku) composable and the generic ProductAttributesTable component from @tvashtr/ui, or create a store-specific styled component (e.g. NutritionLabelA.vue) that accepts the same props:
<ProductAttributesTable
v-if="product"
title="Nutrition Information"
subtitle="Typical values per 100 g"
:attributes="nutritionAttributes"
/>const { attributes: nutritionAttributes } = useProductAttributes(sku)Open apps/my-store/store.config.ts (the CLI pre-fills this from your wizard answers):
export const siteName = 'My Organic Store'
export const siteTagline = 'Fresh & Natural β Delivered'
export const logoLabel = 'My'
export const logoText = 'Store'
export const brandConfig = {
name: 'My Organic Store',
color: '#0284c7', // Used in email templates
colorLight: '#f0f9ff',
colorAccent: '#0ea5e9',
}
export const checkoutPhoneConfig = {
prefix: '+1',
flag: 'πΊπΈ',
digits: 10,
hint: '10-digit US mobile number',
placeholder: '201-555-0123',
maxLength: 12,
}Define your color palette and light/dark CSS custom properties:
@import "tailwindcss";
@import "@nuxt/ui";
@plugin "@tailwindcss/typography";
/* Required: tell Tailwind to scan shared UI package for classes */
@source "../../../../packages/tvashtr-ui/app/**/*.vue";
@source "../../../../packages/tvashtr-ui/app/**/*.ts";
@theme {
--color-brand-50: #f0f9ff;
--color-brand-100: #e0f2fe;
--color-brand-500: #0284c7; /* β your primary brand color */
--color-brand-600: #0369a1;
--color-brand-900: #0c4a6e;
}
:root {
--bg-page: #fafafa;
--bg-surface: #ffffff;
--bg-muted: #f4f4f5;
--text-primary: #18181b;
--text-secondary: #71717a;
--border: #e4e4e7;
}
.dark {
--bg-page: #09090b;
--bg-surface: #18181b;
--bg-muted: #27272a;
--text-primary: #f4f4f5;
--text-secondary: #a1a1aa;
--border: #27272a;
}The Worker handles all order processing, bot protection, and notifications.
cd apps/my-store/cloudflare
# 1. Authenticate with Cloudflare
npx wrangler login
# 2. Create your D1 database
npx wrangler d1 create my-store-dbWrangler will output something like:
[[d1_databases]]
binding = "DB"
database_name = "my-store-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"Copy the database_id and paste it into cloudflare/wrangler.toml:
name = "my-store-worker"
[[d1_databases]]
binding = "DB"
database_name = "my-store-db"
database_id = "paste-your-id-here"
[vars]
ALLOWED_ORIGIN = "https://my-store.pages.dev"# 3. Apply the database schema (creates the orders table)
npx wrangler d1 execute my-store-db --remote --file=schema.sql
# 4. Deploy the worker
npx wrangler deployCopy the worker URL from the output (e.g. https://my-store-worker.yourname.workers.dev) and add to .env:
NUXT_PUBLIC_WORKER_URL=https://my-store-worker.yourname.workers.devThese are injected securely into the Worker at runtime β never commit them to .env.
cd apps/my-store/cloudflare
# Cloudflare Turnstile (bot protection)
npx wrangler secret put TURNSTILE_SECRET_KEY
# Telegram notifications
npx wrangler secret put TELEGRAM_TOKEN
npx wrangler secret put TELEGRAM_CHAT_ID
# Brevo transactional email
npx wrangler secret put BREVO_API_KEY
# Email sender details
npx wrangler secret put BREVO_SENDER_EMAIL
npx wrangler secret put BREVO_SENDER_NAME- Cloudflare Dashboard β Turnstile β Add Site
- Enter your domain (e.g.
mystore.com), choose Managed widget type - Copy the Site Key β add to
apps/my-store/.env:NUXT_PUBLIC_TURNSTILE_SITE_KEY=your_site_key_here
- Copy the Secret Key β add as a Worker secret (Step 5 above)
Every new order and enquiry sends an instant alert to your Telegram group.
- Message @BotFather on Telegram β send
/newbotβ follow prompts β copy your Bot Token - Create a Telegram group and add your bot as a member
- Send a message in the group, then call:
curl https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates
- Find your
chat.idin the response (group IDs are negative, e.g.-1001234567890) - Set both as Worker secrets (see Step 5)
- Cloudflare Dashboard β Pages β Create a project β Connect to Git
- Select your GitHub repository
- Configure the build settings:
| Setting | Value |
|---|---|
| Project name | my-store |
| Production branch | main |
| Framework preset | None |
| Build command | npm run build -w my-store |
| Build output directory | apps/my-store/.output/public |
| Root directory | / β keep as root for monorepo workspace resolution |
- Under Environment Variables, add all values from your
apps/my-store/.envfile - Click Save and Deploy β every
git pushtriggers an automatic edge build
- Cloudflare Dashboard β Pages β your project β Custom domains β Add domain
- Update your Worker to accept requests from the new origin:
# cloudflare/wrangler.toml [vars] ALLOWED_ORIGIN = "https://shop.mydomain.com" [[routes]] pattern = "api.mydomain.com/*" zone_name = "mydomain.com"
- Redeploy the Worker:
npx wrangler deploy
- Update
.env:NUXT_PUBLIC_WORKER_URL=https://api.mydomain.com
Toggle features by setting these in your apps/my-store/.env. No code changes needed.
| Variable | Default | Description |
|---|---|---|
NUXT_PUBLIC_ENABLE_CHECKOUT |
true |
Full cart + checkout flow |
NUXT_PUBLIC_ENABLE_SERVICE_MODE |
false |
When true, "Add to Cart" β "Send Enquiry" |
NUXT_PUBLIC_ENABLE_RECIPES |
false |
Recipes module + nav link |
NUXT_PUBLIC_ENABLE_BLOG |
false |
Blog module + nav link |
NUXT_PUBLIC_ENABLE_CART |
true |
Cart icon and drawer |
NUXT_PUBLIC_ALLOWED_COUNTRY |
DK |
ISO country code for geo-restriction (leave blank to disable) |
NUXT_PUBLIC_FREE_DELIVERY_THRESHOLD |
399 |
Order total for free delivery |
NUXT_PUBLIC_SHEET_ID |
(required) | Product catalogue Google Sheet ID |
NUXT_PUBLIC_DELIVERY_SHEET_ID |
β | Delivery options Google Sheet ID |
NUXT_PUBLIC_RECIPE_SHEET_ID |
β | Recipes Google Sheet ID |
NUXT_PUBLIC_CATEGORY_SHEET_ID |
β | Categories Google Sheet ID |
NUXT_PUBLIC_PRODUCT_ATTRIBUTES_SHEET_ID |
β | Optional per-product attributes sheet (Nutrition, Specs, Features, etc.) |
NUXT_PUBLIC_WORKER_URL |
MOCK |
Cloudflare Worker URL (MOCK = dev-only simulation) |
NUXT_PUBLIC_TURNSTILE_SITE_KEY |
β | Cloudflare Turnstile site key |
# 1. Install all workspace dependencies
npm install
# 2. Copy and fill in environment variables
cp apps/my-store/.env.example apps/my-store/.env
# Edit .env β minimum: set NUXT_PUBLIC_SHEET_ID
# 3. Start the dev server
npm run dev -w my-store
# β http://localhost:3000
# 4. (Optional) Run the Cloudflare Worker locally
cd apps/my-store/cloudflare
npx wrangler dev
# β http://localhost:8787Dev mode tip: Set
NUXT_PUBLIC_WORKER_URL=MOCKin your.envto simulate orders locally without a running Worker. Orders are saved toapps/my-store/.data/orders.db.
After running npx tvashtr create my-store, you get:
apps/my-store/
βββ .env β Pre-filled from your wizard answers
βββ .env.example β Committed template (no secrets)
βββ store.config.ts β Brand identity, phone config, nav links
βββ nuxt.config.ts β Nuxt 4 config (auto-extends @tvashtr/ui)
βββ tailwind.config.ts β Tailwind configuration
βββ app/
β βββ app.config.ts β Nuxt UI component defaults (primary colour)
β βββ assets/
β βββ themes/
β βββ default/
β βββ main.css β Your custom CSS theme (edit this!)
βββ cloudflare/
βββ worker.ts β Edge API (orders + enquiries + bot protection)
βββ schema.sql β D1 database schema
βββ wrangler.toml β Cloudflare Worker config
Use this before going live:
Google Sheets
[ ] Product sheet created with correct column headers
[ ] Sheet shared as "Anyone with the link β Viewer"
[ ] NUXT_PUBLIC_SHEET_ID added to .env and Cloudflare Pages env vars
Cloudflare Worker
[ ] wrangler login completed
[ ] D1 database created: npx wrangler d1 create <name>
[ ] database_id copied to wrangler.toml
[ ] schema.sql applied: npx wrangler d1 execute <name> --remote --file=schema.sql
[ ] npx wrangler deploy succeeded
[ ] NUXT_PUBLIC_WORKER_URL added to .env
Worker Secrets (via wrangler secret put)
[ ] TURNSTILE_SECRET_KEY
[ ] TELEGRAM_TOKEN
[ ] TELEGRAM_CHAT_ID
[ ] BREVO_API_KEY
[ ] BREVO_SENDER_EMAIL
[ ] BREVO_SENDER_NAME
Cloudflare Turnstile
[ ] Site created in Turnstile dashboard
[ ] NUXT_PUBLIC_TURNSTILE_SITE_KEY added to .env
Cloudflare Pages
[ ] Project connected to GitHub repo
[ ] Build command: npm run build -w my-store
[ ] Build output: apps/my-store/.output/public
[ ] All .env variables added to Pages environment variables
[ ] First deploy succeeded
Optional
[ ] Custom domain configured
[ ] wrangler.toml ALLOWED_ORIGIN updated to custom domain
[ ] .env.example committed (never commit .env itself)
Q: Products aren't loading β I just see an empty page.
Make sure NUXT_PUBLIC_SHEET_ID is set in .env and that your Google Sheet is shared as "Anyone with the link β Viewer".
Q: The checkout form submits but nothing happens.
Set NUXT_PUBLIC_WORKER_URL to your deployed Worker URL. MOCK only simulates orders locally.
Q: I get CORS errors when submitting the checkout.
Your Worker's ALLOWED_ORIGIN env var must match your frontend URL exactly (including https://). Update wrangler.toml and redeploy.
Q: I'm not receiving Telegram notifications.
Double-check your TELEGRAM_CHAT_ID. Group chat IDs are negative numbers. Run getUpdates again to confirm.
Q: How do I add a second language?
Add a locale file to apps/my-store/locales/ and register it in nuxt.config.ts under i18n.locales.
Q: Can I use this without the Recipes or Blog modules?
Yes β set NUXT_PUBLIC_ENABLE_RECIPES=false and NUXT_PUBLIC_ENABLE_BLOG=false in .env. Their nav links and pages are hidden automatically.
Q: Where do I put product images?
Add them to apps/my-store/public/images/ and reference them in Google Sheets as /images/yourimage.webp. Run npm run optimize-images to generate WebP versions automatically.
MIT β Built as a reference architecture for scalable, zero-cost e-commerce storefronts.