AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
MCP verified MIT Self-run

Minshop

mcp-ddyy-minshop · by ddyy

Full-stack ecommerce on Cloudflare Workers, D1, and R2 - with admin, Stripe, OpenNode, Lightning payments, and agent-ready APIs.

No reviews yet
0 installs
30 views
0.0% view→install

Install

$ agentstack add mcp-ddyy-minshop

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/mcp-ddyy-minshop)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2mo ago

Declared compatibility

Claude CodeClaude DesktopCursorWindsurf

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Minshop? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

minshop

A small, full-Cloudflare ecommerce store: storefront + admin + Stripe payments, running on Cloudflare Workers. No brand baked in — the store name is a config value, so it clones cleanly as a template.

It's intentionally lightweight and cheap to run: Cloudflare's free tier covers it until real volume, and the only hard cost is Stripe's per-sale fee.

Features

  • Storefront — server-rendered product list + product detail, near-zero client JS
  • Cart — cookie-based (no client JS), with both "Add to cart" and one-click "Buy now"
  • Search — FTS5 full-text (bm25, prefix match, typo-correct), no JS — or semantic search via Workers AI + Vectorize, one config toggle (see [Search](#search))
  • Categories — nested (arbitrary-depth tree), many-to-many with products; storefront category pages with breadcrumbs + sub-category drill-down (recursive descendant queries)
  • Admin — product CRUD (create / edit / delete) with image upload, plus a recent-orders view
  • Payments — Stripe Checkout (hosted, single or multi-item) with a signature-verified webhook that records orders + line items — or Bitcoin Lightning (phoenixd / LNbits self-hosted, or OpenNode hosted), one config switch (see [Payments](#payments))
  • Shipping — Stripe Checkout collects the shipping address + offers configurable rates (flat rates + free-over-threshold); the address & cost are captured onto the order
  • Discount codes — promo-code field on checkout (toggle in config); codes are created/managed in the Stripe Dashboard, and the applied discount is captured onto the order
  • Tax — sales tax / VAT via Stripe Tax (off by default — activate Stripe Tax in the Dashboard first); computed from the customer address and captured onto the order
  • Order email — confirmation email behind an EmailProvider seam with two adapters: Resend (HTTPS API, works on the Workers free plan) or Cloudflare Email (binding, paid plan); gated by config (off until configured, then it's a flag)
  • Images — uploaded to R2, served through the app (no public bucket required)
  • Swappable seams — payments and storage sit behind interfaces (see [Architecture](#architecture))

Stack

| Piece | Choice | |---|---| | Framework | Astro (SSR) on Cloudflare Workers via @astrojs/cloudflare | | Styling | Tailwind CSS v4 (@tailwindcss/vite) | | Data | Cloudflare D1 (SQLite) — products, orders | | Images | Cloudflare R2 — zero egress | | Payments | Stripe Checkout, or Bitcoin Lightning (phoenixd / LNbits / OpenNode) |

Quick start (local)

> Requires Node ≥ 22.12 — use Node 22, the tested/supported release line.

npm install

# One shot: build, migrate + seed the local DB, and generate the two local
# secrets (SECRETS_KEK + AUTH_SECRET) into .dev.vars
npm run provision:local -- --seed

npm run dev                        # http://localhost:4321

Storefront is at /, admin at /admin — first visit lands on the setup wizard. There are no provider secrets to fill in by hand: payment/email/Turnstile keys are pasted in Admin → Settings and stored encrypted in D1 (see [Settings](#settings)). Granular scripts also exist (db:migrate, db:seed), npm run preview runs the built worker in prod-mode (admin gate active), and npm run destroy:local resets the local store.

Tests

npm test          # vitest run (unit tests for pure logic)
npm run test:watch
npm run test:d1   # fresh migrations + seed + built Worker against isolated D1

Covers the pure functions — slugify, the FTS search sanitizer + edit-distance, parseProductForm, image validation, cart counting, reservation target aggregation, the order-number scheme, the Access-JWT verifier, pagination clamping, and the whitelisted orderByClause sort builders (the SQL-injection boundary for sortable tables). npm run test:d1 adds clean-room D1 gates for reservation concurrency/release/settlement/legacy compatibility, then boots the production Worker against an isolated database and runs a demo checkout through paid-order settlement and confirmation. npm run verify runs both suites, full Astro diagnostics, the production build, and the MCP typecheck/deployment dry run.

Testing payments locally

Paste your sk_test_… key in Admin → Settings → Payments → Card (Stripe), then forward Stripe events to your dev server with the Stripe CLI:

stripe listen --forward-to localhost:4321/api/webhook
# paste the printed whsec_… into Settings → Payments → Card (Stripe) → webhook signing secret
stripe trigger checkout.session.completed

The order shows up in /admin (and in D1: npx wrangler d1 execute minshop-db --local --command "SELECT * FROM orders").

Deploy

One-click (Deploy to Cloudflare)

[](https://deploy.workers.cloudflare.com/?url=https://github.com/ddyy/minshop)

Forks the repo, provisions D1 (minshop-db) + R2 (minshop-images), applies migrations, and deploys — free-plan resources only. Then finish onboarding at /admin/setup (below). Optional paid add-ons (Images, semantic search, Cloudflare Email) are commented in wrangler.jsonc — uncomment + redeploy.

Fields the deploy form shows:

  • SECRETS_KEK, AUTH_SECRET (masked, required) — the only two you must set. Paste a fresh random value into each (they must differ): run openssl rand -base64 32 twice. The button can't generate them, and because the fields are masked their hints are hidden — so a leading label field, USE_A_LONG_RANDOM_STRING_FOR_SECRETS_KEK_AND_AUTH_SECRET, carries the instruction above them (its own value is unused filler). Left empty, the store still runs: SECRETS_KEK unset keeps the payment-key vault dormant (demo checkout only), AUTH_SECRET unset disables customer accounts — both configurable later.
  • Location hint — Cloudflare's own field for where to place the D1 database; pick the region nearest your shoppers.
  • No store name / time zone / search fields — those are runtime settings you configure in the setup wizard and Admin → Settings (stored in D1), not at deploy time. Defaults until then: My Shop / UTC / keyword (FTS) search.

CLI (one shot)

One shot — provisions a fresh, fully-independent instance (its own D1, R2, Vectorize index, Worker) and sets both Worker secrets:

npx wrangler login
npm run provision:cf my-store      # scripts/provision-cf.sh 

Or manually:

npx wrangler login

# Provision real resources. The committed wrangler.jsonc declares D1/R2 by NAME
# with no ids (so a one-click / Workers Builds deploy auto-provisions them); for a
# manual deploy, add the printed database_id to the "DB" entry — or let
# `wrangler deploy` create it.
npx wrangler d1 create minshop-db
npx wrangler r2 bucket create minshop-images
npm run db:migrate:remote          # applies migrations/ to the production DB

# The ONLY two Worker secrets. Everything else (Stripe, OpenNode, Lightning,
# Resend, Turnstile keys + their config) is entered in Admin → Settings and
# stored encrypted in D1 under SECRETS_KEK.
openssl rand -base64 32 | npx wrangler secret put AUTH_SECRET   # signs sessions
openssl rand -base64 32 | npx wrangler secret put SECRETS_KEK   # encrypts the key vault

npm run deploy                     # astro build && wrangler deploy

Then open the site — it funnels to the setup wizard: set the admin password (required to finish; until then /admin/setup is open, so do it right away or front /admin with Access), then paste your payment keys in Settings → Payments. Finally point a production Stripe webhook at https:///api/webhook/stripe (events: checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, and checkout.session.expired; async success fulfils delayed methods, while failure/expiry releases held inventory) and paste its whsec_… signing secret in the same card. Tear an instance down with npm run destroy:cf ; reset its data in place with npm run reset:remote.

Admin auth

Both /admin and /api/admin/* are gated by [src/middleware.ts](src/middleware.ts). You authenticate one of two ways:

  • Setup-wizard password (built-in). On a fresh store the setup wizard at /admin/setup is reachable so you can create an admin password; it's stored as a PBKDF2 hash in D1 (there is no ADMIN_PASSWORD env var). The moment you save it the gate activates: /admin + /api/admin require sign-in at /admin/login (a form issuing an HMAC-signed, HttpOnly session cookie; /api/admin returns 401 instead of redirecting), and saving logs you in immediately. First-run is open — until a password exists, /admin/setup is publicly reachable, so set it promptly on a public deploy (or front /admin with Access, below).
  • Optional Turnstile on the login form: enable it in Admin → Settings → Bot protection (toggle + sitekey, with the secret stored in the encrypted vault) to add Cloudflare's bot challenge before the password is checked. The same toggle also gates the customer account sign-in. Cloudflare's documented always-pass test keys work for local trials.
  • Cloudflare Access (recommended for production). Edge SSO/MFA, no app passwords, and it protects first-run setup too. In the Zero Trust dashboard add a self-hosted application covering both paths your-host/admin and your-host/api/admin (the API is a separate path — gating only /admin leaves the mutation endpoints open), add an identity provider (One-time PIN is enough), and an Allow policy for your email. Set both CF_ACCESS_TEAM_DOMAIN and CF_ACCESS_AUD in wrangler.jsonc vars; they are required for Access mode so the middleware can verify every JWT signature (zero-dependency, Web Crypto — see [src/features/auth/access.ts](src/features/auth/access.ts)). With either variable present, requests fail closed unless both are set and a valid assertion is supplied—including direct *.workers.dev requests with no header. (Use Access or the wizard password; if a password is set, password login takes precedence.)

Forgot the admin password? The hash lives in your D1 database, so the store owner can always reset it — clearing it drops the store back into the open setup wizard:

npm run admin:reset:remote   # deployed store  (or: npm run admin:reset for local)

Then reload /admin/setup and set a new one. (Same effect as running DELETE FROM settings WHERE key='admin_password_hash'; in the Cloudflare dashboard → D1 console.) It requires Cloudflare account access, so it isn't a public reset — and if you run Cloudflare Access you're never locked out in the first place.

Local dev (astro dev) bypasses the gate so you're never locked out. Don't make /admin a "secret" path — that's security-through-obscurity; the gate is what protects it.

The Worker also applies native edge rate limits to anonymous login POSTs (10/minute per store, route, and connecting client) and checkout/invoice POSTs (20/minute). Webhooks and authenticated admin APIs are deliberately excluded; provider signatures and the admin gate protect those paths without disrupting legitimate retries or bulk administration. The limits are declared as AUTH_RATE_LIMITER and CHECKOUT_RATE_LIMITER bindings in the provisioning template; existing manually maintained Wrangler configs need the same ratelimits block.

Payments

Checkout and the webhook depend on a PaymentProvider port (src/features/payments). Every rail is configured in Admin → Settings → Payments — its keys go in the encrypted vault, its config (default rail, node URLs) in D1 settings; a rail's checkout button appears automatically once its key is set, and the always-on Demo checkout works with zero keys. The "default rail" selector picks which one is offered first:

| Rail | What it is | Setup (all in Settings → Payments) | |---|---|---| | stripe (default) | Hosted card checkout | paste the secret key + webhook signing secret | | lightning | Bitcoin Lightning via a self-hosted node — minshop mints a BOLT11 invoice and renders its own /pay page (QR + lightning: link), then confirms settlement | run phoenixd or LNbits (below); enter its URL + key | | opennode | Hosted Lightning checkout (custodial processor) — redirect + webhook, like Stripe | paste the OpenNode API key |

Two ports, nested: the outer PaymentProvider (stripe / lightning / opennode) and an inner LightningBackend (phoenixd / lnbits) shared by the self-rendered flow. Adding another node is one adapter file.

How settlement is trusted. Lightning webhooks are treated as an untrusted nudge — on receipt minshop re-polls the node for the payment (the authority), so a forged webhook can't fake a sale. The /pay page also settles on load by polling, so it works even with no public webhook (e.g. local dev). Orders stay "paid-only": unpaid invoices live in a pending_payments table, never in orders.

Shipping (Lightning). When shipping.enabled, the Lightning cart routes through minshop's own /checkout page — a server-rendered address + shipping-option step (zone-based rates from cfg.shipping, see [Shipping](#shipping--order-email-config)) — so the invoice total includes shipping and the address + email land on the order, same as Stripe. (Stripe keeps its own hosted address/shipping collection, unchanged.)

Limitations (Lightning). Tax and promo codes are still Stripe-Checkout features and are skipped on the Lightning path — to charge tax on a non-Stripe rail you'd compute it yourself (e.g. Stripe's Tax Calculation API) and add it to the total. No automatic refunds (Lightning can't reverse in place). Invoices are priced in sats from a BTC spot rate fetched at checkout (payments.lightning.rateUrl, default Coinbase — no key).

Running phoenixd (recommended self-hosted backend)

phoenixd is ACINQ's self-custodial headless daemon — no full Bitcoin node, no manual channel management (ACINQ is the LSP), mainnet (real sats).

  1. Run phoenixd on an always-on host (small VPS / Pi). First launch writes a seed to ~/.phoenix/ — back it up.
  2. Expose its API (127.0.0.1:9740) to the Worker over HTTPS — a Cloudflare Tunnel (cloudflared) is the cleanest, no open ports.
  3. In Settings → Payments → Lightning, pick the phoenixd node, enter its public URL, and paste the password — use the http-password-limited-access value (~/.phoenix/phoenix.conf): receive-only, can't spend.
  4. Point phoenixd's webhook-url at https:///api/webhook/lightning.

LNbits instead: pick LNbits in the same card and enter its URL + the wallet's Invoice/read key. OpenNode: paste the API key in its card and set the default rail to OpenNode.

Architecture

Organized as feature folders (vertical slices) — each owns its data access, types, and components. Deleting a folder still builds.

src/
  config.ts                  store name + feature toggles
  layouts/Layout.astro
  features/
    products/  db · form · image · ProductForm.astro
    orders/    db
    payments/  provider (port) · stripe (adapter) · index (factory)
    storage/   provider (port) · r2 (adapter) · index (factory)
  pages/
    index.astro              storefront
    product/[id].astro
    admin/                   admin UI (CRUD)
    images/[...key].ts       serves R2 objects
    api/                     checkout · webhook · admin product endpoints

Two ports-and-adapters seams keep vendor code at the edges:

  • PaymentProvidercheckout.ts / webhook.ts depend on the interface, not Stripe. Swapping to Paddle / Lemon Squeezy is one new adapter file.
  • StorageProvider — the app stores and serves images by key; R2 is one adapter.

Deliberately no runtime plugin system — Workers bundle at build tim

Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.