-
Notifications
You must be signed in to change notification settings - Fork 20
Open
Labels
enhancementNew feature or requestNew feature or request
Description
Overview
Add an e-commerce plugin that covers the full buy-side flow: product catalog, cart, checkout, and order management. v1 is intentionally scoped to the core primitives — no payment processing built-in, but a clean adapter interface so consumers plug in Stripe, Lemon Squeezy, or any provider.
The plugin ships:
- A product catalog backed by the CMS plugin (products are CMS content items, like the Job Board pattern)
- A cart (client-side state + optional server-side persistence)
- A checkout flow that hands off to the payment adapter and stores the resulting order
- An orders admin dashboard
Core Features
Product Catalog
- Product list page with filters (category, price range, in-stock)
- Product detail page (images via Media Library plugin, variants, description)
- Categories / collections
- Inventory tracking (in-stock count, out-of-stock state)
- Product variants (size, colour, etc.) with per-variant pricing + stock
Cart
- Add / remove / update quantity
- Persistent cart (localStorage + optional server-side session)
- Cart drawer / mini-cart component
- Discount codes (validated server-side)
Checkout
- Multi-step checkout: cart → shipping → payment → confirmation
- Shipping address form (validates with Zod)
- Payment adapter interface — hand off to Stripe, Lemon Squeezy, Paddle, etc.
- Order created in DB after payment success webhook
Orders
- Order list for admin (status, total, date, customer email)
- Order detail page (line items, shipping address, payment status)
- Order status lifecycle:
pending→paid→fulfilling→fulfilled→cancelled - Customer "my orders" page (if auth plugin present)
Schema
import { createDbPlugin } from "@btst/stack/plugins/api"
export const ecommerceSchema = createDbPlugin("ecommerce", {
product: {
modelName: "product",
fields: {
name: { type: "string", required: true },
slug: { type: "string", required: true },
description: { type: "string", required: false },
price: { type: "number", required: true }, // cents
currency: { type: "string", defaultValue: "USD" },
stock: { type: "number", defaultValue: 0 },
images: { type: "string", required: false }, // JSON array of URLs
category: { type: "string", required: false },
tags: { type: "string", required: false }, // JSON array
status: { type: "string", defaultValue: "draft" }, // "draft" | "published" | "archived"
createdAt: { type: "date", defaultValue: () => new Date() },
updatedAt: { type: "date", defaultValue: () => new Date() },
},
},
productVariant: {
modelName: "productVariant",
fields: {
productId: { type: "string", required: true },
name: { type: "string", required: true }, // e.g. "Blue / XL"
options: { type: "string", required: true }, // JSON: { color: "blue", size: "XL" }
price: { type: "number", required: false }, // override product price
stock: { type: "number", defaultValue: 0 },
sku: { type: "string", required: false },
},
},
order: {
modelName: "order",
fields: {
email: { type: "string", required: true },
status: { type: "string", defaultValue: "pending" },
lineItems: { type: "string", required: true }, // JSON: [{ productId, variantId, qty, price }]
subtotal: { type: "number", required: true }, // cents
total: { type: "number", required: true }, // cents
currency: { type: "string", defaultValue: "USD" },
shippingAddress: { type: "string", required: false }, // JSON
paymentProvider: { type: "string", required: false },
paymentRef: { type: "string", required: false }, // Stripe charge ID, etc.
createdAt: { type: "date", defaultValue: () => new Date() },
updatedAt: { type: "date", defaultValue: () => new Date() },
},
},
discountCode: {
modelName: "discountCode",
fields: {
code: { type: "string", required: true },
type: { type: "string", required: true }, // "percent" | "fixed"
value: { type: "number", required: true },
maxUses: { type: "number", required: false },
uses: { type: "number", defaultValue: 0 },
expiresAt: { type: "date", required: false },
},
},
})
Plugin Structure
src/plugins/ecommerce/
├── db.ts
├── types.ts
├── schemas.ts
├── query-keys.ts
├── client.css
├── style.css
├── api/
│ ├── plugin.ts # defineBackendPlugin — catalog, cart, checkout, order endpoints
│ ├── getters.ts # listProducts, getProduct, listOrders, getOrder
│ ├── mutations.ts # createOrder, updateOrderStatus, validateDiscountCode
│ ├── query-key-defs.ts
│ ├── serializers.ts
│ ├── payment-adapter.ts # PaymentAdapter interface
│ ├── adapters/
│ │ ├── stripe.ts # StripePaymentAdapter
│ │ └── lemon-squeezy.ts # LemonSqueezyPaymentAdapter
│ └── index.ts
└── client/
├── plugin.tsx # defineClientPlugin — all storefront + admin routes
├── overrides.ts # EcommercePluginOverrides
├── index.ts
├── hooks/
│ ├── use-cart.tsx # useCart — client-side cart state
│ ├── use-products.tsx # useProducts, useProduct
│ ├── use-orders.tsx # useOrders, useOrder
│ └── index.tsx
└── components/
├── cart-drawer.tsx # Floating cart mini-drawer
└── pages/
├── product-list-page.tsx / .internal.tsx
├── product-detail-page.tsx / .internal.tsx
├── checkout-page.tsx / .internal.tsx
├── order-confirmation-page.tsx / .internal.tsx
├── orders-admin-page.tsx / .internal.tsx
└── order-detail-page.tsx / .internal.tsx
Routes
| Route | Path | Description |
|---|---|---|
shop |
/shop |
Product catalog with filters |
product |
/shop/:slug |
Product detail + add to cart |
cart |
/shop/cart |
Cart review |
checkout |
/shop/checkout |
Multi-step checkout flow |
orderConfirmation |
/shop/orders/:id/confirmation |
Post-purchase confirmation |
orders |
/shop/admin/orders |
Order management (admin) |
orderDetail |
/shop/admin/orders/:id |
Order detail (admin) |
Payment Adapter Interface
export interface PaymentAdapter {
/** Create a payment intent / session and return a client secret or redirect URL */
createPayment(options: {
amount: number // cents
currency: string
orderId: string
metadata?: Record<string, string>
}): Promise<{ clientSecret?: string; redirectUrl?: string }>
/** Verify a webhook payload and return the order ID + new status */
handleWebhook(request: Request): Promise<{
orderId: string
status: "paid" | "failed" | "refunded"
paymentRef: string
} | null>
}
// Built-in adapters:
export function stripeAdapter(secretKey: string, webhookSecret: string): PaymentAdapter
export function lemonSqueezyAdapter(apiKey: string, webhookSecret: string): PaymentAdapter
SSG Support
// prefetchForRoute route keys
export type EcommerceRouteKey = "shop" | "product"
// "orders" / checkout / cart are dynamic — skipped
// SSG page.tsx pattern
await myStack.api.ecommerce.prefetchForRoute("shop", queryClient)
await myStack.api.ecommerce.prefetchForRoute("product", queryClient, { slug: "my-product" })
| Route key | SSG? | Notes |
|---|---|---|
shop |
✅ | Product list |
product |
✅ | Per-product static pages |
cart / checkout / orders |
❌ | Dynamic, user-specific |
Consumer Setup
// lib/stack.ts
import { ecommerceBackendPlugin } from "@btst/stack/plugins/ecommerce/api"
import { stripeAdapter } from "@btst/stack/plugins/ecommerce/api"
ecommerce: ecommerceBackendPlugin({
paymentAdapter: stripeAdapter(
process.env.STRIPE_SECRET_KEY!,
process.env.STRIPE_WEBHOOK_SECRET!,
),
})
// lib/stack-client.tsx
import { ecommerceClientPlugin } from "@btst/stack/plugins/ecommerce/client"
ecommerce: ecommerceClientPlugin({
apiBaseURL: "",
apiBasePath: "/api/data",
siteBaseURL: "https://example.com",
siteBasePath: "/pages",
queryClient,
currency: "USD",
})
Non-Goals (v1)
- Subscription / recurring billing
- Multi-currency storefront
- Tax calculation (expose totals in cents; tax via payment provider)
- Shipping rate APIs (flat rate or free shipping only in v1)
- Digital product delivery
- Refund workflows (update order status manually)
- Multi-vendor / marketplace
Plugin Configuration Options
| Option | Type | Description |
|---|---|---|
paymentAdapter |
PaymentAdapter |
Stripe, Lemon Squeezy, or custom |
currency |
string |
Default store currency (default: "USD") |
hooks |
EcommercePluginHooks |
onBeforeCreateOrder, onAfterOrderPaid, etc. |
Documentation
Add docs/content/docs/plugins/ecommerce.mdx covering:
- Overview — catalog + cart + checkout; v1 scope
- Setup —
ecommerceBackendPluginwith payment adapter,ecommerceClientPlugin - Payment adapters — Stripe and Lemon Squeezy examples; custom adapter interface
- Webhook endpoint — how to register the webhook route for payment callbacks
- SSG —
prefetchForRouteroute key table + Next.jspage.tsxexamples for catalog + product detail useCarthook — cart state management reference- Schema reference —
AutoTypeTablefor all config + hooks - Routes — table of route keys, paths, descriptions
Related Issues
- Media Library Plugin #77 Media Library Plugin (product images)
- Newsletter / Marketing Emails Plugin #75 Newsletter / Marketing Emails Plugin (order confirmation emails)
- CRM Plugin #76 CRM Plugin (customer contact records)
- Job Board Plugin #58 Job Board Plugin
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request