How to customize this starter for your store. The storefront, theming, catalog, and i18n are all driven by a few config files. The commerce engine (Throttle) and auth (Clerk) are pre-wired but swappable.
For the architecture of the Throttle + Clerk integration (cart sync, checkout, webhooks, customer mirror, security), see the How the Throttle integration works and Authentication sections of the README.
- Copy
.env.local.example→.env.localand fill in your Throttle + Clerk keys (see Environment Variables) - Update
src/lib/config.tswith your store name, contact info, and social links - Update product data in
src/data/products.json(images fall back topublic/images/products/placeholder.png) - Edit theme colors in
src/app/globals.css - Configure navigation in
src/lib/navigation.ts
All store-wide settings live in one file: src/lib/config.ts
export const siteConfig = {
name: "Your Store Name",
tagline: "Your tagline here.",
contact: { email: "you@yourstore.com", ... },
social: { instagram: "https://instagram.com/yourstore", ... },
freeShippingThreshold: 7500, // $75.00 in cents
taxRate: 0.08,
currency: "USD",
locale: "en-US",
}Referenced by the header, footer, metadata, cart summary, trust signals, and more. Change it once, it updates everywhere.
Note:
freeShippingThreshold/taxRatehere are display estimates used before checkout. At checkout, real shipping rates and tax come from Throttle'sshippingTax.calculateCart(when a shipping/tax provider is connected to your Throttle store).
Edit CSS variables in src/app/globals.css under :root:
/* Base colors (shadcn/ui) */
--primary: oklch(0.205 0 0); /* buttons, active states */
--secondary: oklch(0.97 0 0); /* secondary buttons */
--destructive: oklch(0.577 0.245 27.325); /* errors, delete */
/* Store accent colors */
--rating: oklch(0.795 0.184 86.047); /* star rating color */
--wishlist: oklch(0.637 0.237 25.331); /* wishlist heart color */
--success: oklch(0.627 0.194 149.214); /* checkmarks, success */
/* Order status badge colors */
--status-pending-bg: ...
--status-processing-bg: ...Use them in Tailwind: text-rating, fill-wishlist, bg-status-delivered-bg, etc.
The store uses Inter via next/font. To change it, edit src/app/layout.tsx:
import { Poppins } from "next/font/google"
const font = Poppins({ subsets: ["latin"], weight: ["400", "500", "600", "700"] })And update --font-sans in globals.css.
All menus (desktop nav, mobile menu) are configured in src/lib/navigation.ts:
export const shopLinks = [
{ name: "Electronics", href: "/electronics" },
// add/remove categories here
]
export const mobileMenuSections = [
{ label: "Shop", items: shopLinks },
{ label: "Account", items: accountLinks },
{ label: "Info", items: infoLinks },
]Product data lives in src/data/products.json. To add a product:
- Add the product object to the
productsarray - Reference existing
categoryIdsor create new categories - Assign a
brandIdfrom thebrandsarray - Set image URLs. Missing/broken images fall back to
public/images/products/placeholder.pngautomatically (seeProductImageinsrc/components/ui/product-image.tsx)
Image URLs that get sent to Throttle (cart line items, orders) must be absolute
http(s)URLs — Throttle rejects relative paths. The cart sync route drops relative image URLs before sending; supply absolute CDN URLs inproducts.jsonif you want product images to appear on Throttle-side line items.
Note: this starter is bring-your-own-catalog — Throttle is the commerce engine (cart, checkout, orders, customers, subscriptions), but your product catalog lives in products.json (or whatever you swap in). To move the catalog to a CMS or database, implement the ProductRepository / CategoryRepository interfaces from src/types/index.ts and update src/lib/repositories/index.ts.
Blog posts and CMS pages are plain markdown files — no CMS required:
content/blog/<slug>.md→/blog/<slug>content/pages/<slug>.md→/pages/<slug>
The filename is the URL slug; YAML frontmatter holds the metadata; the markdown body renders via react-markdown (GitHub-flavored markdown, XSS-safe — raw HTML in the source is not rendered).
---
id: "post-7"
title: "My New Post"
excerpt: "A short summary for the listing page."
author: "The Team"
tags: ["guides"]
publishedAt: "2026-01-01T10:00:00Z"
---
## A heading
Body content in **markdown**. Internal [links](/contact) work too.Add a post or page by dropping a new .md file in the folder — no code changes. Frontmatter fields map to the BlogPost / CmsPage types in src/types/index.ts. The loader is src/lib/content/markdown.ts; the repositories (markdown-blog-repository.ts, markdown-page-repository.ts) read these folders and are swappable like the product repository.
Throttle is pre-wired as the commerce engine — cart sync, checkout sessions, the PaymentEmbed, orders, customers, addresses, payment methods, discounts, shipping/tax, and subscriptions. The wiring lives in src/lib/throttle/* and src/app/api/throttle/*.
To use it, set THROTTLE_API_KEY + THROTTLE_STORE_ID (see Environment Variables) and allow your origin for the embed:
curl -X PUT https://api.usethrottle.dev/api/v1/embed-config \
-H "x-api-key: $THROTTLE_API_KEY" \
-H "content-type: application/json" \
-d '{"allowed_origins":["http://localhost:3000","https://your-domain.com"]}'Without THROTTLE_API_KEY + THROTTLE_STORE_ID, the starter falls back to a stub checkout provider so the UI keeps rendering — but no real cart/order/payment is created.
To swap engines (e.g. Stripe), the checkout layer is behind a pluggable interface:
interface CheckoutProvider {
createSession(cart, customer?): Promise<CheckoutSession>
getSession(sessionId): Promise<CheckoutSession>
handleWebhook(payload, signature): Promise<WebhookResult>
}- Create
src/lib/checkout/your-provider.tsimplementingCheckoutProvider - Re-point the export in
src/lib/checkout/index.ts - Add a webhook route under
src/app/api/
Clerk is the default auth provider, behind a pluggable AuthProvider seam (src/lib/auth/*). On a buyer's first authenticated request, the Clerk user is mirrored to a Throttle customer (the Clerk user id is stored as the customer's externalId), so orders, addresses, and subscriptions scope to that customer.
To use it, set CLERK_SECRET_KEY + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY. Without them, the starter falls back to a localStorage-backed mock auth so the UI still renders, and the order-read routes return 501 (they refuse to identify a buyer from request input alone).
To swap providers (NextAuth, Lucia, magic-link, etc.):
- Implement the one-method
AuthProviderinterface insrc/lib/auth/types.ts - Re-point the export in
src/lib/auth/index.ts - Replace the auth route pages in
src/app/(store)/auth/*with your provider's UI
The starter uses next-intl with translation files in messages/:
messages/en.json— English (default)messages/es.json— Spanish (example)
// Client components
import { useTranslations } from "next-intl"
const t = useTranslations("cart")
// Server components
import { getTranslations } from "next-intl/server"
const t = await getTranslations("shop")To add a language: copy messages/en.json to messages/<locale>.json, translate, and add the locale to src/i18n/config.ts. For locale routing (/en/, /fr/), add next-intl proxy handling in src/proxy.ts.
Edit src/lib/analytics.ts to connect your analytics provider:
export function trackEvent(name, properties) {
// Replace with your provider:
window.gtag?.("event", name, properties)
// or: window.analytics?.track(name, properties)
// or: posthog?.capture(name, properties)
}Pre-defined ecommerce events include addToCart, purchase, search, viewProduct, signUp, login.
Security headers + CSP are configured in src/proxy.ts (Next 16's replacement for middleware.ts), composed with Clerk's middleware. The CSP runs in report-only mode in development and enforces in production, and already allow-lists Clerk and the Throttle checkout origin. It is composed: a baseCsp for the core storefront, a preview-only fragment (gated on VERCEL_ENV), and a connectorCsp array where each optional module registers its own origins. Add an entry there — or extend baseCsp — when you introduce external scripts (analytics, other SDKs).
All env vars are validated in src/lib/env.ts with Zod. Copy .env.local.example to .env.local and fill in:
Throttle (commerce)
| Var | Required | Purpose |
|---|---|---|
THROTTLE_API_KEY |
yes | Server-side secret key (sk_…). |
THROTTLE_STORE_ID |
yes | UUID of the Throttle store/application carts + orders attach to. |
THROTTLE_WEBHOOK_SECRET |
for webhooks | Verifies signatures on /api/throttle/webhook. |
Clerk (auth)
| Var | Required | Purpose |
|---|---|---|
CLERK_SECRET_KEY |
yes | Server-side secret key (sk_…). |
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY |
yes | Browser publishable key (pk_…). |
CLERK_WEBHOOK_SIGNING_SECRET |
for webhooks | Verifies /api/webhooks/clerk (customer mirror). |
Path/origin defaults (NEXT_PUBLIC_CLERK_SIGN_IN_URL, NEXT_PUBLIC_THROTTLE_CHECKOUT_URL, etc.) ship in the committed .env and rarely need changing — override in .env.local only if you mount things differently.