Custom Store Management (CSM)

A headless commerce back office for WordPress — catalogue, orders, payments, returns and a storefront assistant, driven by a React admin and 237 MCP tools.

Client
EpsolDev
Category
Platform Engineering
Completed
2026-08-14
Built with
WordPress, PHP, React.js, TypeScript, Vite, Tailwind CSS, MySQL, REST API
Topics
API Development, Backend, CMS, E-commerce
Custom Store Management (CSM)

WordPress Plugin Headless Commerce MCP Server In production

CSM is the commerce instance of a platform architecture that also powers Custom Blog Management and EpsolDev Management. Same skeleton — convention autoloader, singleton loader, a React SPA mounted inside wp-admin, a capability-gated admin API beside a public storefront API, and an in-plugin MCP server. Different domain: a complete storefront back office.

It is by far the largest of the three, and it runs a live store.

The CSM dashboard: catalogue, order, customer and revenue counters, an order pipeline bar and a live activity feed

Every screenshot on this page is the running admin. Customer names, email addresses, order references and money figures are blurred — the product is the subject, not the shop's customers.


The split that shapes everything

Entity kindWhere it livesWhy
Products, categories, brands, pagesWordPress post types and taxonomieseditorial, low write volume, wants revisions and media
Orders, carts, reviews, returns, stock history28 custom database tablestransactional, high write volume, needs real indexes and ledgers

Products are a post type that is deliberately not publicly queryable. The store is headless, so the only way in is the API.


Catalogue

The CSM product catalogue with search, filters, stock state and bulk actions

Variants are stored as normalised JSON in post meta rather than a separate variant table, and every write path funnels through one canonicalisation step — so a variant written by the SPA, the storefront API or an MCP tool comes out identically shaped.

The search index behind this screen exists because of one measurable failure. WordPress core ANDs search terms across title, excerpt and content only, so black leather jacket returned zero results while leather jacket returned dozens — because "black" lived inside variant JSON that core never looks at. The fix is a denormalised index built from every searchable surface and refreshed on write.


Orders

CSM order management showing the status pipeline, per-status filters, payment state and export

Orders live in their own tables with a model and service split, and status transitions are stock-aware: cancelling or refunding restores inventory rather than silently leaving it committed.

Money paths get invariants, not just tests The payment layer states three rules in code: the amount is never taken from the client, the secret key never leaves the server, and payment is only ever believed from the provider — the verification step re-reads the intent and checks both status and amount received. Checkout races the browser return against the webhook, so a single intent can only ever produce one order.


Inventory

CSM inventory with low-stock and out-of-stock views and the stock movement ledger

Stock is a ledger, not a number. Every adjustment — single, bulk, or an order transition that restores stock — writes a history row. You can always answer "why is this SKU at 3?" Low-stock and out-of-stock are views over the same data rather than separate flags that can drift.


Reviews and Q&A

CSM review moderation queue with pending, approved and rejected states

Reviews move through a moderation queue — pending, approved, rejected — and support images and threaded comments, each with its own table and controller.

CSM product questions and answers with moderation and a content-gap view

Product questions add a content-gap view: approved questions that still have no approved answer. That list is the highest-value copy work in the store, because each row is a real shopper objection nobody has answered yet. An AI draft-answer endpoint exists to speed it up and deliberately does not persist — a human still approves every word.


Coupons and offers

CSM coupon management with usage limits and validity windows

The database UNIQUE index on the coupon code is the source of truth, so a duplicate returns a clean 409 instead of racing two admins into two coupons with the same code.


The storefront assistant

CSM chat insights summarising storefront assistant sessions, topics and answer gaps

The public bot is a separate controller from the admin AI, on purpose. It structurally cannot reach the admin tool registry, has no edit-mode parameter at all, and replays history server-side so a visitor cannot forge an assistant turn.

It also caches its own answers, but only when every gate passes:

  • the question is self-contained, with no "it" or "that one"
  • the reply was a real answer, not a refusal or a handoff
  • every tool it used was impersonal
  • PII redaction changed nothing — if redaction would alter the text, the turn is discarded rather than cached

Chat insights turn those sessions into a work queue: what shoppers ask, which topics recur, and where the assistant had no good answer.

CSM AI provider settings showing per-feature model routing

Model choice is resolved per feature, not globally, with capability metadata attached to each one — that metadata is what stops a provider that can only run asynchronously from being routed to a synchronous consumer. Keys are decrypted inside the resolver and never logged or returned.


SEO

The CSM SEO manager with per-surface coverage scoring and structured-data controls

A per-surface SEO inventory with a 0–100 completeness score, a public sitemap endpoint, a redirects controller, and a 10-class JSON-LD engine with a documented merge strategy for repeated types. The AI-crawler policy is first-class: allow all, allow search but block AI training, block all AI, or a custom per-bot policy.


Access control

CSM roles and permissions showing granular per-feature capabilities

An umbrella store capability with 38 granular per-feature capabilities layered over it, so a warehouse role can touch inventory without seeing revenue. Rate limiting is keyed on client address and target, so brute-forcing one account cannot lock out the whole store and a shared proxy address does not collapse into a single bucket.


Transactional email

CSM transactional email templates with per-template editing and preview

Templates are editable per message, with preview and test send. Only the human-facing copy is editable; the HTML shell stays code-owned, because hand-edited table markup is exactly how transactional email breaks in Outlook.


Operating it through machines

The CSM MCP console showing registered tools, tokens and connection health

237 MCP tools from an auto-discovering registry — drop in a file that registers itself and it appears in tools/list. Plus a full OAuth 2.1 authorization server implemented inside the plugin, so a desktop AI client connects through browser consent instead of a pasted token, while static bearer auth keeps working.

Mutating tools on an approval-required token use a two-step protocol:

1. call tool   ->  dry-run preview + a one-time approval token,
                   bound to (token id + tool name + hash of the exact args)

2. re-call it  ->  executes once, then the grant is consumed

Approval for one call provably cannot authorise a different one.


Shipping the storefront from the back office

CSM frontend integration: connected storefront repository, branch, pull request and deploy state

The storefront repository is connected with a fine-grained token, encrypted at rest and never echoed back to the browser. From this screen you can branch off latest main, open a pull request, read the CI and mergeability rollup, and merge only when it is both mergeable and green — with rollback and deploy history.

The storefront it ships is Luxwearz.


Other engineering worth naming

Non-destructive image pipeline. New uploads convert to WebP; existing media is regenerated in batches by an admin action, an MCP tool and a self-scheduling cron. Full-size requests are short-circuited to the derivative. Explicit file URLs are emitted rather than relying on header negotiation, so it stays CDN-safe and needs zero storefront changes. Originals are never modified or deleted.

A tripwire that is a test. The expected MCP tool count is asserted in CI. It was migrated from counting files to counting registrations after a shared helper broke the original assumption.


Scale

PHP402 files, ~96,800 lines, 39 services, 51 controllers
Admin SPA341 TS and TSX files, ~91,700 lines, 69 routes
Data28 custom tables, 27 migrations, 7 post types
API2 namespaces, 341 route registrations
Automation237 MCP tools, 6 cron schedules
Access controlumbrella capability plus 38 granular

Counts taken from the shipped main branch at v1.15.55.


Stack

PHP 8.1 WordPress 6.5 React 18.3 TypeScript 5.5 Vite 5 Tailwind 3.4 TanStack Query 5 Radix UI Stripe MySQL Playwright PHPStan

Zero runtime Composer dependencies — the payment integration is written against the HTTP API directly rather than vendoring an SDK into someone else's WordPress.

All projects