https://github.com/odanree/shopify-ecommerce
Full-stack headless ecommerce engine featuring Next.js 14, Shopify Admin/Storefront APIs, and a resilient Stripe webhook architecture.
https://github.com/odanree/shopify-ecommerce
ai-chatbot ecommerce headless-commerce nextjs shopify stripe-payments typescript
Last synced: 3 months ago
JSON representation
Full-stack headless ecommerce engine featuring Next.js 14, Shopify Admin/Storefront APIs, and a resilient Stripe webhook architecture.
- Host: GitHub
- URL: https://github.com/odanree/shopify-ecommerce
- Owner: odanree
- Created: 2025-10-30T21:02:40.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2026-04-09T03:31:30.000Z (4 months ago)
- Last Synced: 2026-04-09T05:28:18.116Z (4 months ago)
- Topics: ai-chatbot, ecommerce, headless-commerce, nextjs, shopify, stripe-payments, typescript
- Language: TypeScript
- Homepage: https://shopify-headless-lemon.vercel.app
- Size: 1.11 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# ποΈ Shopify Headless Commerce with Next.js
A high-performance headless ecommerce storefront built with **Next.js 14**, **TypeScript**, and **Shopify Storefront API**. This project features a **custom, production-grade Stripe payment engine** with a webhook-driven architecture for real-time Shopify Admin order synchronization.
π― **[Live Demo](https://shopify-headless-lemon.vercel.app/)** β _Experience the custom checkout flow._
---
## π οΈ Tech Stack & System Architecture
### The Core Engine
- **Framework:** [Next.js 14+](https://nextjs.org/) (App Router) for Server-Side Rendering and optimized Route Handlers
- **Language:** [TypeScript](https://www.typescriptlang.org/) for strict type-safety across the payment and order pipelines
- **Styling:** CSS Modules for scoped, maintainable component styling
### The "Headless" Integration
- **Commerce:** [Shopify Admin API](https://shopify.dev/docs/api/admin-rest) (REST) for robust order management, tagging, and inventory sync
- **Payments:** [Stripe SDK](https://stripe.com/docs/api) utilizing Stripe Elements for a secure, PCI-compliant checkout experience
- **Architecture:** Asynchronous Webhook Handshake with frontend polling to ensure data consistency between Stripe and Shopify
---
## ποΈ Technical Architecture: The Stripe-to-Shopify Bridge
The core of this project is a bespoke checkout system that maintains **100% brand control** while ensuring data consistency between two distinct third-party ecosystems.
### 1. Intent Orchestration & PCI Compliance
- **Elements-First Flow:** Implements Stripe's latest `PaymentElement` standards, supporting Apple Pay, Google Pay, and link-based payments.
- **Metadata Injection:** Upon `/api/payment/create-intent`, the backend injects Shopify `variant_ids` and cart snapshots into the Stripe metadata to preserve state through the payment lifecycle.
### 2. Resilience & "Ghost Order" Prevention
- **Webhook-Driven Logic:** Orders are *not* created on the frontend redirect. Instead, a Next.js API Route (`/api/payment/webhook`) listens for `payment_intent.succeeded`.
- **Signature Verification:** Employs `stripe.webhooks.constructEvent` to verify cryptographic signatures, ensuring only authentic Stripe events can trigger Shopify order creation.
- **Asynchronous Reliability:** The architecture handles "Ghost Orders"βsituations where a user pays but closes their browser before the redirectβensuring the Shopify Admin is always updated.
### 3. Shopify Admin Sync
- **Variant ID Translation:** Maps Storefront GIDs to Admin-specific numeric IDs to handle inventory decrements.
- **Race-Condition Handling:** The success page uses a polling mechanism to fetch the order number from a temporary cache, providing a seamless UX while the webhook processes in the background.
- **Idempotency:** Utilizes Payment Intent ID tagging to prevent duplicate orders during webhook retries.
---
## ποΈ Robust Checkout Architecture
Unlike basic Shopify integrations, this project implements a **Resilient Webhook Handshake** that gracefully handles asynchronous operations and network delays:
### 1. **Frontend Payment Capture**
- User fills Shipping Address β Payment Intent created with customer data in metadata
- Stripe Elements collect payment details (no card numbers on our server)
- User clicks "Complete Purchase" β Stripe PaymentElement validates and submits
### 2. **Webhook Verification & Signature Check**
- Stripe pings `/api/payment/webhook` with signed event
- We verify cryptographic signature using `stripe.webhooks.constructEvent()`
- Only authenticated Stripe events can trigger order creation (prevents spoofing)
### 3. **Async Background Processing**
- Webhook handler returns `200 OK` to Stripe **immediately** (~50ms)
- Shopify order creation happens asynchronously in background
- This prevents Stripe's 30-second timeout during slow Admin API calls (which can take 1β3 seconds)
- Payment is confirmed to customer, order is guaranteed to eventually appear in Shopify
### 4. **Idempotency & Duplicate Prevention**
- Each Shopify order is tagged with the Payment Intent ID: `Stripe-Payment, pi_xxxxx`
- Webhook checks for existing orders before creating new ones
- If webhook is retried by Stripe, we return the existing order (no duplicates)
### 5. **Resilient Success Page Polling**
- Success page can't display order number until cache is populated
- Webhook processes asynchronously, so data arrives ~500msβ2s after payment
- Instead of showing "Processing..." forever, page polls `/api/payment/order-number` up to 10 times with 2-second intervals
- Once order is found, displays order number and provides direct link to Shopify Admin
### 6. **Shopify Admin Link for Portfolio Proof**
- Success page includes button: "Open Order in Shopify Admin β"
- Direct link to `admin.shopify.com/store/{store}/orders/{shopifyOrderId}`
- Recruiter/client can instantly verify the order exists in Shopify with correct customer data
### The Complete Webhook Sequence (Interactive Diagram)
```mermaid
sequenceDiagram
participant User
participant Frontend as Next.js Client
participant API as Next.js API (Route Handlers)
participant Stripe as Stripe API
participant Shopify as Shopify Admin API
User->>Frontend: Clicks "Pay Now"
Frontend->>Stripe: Process Payment (Stripe Elements)
Stripe-->>Frontend: Payment Successful (Client-side)
par Background Webhook Handshake
Stripe->>API: POST /api/payment/webhook (payment_intent.succeeded)
API->>API: Verify Stripe Signature
API-->>Stripe: 200 OK (Immediate Acknowledge)
API->>Shopify: Create Order (Line Items + Metadata)
Shopify-->>API: Order #1014 Created
API->>API: Cache Order Mapping (PI ID -> Order #)
and Frontend UX
Frontend->>User: Redirect to /checkout/success
loop Polling
Frontend->>API: GET /api/payment/order-number?pi=xyz
alt Not in Cache
API-->>Frontend: 404 Not Found
else Order Found
API-->>Frontend: 200 OK (Order #1014)
end
end
end
Frontend->>User: Display Order #1014 & Admin Link
```
### ASCII Sequence Diagram (Reference)
```
βββββββββββ ββββββββββ ββββββββββββ ββββββββββ
β User β β Stripe β β Your API β β Shopifyβ
ββββββ¬βββββ βββββ¬βββββ ββββββ¬ββββββ βββββ¬βββββ
β β β β
β 1. Fill Address β β β
βββββββββββββββββββββββ>β β β
β β β β
β 2. Click "Pay" β β β
βββββββββββββββββββββββ>β Create Intent β β
β β (with metadata) β β
β βββββββββββββββββββββββ>β β
β β<βββ Intent Created ββββ€ β
β β β β
β 3. Verify Signature β β β
β (Elements.submit()) β β β
βββββββββββββββββββββββ>β Confirm Payment β β
β βββββββββββββββββββββββ>β β
β (Waiting) β β β
β β β
Payment Success β β
β β β β
β β POST /webhook (signed)β β
β βββββββββββββββββββββββ>β β
β β<ββββββ 200 OK βββββββββ€ (return immediately) β
β β β β
β 4. Redirect β [async background] β
β to Success β βββββββCreate Orderβββββ>β
β (polling starts) β β β
β β β<βββ Order Created βββββββ€
β β β (1-3 seconds) β
β 5. Poll for order β β β
βββββββββββββββββββββββββββββββ> GET /order-number β
β β ββββ return order # βββββ>β
β β β β
β "Order #1014 β" β β β
β [Click: View Admin] β β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ>β
β β β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
KEY FEATURES OF THIS FLOW:
β’ Phase A: Stripe Elements captures payment with cart metadata
β’ Phase B: 200 OK returned to Stripe immediately (prevents timeout/retries)
β’ Phase C: Async order creation happens in background
β’ Phase D: Frontend polling bridges the gap between payment success and Shopify confirmation
β’ Phase E: Direct Shopify Admin link proves order exists (portfolio demo gold!)
```
---
## β¨ Key Features
- β‘ **Next.js 14 App Router:** Utilizing Server Components for lightning-fast catalog browsing.
- π **Inventory Management:** Real-time stock decrements in Shopify Admin upon verified payment.
- π€ **AI Chatbot:** GPT-4 powered product search & recommendations (see [docs/CHATBOT.md](./docs/CHATBOT.md)).
- π¨ **CSS Modules:** 100% component-scoped styling for zero CSS bloat.
- π **Type Safety:** End-to-end TypeScript definitions for Shopify and Stripe payloads.
- π **Cart Persistence:** LocalStorage-backed cart with hydration safety and automatic cleanup on order success.
- π± **Fully Responsive:** Professional UI optimized for all screen sizes.
---
## π Stripe Checkout Implementation
### π Webhook Setup (Local Development)
Webhooks allow orders to be created in Shopify Admin even if the user closes their browser after paying.
1. **Install & Login:**
```bash
stripe login
```
2. **Listen for events:**
```bash
stripe listen --forward-to localhost:3000/api/payment/webhook
```
3. **Configure `.env.local`:**
Use the `whsec_` secret provided by the CLI.
### π§ͺ Integration Sequence (Observed Logs)
```
β
Payment Intent created: pi_3T4Fy...
π§ Webhook event received: payment_intent.succeeded
π¦ Line item mapping: variantId=44303963652141, quantity=2
β
Order created: #1010 (ID: 6137892339757) in Shopify Admin
π¦ Cached order #1010 for frontend polling
```
---
## π Project Structure
```
βββ app/ # Next.js App Router
β βββ api/payment/ # The "Bridge": create-intent, webhook, order-number
β βββ checkout/ # Custom Multi-step checkout UI
β βββ success/ # Order confirmation with polling logic
βββ components/
β βββ checkout/ # Stripe Element wrappers & Address forms
βββ lib/
β βββ shopify.ts # Storefront API (Catalog)
β βββ shopify-admin.ts # Admin API (Order Creation)
βββ contexts/
β βββ CartContext.tsx # Cart logic + persistence
βββ docs/ # Detailed feature documentation
```
---
## π§ Deployment
### Vercel (Recommended)
This project is optimized for Vercel. Ensure the following Environment Variables are configured:
| Variable | Source |
|----------|--------|
| `SHOPIFY_STOREFRONT_ACCESS_TOKEN` | Shopify App Settings |
| `SHOPIFY_ADMIN_API_TOKEN` | Shopify Custom App (write_orders) |
| `STRIPE_SECRET_KEY` | Stripe Dashboard |
| `STRIPE_WEBHOOK_SECRET` | Stripe Dashboard > Webhooks |
---
## π Lessons Learned & Architectural Decisions
### π― Security & Deployment Protection
**Problem:** Vercel's **Deployment Protection** (authentication wall for Preview URLs) blocked incoming Stripe Webhooks with `401 Unauthorized` errors. The webhook logic was correct, but the security layer prevented Stripe's servers from reaching the API route. This made it impossible to test webhook-driven order creation on feature branches.
**Solution:** Identified the conflict between automated webhook deliveries and Vercel's security layer. Configured bypass tokens for preview environments or toggled protection settings to allow Stripe's "handshake" to reach the `/api/payment/webhook` route without authentication.
**Impact:** Secure, automated testing on every Pull Request without exposing the entire site to the public. Webhooks can be tested safely in preview deployments while keeping other routes behind authentication walls.
---
### π― Environment Variable Scoping
**Problem:** Using a single `STRIPE_WEBHOOK_SECRET` caused conflicts between local testing (Stripe CLI with `whsec_test_...`) and cloud testing (Stripe Dashboard with `whsec_live_...`). Each environment generates a unique signing secret, and using the wrong one causes signature verification to fail.
**Solution:** Implemented environment-aware secret selection using `process.env.VERCEL_ENV` to dynamically switch between:
- `STRIPE_WEBHOOK_SECRET` (Preview deployments on feature branches)
- `STRIPE_WEBHOOK_SECRET_PROD` (Main branch/production)
Validation moved to runtime (in the webhook handler) instead of module load time to prevent build failures when env vars aren't fully available.
**Impact:** Zero-config deployments; the code "just works" on feature branches, preview URLs, and production without code changes. Different environments automatically use the correct secrets.
---
### π― Idempotency & Data Mismatch
**Problem:** Stripe's metadata is flat (all keys at same level), but Shopify's REST API expects deeply nested objects. Order tags required comma-separated strings, not arrays. Additionally, Shopify GIDs (GraphQL IDs like `gid://shopify/ProductVariant/12345`) needed to be converted to REST API numeric IDs.
**Solution:** Implemented a strict data mapping layer that:
- Transforms Stripe's flat metadata into Shopify's required nested schema
- Extracts Payment Intent IDs to use as idempotency keys
- Strips `gid://` prefixes and converts variant IDs for Admin API compatibility
- Prevents duplicates during webhook retries by checking for existing orders
**Impact:** Orders create reliably across mismatched API schemas without data loss, duplication, or ID conversion errors.
---
### π― Serverless Webhook Constraints
**Problem:** In serverless environments (Next.js on Vercel), if the webhook handler takes too long, the request is killed before Shopify order creation completes. Stripe also enforces a 30-second timeout before retrying the webhook. Blocking the webhook response on slow Shopify API calls (1β3 seconds) risks timeout failures and duplicate webhook retries.
**Solution:** Optimized the webhook handler to:
- Return `200 OK` to Stripe as fast as possible (~50ms)
- Process Shopify order creation asynchronously via `processOrderAsync()` pattern
- Use `await` to ensure the order creation completes before the function exits
- Implement idempotency checks to prevent duplicates on Stripe retries
**Impact:** 100% webhook success rate in Stripe while guaranteeing order creation in Shopify, even under API latency. Vercel's serverless platform doesn't prematurely kill the function, and Stripe doesn't retry due to timeout.
---
### π― User Experience Lifecycle
**Problem:** Clearing the cart immediately on "Complete Purchase" button click meant failed or declined payments would lose the user's cart. If they closed the browser mid-checkout, their cart disappeared foreverβa significant conversion killer.
**Solution:** Moved `clearCart()` from the payment button handler to the Success Page, triggered only after the order is confirmed (polling successfully finds the order in Shopify cache). This preserves the cart through payment retries and browser restarts.
**Impact:** Users can safely retry failed payments without losing their items. High-intent users don't abandon the flow due to payment failures; they recover their cart and try again.
---
### π― Handling API Latency
**Problem:** Shopify's Admin API can take 1β3 seconds to respond. The Success Page needs the order number immediately after payment succeeds, but the webhook processes asynchronously. Without polling, users see "Processing order..." indefinitely because the success page doesn't wait for the webhook to complete.
**Solution:** Implemented recursive polling on the frontend (10 attempts, 2-second intervals). The success page queries `/api/payment/order-number` until the Shopify order is found, with graceful handling of `404` responses during the webhook delay.
**Impact:** Seamless UX even with slow backend APIs; users see their order number appear within 2β5 seconds rather than hanging indefinitely. The "Processing..." state feels responsive and intentional.
---
---
## π§ͺ Testing Suite
This project uses **Playwright** for comprehensive End-to-End (E2E) testing. We have migrated from Cypress to Playwright to take advantage of superior speed, parallelization, and native support for complex iframes (like Stripe).
### π Key Features
- **Parallel Execution:** Runs 72+ test cases in ~25 seconds across 3 browsers (Chromium, WebKit, Mobile iPhone).
- **Resilience Patterns:** Uses `expect().toPass()` polling for React hydration and dynamic cart updates, ensuring tests are stable and catch real bugs.
- **Stripe Integration:** Specialized handling for nested Stripe Payment Element iframes with HMAC-SHA256 webhook signature validation.
- **UI Shields:** Automatic injection of CSS shields during tests to hide obstructive third-party widgets (e.g., AI Chatbots).
- **Multi-Device Testing:** Validated across Chromium, Webkit (Safari), and Mobile iPhone profiles with real browser contexts.
- **Semantic Selectors:** Uses `data-testid` convention (Playwright native) instead of framework-specific `data-cy` for better maintainability.
### π οΈ Running Tests
Ensure your local environment variables are loaded:
```bash
# Run all tests (headless mode, production build)
npm run build
npm run start # In one terminal
npm run test:e2e # In another terminal
# Run only the checkout flow with verbose output
dotenv -e .env.local -- npx playwright test checkout --reporter=verbose
# Run in UI Mode (Interactive, browser-based test runner)
npm run test:e2e:ui
```
### β¨ Pre-commit Safety with Husky
We use **Husky** to maintain a "Green Master" branch:
- **Pre-commit Hook:** Every commit automatically runs a focused smoke test of the critical checkout flow (`home β product β cart β checkout β complete payment`).
- **CI Pipeline:** GitHub Actions runs the full 72-test suite on every Pull Request, ensuring no regressions.
```bash
# If you want to bypass the pre-commit hook (not recommended):
git commit --no-verify
```
### 𧬠Development Conventions
When building new components or fixing functionality, always use `data-testid` for test selectors:
```html
Add to Cart
Add to Cart
```
This ensures:
- Tests are decoupled from styling changes
- Selectors survive CSS refactors
- Maintainers know which elements are tested
### π Current Test Coverage
| Test Suite | Count | Status | Coverage |
|-----------|-------|--------|----------|
| **Smoke Tests** | 9 | β
Passing | Home, Product, Cart, Checkout pages load correctly |
| **Webhook Tests** | 15 | β
Passing | HMAC signature validation, order creation, Shopify sync |
| **Idempotency Tests** | 12 | β
Passing | Duplicate prevention via Payment Intent tagging |
| **Checkout Flow** | 24 | β
Passing | Complete cart β checkout β payment β success |
| **Cart Logic** | 12 | β
Passing | Item persistence, hydration, edge cases |
| **Total** | **72** | β
**100% Passing** | Critical payment path fully validated |
### π§ Architecture Patterns
#### 1. Expect-Based Polling (Resilience)
Instead of one-time checks that fail on timing variations, we use Playwright's built-in retry loop:
```typescript
// β
Resilient: Retries continuously until element appears or timeout
await expect(page.getByTestId('cart-item')).toBeVisible({ timeout: 10000 });
// β Flaky: One-time check, misses timing windows
if (await page.getByTestId('cart-item').isVisible({ timeout: 5000 })) { ... }
```
#### 2. CSS Shields for Test Stability
Some third-party widgets (chatbots, analytics) can block clicks on form elements. We inject CSS shields during tests to prevent interference:
```typescript
await page.addStyleTag({
content: `
[class*="imageContainer"] { pointer-events: none !important; }
#ai-chatbot-widget { display: none !important; }
`,
});
```
#### 3. localStorage Persistence Verification
Cart items must persist to localStorage before redirecting. We explicitly sync before navigation:
```typescript
await new Promise((resolve) => {
setTimeout(() => {
const items = JSON.parse(localStorage.getItem('cart') || '[]');
// Verify and sync cart items
localStorage.setItem('cart', JSON.stringify(items));
resolve();
}, 150);
});
router.push('/cart');
```
### π Test Structure
```
playwright/
βββ fixtures/
β βββ payment.fixture.ts # Stripe client + PaymentIntent setup
βββ support/
β βββ stripe-mock.ts # Mock webhook generator (HMAC-SHA256)
β βββ helpers.ts # fillShippingInfo, fillStripeCard, etc.
βββ e2e/
βββ smoke.spec.ts # 9 tests: page loading
βββ checkout.spec.ts # 24 tests: complete payment flow
βββ webhook.spec.ts # 15 tests: signature validation
βββ idempotency.spec.ts # 12 tests: duplicate prevention
βββ cart.spec.ts # 12 tests: cart logic
```
### π¦ Status & Next Steps
**Current:** 72/72 tests passing (100%) across Chromium, WebKit, and iPhone profiles.
**Future Enhancements:**
- Visual regression testing via `expect.toHaveScreenshot()`
- Load testing with Artillery (webhook throughput)
- Lighthouse performance audits in CI
---
## π οΈ Development
### Install Dependencies
```bash
npm install
```
### Run Local Dev Server
```bash
npm run dev
```
### Build for Production
```bash
npm run build
npm start
```
### Run E2E Tests
```bash
npm run test:e2e
```
---
**Built with β€οΈ for modern ecommerce. Production-ready. Portfolio-worthy.**