Changes: - Add FAL_KEY and GEMINI_API_KEY to .env.example - Update picture-it to use ~/.config/opencode/.env (unified creds) - Remove shodh-memory skill (no longer used) - Remove alphaear-* skills (deprecated) - Remove thai-frontend-dev skill (replaced by website-creator) - Remove theme-factory skill - Add mql-developer skill (MQL5 trading) - Add ecommerce-astro skill (Astro e-commerce) - Add website-creator skill (Next.js + Payload CMS) - Update install script for new skills
17 KiB
name, description
| name | description |
|---|---|
| ecommerce-astro | Full-featured e-commerce site builder with Astro 6, React, Supabase backend. Creates online stores with optional multi-vendor marketplace, Thai language support, inventory tracking, and order management. Use when: building e-commerce sites, marketplaces, online stores, or Thai e-commerce stores. |
E-commerce Astro - E-commerce Site Builder
Category: fullstack
Tech Stack: Astro 6 + React + Supabase + Tailwind v4
🎯 Purpose
Create complete e-commerce websites with these core features:
- ✅ Product catalog - Browse, filter, search products from Supabase
- ✅ Inventory management - Stock tracking with low-stock alerts
- ✅ Order management - Cart, checkout, order tracking, status updates
- ✅ Thai language support - Bilingual Thai/English with i18n routing
- ✅ Review system - Verified purchase reviews with ratings
- ✅ Responsive design - Mobile-first with React components
Optional Features (Enable/Disable)
| Feature | Default | Description |
|---|---|---|
multi_vendor |
false |
Multi-vendor marketplace with vendor dashboards |
payso_payment |
false |
PaySo Thai payment gateway (stub for now) |
vendor_payouts |
false |
Automated payout tracking (requires multi_vendor) |
🚀 Quick Start
# Generate e-commerce site (interactive mode)
python3 skills/ecommerce-astro/scripts/create_ecommerce.py \
--name "My Store" \
--output "./my-store"
# With options
python3 skills/ecommerce-astro/scripts/create_ecommerce.py \
--name "My Store" \
--output "./my-store" \
--multi-vendor true \
--languages "th"
📋 Pre-Flight Questions
Before running the script, gather these details:
- Store Name: (e.g., "Deal Plus Tech Store")
- Store Slug: (e.g., "deal-plus-tech-store")
- Supabase Project URL: From supabase.com dashboard
- Supabase Anon Key: Public key for client-side
- Supabase Service Role Key: For admin/server-side operations
- Multi-Vendor Mode: Enable/disable vendor system (true/false)
- Languages: Thai only (th), English only (en), or bilingual (th,en)
📁 Generated Project Structure (Base)
store-name/
├── astro.config.mjs
├── package.json
├── Dockerfile
├── docker-compose.yml
├── .env.example
├── .gitignore
│
├── supabase/
│ └── migrations/
│ └── 001_initial_schema.sql
│
├── src/
│ ├── components/
│ │ ├── cart/
│ │ │ ├── CartBadge.tsx # Floating cart button
│ │ │ ├── CartButton.tsx # Header cart icon
│ │ │ ├── CartDrawer.tsx # Slide-out cart panel
│ │ │ ├── CartItems.tsx # Cart item list
│ │ │ └── CartSummary.tsx # Price breakdown
│ │ ├── checkout/
│ │ │ └── CheckoutForm.tsx # Checkout form
│ │ ├── product/
│ │ │ ├── ProductCard.astro # Product grid card
│ │ │ ├── ProductFilters.tsx # Category/price filters
│ │ │ ├── ProductGallery.tsx # Image gallery
│ │ │ ├── ProductVariants.tsx # Size/color variants
│ │ │ └── StockBadge.tsx # Inventory status
│ │ ├── review/
│ │ │ ├── ReviewList.tsx # Product reviews
│ │ │ └── StarRating.tsx # Star rating display
│ │ └── layout/
│ │ ├── Header.astro # Site header
│ │ └── Footer.astro # Site footer
│ │
│ ├── layouts/
│ │ └── Layout.astro # Base layout
│ │
│ ├── lib/
│ │ ├── supabase.ts # Supabase client (SSR-safe)
│ │ ├── auth.ts # JWT auth helpers
│ │ ├── utils.ts # Utility functions
│ │ └── types.ts # TypeScript types
│ │
│ ├── stores/
│ │ ├── cart.ts # Zustand cart (SSR-safe)
│ │ ├── auth.ts # Auth state
│ │ └── vendor.ts # Vendor state (if multi_vendor)
│ │
│ ├── pages/
│ │ ├── index.astro # Homepage
│ │ ├── products/
│ │ │ ├── index.astro # Product listing
│ │ │ └── [slug].astro # Product detail
│ │ ├── cart.astro # Full cart page
│ │ ├── checkout.astro # Checkout page
│ │ ├── search.astro # Search page
│ │ ├── auth/
│ │ │ ├── login.astro # Login page
│ │ │ └── register.astro # Register page
│ │ ├── account/
│ │ │ ├── index.astro # Account dashboard
│ │ │ └── orders/
│ │ │ ├── index.astro # Order history
│ │ │ └── [id].astro # Order detail
│ │ ├── vendor/ # Only if multi_vendor=true
│ │ │ ├── dashboard.astro # Vendor dashboard
│ │ │ ├── products/
│ │ │ ├── orders.astro
│ │ │ └── settings.astro
│ │ ├── admin/ # Only if multi_vendor=true
│ │ │ ├── dashboard.astro
│ │ │ ├── vendors.astro
│ │ │ ├── users.astro
│ │ │ ├── orders.astro
│ │ │ └── categories.astro
│ │ └── api/
│ │ ├── auth/
│ │ ├── products/
│ │ ├── orders/
│ │ └── payments/
│ │
│ ├── i18n/
│ │ ├── th.json # Thai translations
│ │ └── en.json # English translations
│ │
│ └── styles/
│ └── global.css # Global styles
│
└── public/
└── images/
🗄️ Database Schema (Supabase PostgreSQL)
The migration creates these tables. Schema can be customized per project.
Core Tables (Always Included)
| Table | Purpose |
|---|---|
users |
Customer/admin accounts (id, email, password_hash, name, role, avatar_url) |
categories |
Product categories (hierarchical with parent_id) |
products |
Product catalog (id, vendor_id, category_id, name, slug, description, price, images JSONB, inventory, status, track_inventory, featured) |
reviews |
Product reviews (product_id, user_id, rating, comment, status) |
orders |
Customer orders (id, order_number, user_id, status, payment_status, total, shipping_address JSONB) |
order_items |
Line items per order (order_id, product_id, quantity, unit_price) |
Multi-Vendor Tables (Only if multi_vendor=true)
| Table | Purpose |
|---|---|
vendor_profiles |
Store info (user_id, store_name, store_slug, store_description, status) |
product_variants |
Size/color variants (product_id, name, sku, price, inventory) |
Indexes
-- Core indexes
CREATE INDEX idx_products_category ON products(category_id);
CREATE INDEX idx_products_vendor ON products(vendor_id);
CREATE INDEX idx_products_slug ON products(slug);
CREATE INDEX idx_products_status ON products(status);
CREATE INDEX idx_orders_user ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_reviews_product ON reviews(product_id);
Row Level Security (RLS)
-- Products: Public read, vendor write own
-- Orders: User read own, vendor read own orders
-- Vendors: Admin manages vendor_profiles
Key Lessons Learned
- Images stored as JSONB - Parse with
typeof images === 'string' ? JSON.parse(images) : images - Use service_role key for SSR - Anonymous key blocked by RLS during server-side rendering
- Format:
Authorization: Bearer {key}header for Supabase REST API
💳 Payment Integration
Payment is stubbed by default. To enable real payments:
- Add PaySo credentials to
.env - Create
/api/payments/create.tsendpoint - Create
/api/webhooks/payso.tshandler - Update
checkout.astroto call payment API
Stub Implementation (Default)
// lib/payso.ts (stub)
export async function createPayment(order: Order) {
// TODO: Implement PaySo integration
console.log('Payment stub for order:', order.id);
return { success: true, paymentUrl: '/checkout/success' };
}
🔐 Authentication
User Roles
| Role | Permissions |
|---|---|
customer |
Browse, cart, checkout, orders |
vendor |
Products, orders (requires multi_vendor=true) |
admin |
All management (requires multi_vendor=true) |
Auth Flow
- Register with email/password
- Login → JWT token stored in httpOnly cookie
- Protected routes check session cookie
- Role-based access for vendor/admin pages
SSR Authentication
// Check auth in Astro pages
const token = Astro.cookies.get('session')?.value;
if (!token) return Astro.redirect('/login');
🛒 Cart & Checkout
Cart Features
- Floating Cart Button - Fixed position bottom-right, blue circular button
- Cart Drawer - Slide-out panel with HeadlessUI
- Persistent - Zustand with localStorage (SSR-safe with getStorage)
- Guest cart - localStorage only
- Logged-in cart - Synced to database (optional)
Cart Store (SSR-Safe)
// stores/cart.ts
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({ ... }),
{
name: 'cart-storage',
partialize: (state) => ({ items: state.items }),
getStorage: () => {
if (typeof window === 'undefined') {
return { getItem: () => null, setItem: () => {}, removeItem: () => {} };
}
return localStorage;
},
}
)
);
Checkout Flow
- Cart Review → 2. Shipping Info → 3. Payment → 4. Confirmation
📦 Vendor Dashboard (Only if multi_vendor=true)
When multi_vendor=true, these pages are generated:
Vendor Features
- Dashboard - Stats (products, orders, sales)
- Products - Add/edit/archive products
- Orders - View and manage orders
- Settings - Store profile
Vendor Onboarding Flow
- Register as customer
- Apply for vendor status (
/vendors/apply) - Admin approves → Vendor profile created
- Access
/vendor/dashboard
Hiding Vendor Pages
When multi_vendor=false:
- No vendor registration link
- No
/vendor/*routes - No admin vendor management pages
- Products belong to "store" (no vendor_id)
🌐 Internationalization
Thai/English Support
- URL Structure:
/th/products,/en/products(if bilingual) - Fallback: Missing translation → English
- Default: Thai-only if
--languages th
Translation Keys
// i18n/th.json
{
"common": {
"addToCart": "เพิ่มลงตะกร้า",
"checkout": "ชำระเงิน",
"login": "เข้าสู่ระบบ"
},
"product": {
"outOfStock": "สินค้าหมด",
"inStock": "มีสินค้า"
}
}
🔧 Environment Variables
# Supabase (Required)
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_ANON_KEY=eyJxxx # Public key (client-side)
SUPABASE_SERVICE_ROLE_KEY=eyJxxx # Admin key (server-side only!)
# JWT (Required for auth)
JWT_SECRET=your-super-secret-jwt-key-min-32-chars
# Site
SITE_URL=https://yourdomain.com
SITE_NAME=My Store
# PaySo (Optional - stub by default)
PAYSOLO_MERCHANT_ID=your-merchant-id
PAYSOLO_API_KEY=your-api-key
PAYSOLO_SECRET_KEY=your-secret-key
PAYSOLO_CALLBACK_URL=https://yourdomain.com/api/webhooks/payso
🐳 Docker Deployment
Dockerfile (Astro SSR Mode)
FROM node:20-alpine
WORKDIR /app
# Build-time env vars (needed for npm run build)
ENV PUBLIC_SUPABASE_URL=https://xxx.supabase.co
ENV PUBLIC_SUPABASE_ANON_KEY=eyJxxx
ENV SUPABASE_SERVICE_ROLE_KEY=eyJxxx
ENV SITE_URL=https://yourdomain.com
ENV JWT_SECRET=your-32-char-min-secret-key
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 4321
ENV HOST=0.0.0.0
ENV PORT=4321
CMD ["npm", "run", "start"]
docker-compose.yml
services:
web:
build: .
ports:
- "4321:4321"
env_file:
- .env
Key Points
- Build-time env vars - Set
ENVbeforenpm run buildso Astro can access them - Service role key - Only in Dockerfile ENV, not in client code
- Port 4321 - Astro default, map to 80 or your preferred port
🚀 Deployment to Easypanel
# 1. Generate site locally
python3 skills/ecommerce-astro/scripts/create_ecommerce.py \
--name "my-store" \
--output "./my-store"
# 2. Push to Gitea
cd my-store
git init
git add .
git commit -m "Initial e-commerce site"
git remote add origin https://git.moreminimore.com/user/my-store.git
git push -u origin main
# 3. Deploy to Easypanel
# Use easypanel-deploy skill or dashboard
✅ Success Criteria
- Astro dev server runs without errors
- Supabase tables created successfully
- Products display from Supabase (images as JSONB)
- Cart adds/removes items (SSR-safe Zustand)
- Checkout creates order
- User registration/login works
- (If multi_vendor=true) Vendor dashboard accessible
- (If bilingual) Language switching works
- Docker build succeeds
- Deploys to Easypanel
📚 Dependencies
{
"astro": "^6.1.4",
"@astrojs/react": "^4.2.0",
"@astrojs/node": "^9.1.0",
"@astrojs/sitemap": "^3.2.0",
"@supabase/supabase-js": "^2.47.0",
"@supabase/ssr": "^0.6.1",
"@tailwindcss/vite": "^4.2.1",
"tailwindcss": "^4.2.1",
"zustand": "^5.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"jose": "^6.0.0",
"@headlessui/react": "^2.0.0",
"lucide-react": "^0.400.0"
}
Note: Astro 6 is required for CSRF protection and other security features.
🔗 Related Skills
- thai-frontend-dev - Base Astro setup with PDPA compliance, cookie consent
- easypanel-deploy - Deploy to Easypanel
- gitea-sync - Sync code to Gitea
📝 Example Usage
Single Vendor Store (Thai Only)
python3 skills/ecommerce-astro/scripts/create_ecommerce.py \
--name "ร้านค้าออนไลน์" \
--slug "online-store" \
--output "./thai-store"
# multi_vendor=false by default
Multi-Vendor Marketplace (Bilingual)
python3 skills/ecommerce-astro/scripts/create_ecommerce.py \
--name "ThaiMart" \
--slug "thaimart" \
--multi-vendor true \
--languages "th,en" \
--output "./thaimart"
Note: After generation:
- Run the Supabase migration in your dashboard
- Update
.envwith your Supabase credentials - Add sample products to test
🔧 Troubleshooting
SSR Error: Cannot read properties of undefined (reading 'value')
Cause: Zustand persist middleware tries to access localStorage during SSR.
Fix: Add getStorage to handle server-side:
getStorage: () => {
if (typeof window === 'undefined') {
return { getItem: () => null, setItem: () => {}, removeItem: () => {} };
}
return localStorage;
}
RLS Policy Blocks Read
Cause: Anon key doesn't bypass RLS during SSR.
Fix: Use service role key for server-side fetches:
headers: {
'apikey': import.meta.env.SUPABASE_SERVICE_ROLE_KEY,
'Authorization': `Bearer ${import.meta.env.SUPABASE_SERVICE_ROLE_KEY}`
}
Images Show as [ or Empty
Cause: Images stored as JSONB string, not array.
Fix: Parse before use:
const images = typeof product.images === 'string'
? JSON.parse(product.images || '[]')
: (product.images || []);
URLSearchParams Error
Cause: Spread operator with undefined in template literal.
Fix: Use string concatenation instead:
// Bad
href={`/products?${new URLSearchParams({...category && {category}})}`}
// Good
href={`/products?sort=${sort}`}
Cross-site POST form submissions are forbidden
Cause: Astro 6 has built-in CSRF protection that blocks native form POST from different origins.
Fix: Use client-side fetch instead of native form submission:
// In your .astro page
<form id="registerForm">
<input name="email" id="emailInput" />
<input name="password" id="passwordInput" />
<button type="submit">Register</button>
</form>
<script>
document.getElementById('registerForm').addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('emailInput').value;
const password = document.getElementById('passwordInput').value;
const res = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (res.ok) {
window.location.href = '/';
}
});
</script>