Official Inngest swag store. Built durably — every order flows through an Inngest function you can watch in real time.
Official store for Inngest merchandise. Built durably — every order flows through an Inngest function you can watch run in real time.
Live at swag.inngest.com.
A small Next.js e-commerce site that demonstrates Inngest in production. Stripe handles payments. Inngest handles the durable order workflow. Google Sheets is the fulfillment surface. Realtime publishes power a public live order tracker at /admin.
Stripe Checkout → POST /api/webhooks/stripe → inngest.send("store/order.placed")
↓
fulfill-order Inngest function (4 durable steps):
├─ step.run("capture-payment")
├─ step.run("reserve-inventory")
├─ step.run("send-confirmation")
└─ step.run("record-to-sheet") ← appends to Google Sheet
Each step publishes to two Realtime channels:
- order:{orderId} → the customer's /orders/[orderId] page
- admin → the public /admin live tracker
PII (email, name, phone, shipping) flows through event.data.encrypted and is encrypted at rest in Inngest’s storage via @inngest/middleware-encryption. Step outputs and realtime payloads are PII-free.
You need:
brew install stripe/stripe-cli/stripe)node scripts/setup-orders-sheet.mjs <spreadsheet-id> after sharing the sheet with the service account)npm install
cp .env.local.example .env.local
# fill in real values
# Terminal 1 — Next.js dev server
npm run dev
# Terminal 2 — Inngest dev server
npx inngest-cli@latest dev
# Terminal 3 — Stripe webhook forwarding
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# copy the printed whsec_... into STRIPE_WEBHOOK_SECRET in .env.local
Visit http://localhost:3000 to browse, http://localhost:8288 for the Inngest dashboard, http://localhost:3000/admin for the live order tracker.
| Path | Purpose |
|---|---|
src/app/api/checkout/route.ts |
Creates Stripe Checkout Sessions |
src/app/api/webhooks/stripe/route.ts |
Validates Stripe signatures, fires store/order.placed |
src/app/api/inngest/route.ts |
Inngest serve handler |
src/inngest/client.ts |
Inngest client + encryption middleware |
src/inngest/channels.ts |
Realtime channels (order:{id}, admin) |
src/inngest/functions/fulfill-order.ts |
The durable workflow — 4 steps |
src/lib/sheets.ts |
Google Sheets read/write helpers |
src/lib/catalog.ts |
Static product catalog |
src/components/OrderStatusClient.tsx |
Customer-facing order page |
src/components/AdminClient.tsx |
Public live order tracker |
/orders/[id]) defaults to masked PII (email + total).unlockOrderViewing server action → cookie set → full data on subsequent visits to that order.Cookie is httpOnly + 30-day, scoped per order ID. Stripe session_id verified server-side before issuing.
The site deploys to Vercel. Configure these env vars in the Vercel dashboard:
STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRETINNGEST_EVENT_KEY, INNGEST_SIGNING_KEY (from Inngest Cloud — leave INNGEST_DEV unset)INNGEST_ENCRYPTION_KEY (32-byte base64; same key across environments)GOOGLE_SERVICE_ACCOUNT_JSON (base64 of the SA key JSON)ORDERS_SHEET_ID, ORDERS_SHEET_NAMENEXT_PUBLIC_APP_URL (e.g. https://swag.inngest.com)Stripe webhook destination (production): https://swag.inngest.com/api/webhooks/stripe. Get the production whsec_ from Stripe Dashboard → Developers → Webhooks.
MIT — see LICENSE. Forks, copies, and adaptations welcome. If you build something cool with this pattern, we’d love to hear about it.