Developers
Companion plugin
The WordPress side of the product — the Companion plugin's structure, build, settings app, REST surface, custom tables, Store API extensions, JWT bridge, security model and AI abilities, and how to extend it.
The Companion plugin (Retail Store Companion) is the WordPress and WooCommerce backend for the storefront. WooCommerce’s own Store API and REST API still serve products, cart, checkout, orders, coupons and shipping. The Companion adds what they don’t: storefront sign-in (its own JWTs), account extras, the storefront’s content and look settings, back-in-stock alerts, wishlist and compare, review media, embedded Stripe and PayPal hardening, extra fields on Store API responses, and AI-agent abilities. It prints no storefront HTML itself.
| Plugin header name | “Retail Store Compaion” (the slug, text domain, namespace and constants all use compaion) |
| Main file | retail-store-compaion.php |
Version on develop |
0.4.3 |
| PHP namespace | WeLabs\RetailStoreCompaion\ (PSR-4, mapped to includes/) |
| REST namespace | retail-store-companion/v1 |
| Requires | WooCommerce (Requires Plugins: woocommerce), PHP 8.0 or newer. WordPress 6.9 or newer only for the Abilities API. |
| Runtime Composer dependency | firebase/php-jwt |
| wp-admin menu | StoreFront → Settings (one React screen with a rail of tabs), Subscribers, Stock Notifications, Contact Entries |
How it boots
Section titled “How it boots”retail-store-compaion.phpdefines the file constants, requires the Composer autoloader and callswelabs_retail_store_compaion(), which returns the singleton.includes/RetailStoreCompaion.phpis the bootstrap. Onplugins_loadedit runsinit_plugin()(includes and hooks); classes are instantiated oninitat priority 4 (init_classes()); sub-objects live in a container reached through magic__get().- REST controllers register on
rest_api_initinRetailStoreCompaion::register_rest_route(). A few register only when their dependency exists: brand facets and the Address Book need WooCommerce, and/contactneeds Contact Form 7. - Most WooCommerce-dependent setup sits inside a
has_woocommerce()check and no-ops without it.
Directory map
Section titled “Directory map”- retail-store-compaion.php plugin header and bootstrap call
Directoryincludes/ PHP source, PSR-4
WeLabs\RetailStoreCompaion\- RetailStoreCompaion.php the bootstrap singleton
DirectoryAbilities/ WordPress Abilities API registrations (settings and item SEO)
- …
DirectoryAccount/ the synced compare list
- …
DirectoryAddressBook/ the Address Book service, WooCommerce field mirroring, privacy hooks
- …
DirectoryAdmin/ menu, list screens, product and term screen fields, SEO box, notices
- …
DirectoryAuth/ JWT, refresh tokens, blacklist, email tokens, social verification, the WooCommerce REST bridge
- …
DirectoryBrands/ brand primary category
- …
DirectoryCategories/ trending categories
- …
DirectoryContact/ Contact Form 7 gateway and contact entries
- …
DirectoryEmails/ WooCommerce email classes
- …
DirectoryNewsletter/ subscribers table and the Brevo provider
- …
DirectoryPayment/ payment config, Stripe and PayPal seams, checkout guards, rate limits, shopper address
- …
DirectoryPreview/ Product Preview tokens
- …
DirectoryProducts/ videos, trust badges, Payment and Security, primary category, review media and threads
- …
DirectoryREST/ the REST controllers (24, on a shared
AbstractController)- …
DirectorySeo/ SEO fields, resolver, Yoast and Rank Math bridges, mirroring, sync job
- …
DirectoryStockNotifications/ back-in-stock sign-ups and the sending pipeline
- …
DirectoryStoreApi/ extensions to WooCommerce Store API responses, WP REST Cache integration
- …
DirectoryStorefront/ settings classes per section, header builder, the section catalog
- …
DirectoryWishlist/ wishlist tables and service
- …
- Settings.php site settings and their
RSC_*constant overrides - RestGuardSettings.php API Access: protected namespaces and public routes
- Turnstile.php Cloudflare Turnstile verification
- SocialCredentials.php Google and Facebook credentials
Directorysrc/
Directorysettings/ the React settings app (one component per tab)
- …
Directoryproduct-settings/ the trust badge and Payment and Security editors on product and category screens
- …
Directoryassets/ admin and public assets;
assets/build/is generated- …
Directorytemplates/ admin and email templates
- …
Directorytests/
Directoryphp/ PHPUnit:
src/mirrorsincludes/,integration/drives the real Store API- …
Directoryhttp/ REST request samples
- …
DirectoryArchitectur-docs/ specs, as-built contracts and the plugin’s own decisions
- …
Directorydocs/
Directoryapi/ REST reference
- …
Directorybin/
- build.sh builds the release zip
vendor/ and assets/build/ are not committed, so a raw checkout fatals or shows a blank settings screen until it is built.
composer install # development, with PHPCS and PHPUnitcomposer install --optimize-autoloader --no-dev -q # production dependencies onlynpm install && npm run build # the admin React apps, Node 22 (.nvmrc)bin/build.sh # build/retail-store-compaion-<version>.zipThe admin apps use @wordpress/scripts. webpack.config.js runs one compilation per app, settings and product-settings, each
into its own assets/build/ directory, so shared stylesheets load with both. bin/build.sh reads the version from the plugin header,
refuses a Node older than .nvmrc, runs npm ci and npm run build, copies the plugin, installs production Composer
dependencies and zips the result.
The settings app and its REST
Section titled “The settings app and its REST”StoreFront → Settings is one React app (src/settings/App.js). It loads everything from GET /admin/settings and saves with
POST /admin/settings, both requiring manage_options (wp-admin cookie plus X-WP-Nonce). Secrets come back as "" with a
has_value flag and are never echoed.
Content and look settings are sections in one catalog, Storefront\Sections\SettingsSections. Each Section knows its defaults,
its current values, how to save a submitted copy (through the section’s sanitizer into its option, plus any cache flush), and its JSON
schema. POST /admin/settings and the AI abilities both save through the same Section, so a person and an agent can never store a
section differently. Secrets and security switches (sign-in and Turnstile keys, API access, sessions, payment security), custom
scripts and the newsletter provider are deliberately outside the catalog and are saved by the settings controller only.
The storefront reads the public side in one request, GET /storefront/settings, which returns colours (light and dark), the
resolved main header, footer, product page, reviews, wishlist, compare, stock alerts, contact, FAQ, scripts, Policy Page picks,
SEO, the store time zone and more, with Cache-Control: public, max-age=300, stale-while-revalidate=600.
Custom tables
Section titled “Custom tables”Created on activation and re-checked on every init (priority 3) against a schema-version option, so a deploy that skips activation
still gets them. Deactivation does not drop them, and there is no uninstall routine.
| Table | Purpose |
|---|---|
wp_rsc_wishlists, wp_rsc_wishlist_products |
One wishlist per customer (the shape allows several later) and its products |
wp_rsc_newsletter_subscribers |
Storefront newsletter sign-ups; backs StoreFront → Subscribers |
wp_rsc_stock_notifications, wp_rsc_stock_notificationmeta |
Back-in-stock sign-ups; laid out like WooCommerce’s planned table so a hand-off is a copy |
wp_rsc_contact_entries |
Every storefront contact message; backs StoreFront → Contact Entries |
Store API extensions
Section titled “Store API extensions”Most extensions hook rest_request_after_callbacks and add a top-level field to matching WooCommerce Store API responses. Only the
free-shipping meter uses WooCommerce’s official extension API, so it is the only field under extensions. The summary is in the
REST API reference: colour swatches, sale end dates, upsell and cross-sell ids,
videos, trust badges, the Payment and Security card, SEO fields, brand and category extras, review media, the free-shipping
progress on the cart, and the payment method on the order. With the free WP REST Cache plugin active, the Companion lets it cache
catalog reads and flushes them on stock, variation, review, settings and SEO changes; it never caches cart, checkout, order,
payment or any request that carries Authorization.
Identity and the WooCommerce JWT bridge
Section titled “Identity and the WooCommerce JWT bridge”The Companion issues its own tokens: a 15-minute HS256 JWT signed with RSC_JWT_SECRET from wp-config.php, and an opaque 30-day
refresh token that rotates on every use, with reuse detection that revokes every session and emails a security alert.
Auth/RestAuthBridge.php lets the storefront use that JWT against WooCommerce itself:
- On
determine_current_userit sets the user from a valid Bearer JWT for requests underwc/v3,wc/storeor any namespace configured as protected. - On
rest_authentication_errorsit hard-gates the protected namespaces (defaultwc/v3): a request needs a valid JWT, or a logged-in cookie plus a validwp_restnonce. WooCommerce consumer keys and nonce-less cookies get401 rsc_unauthorized. A public allowlist, checked first, can open specific routes. WooCommercePermissionsonly ever adds access: a customer may read their own orders and refunds, and read and edit their own customer record (email and password are stripped from that edit).
The protected namespaces and the allowlist are edited on the API Access tab and filterable (rsc_protected_rest_namespaces,
rsc_public_rest_routes).
Security model in brief
Section titled “Security model in brief”- Secrets off the database.
RSC_JWT_SECRETandRSC_STOREFRONT_SHARED_SECRETlive inwp-config.phponly. Social and Turnstile credentials may be set in the admin or pinned withRSC_*constants, which then win. - Instant revocation. A per-user generation counter (bumped on logout-all, password change or reset, token reuse, admin revoke) plus a per-token blacklist for single logout, applied on the bridge too.
- Single-use email tokens. Verify email (24 hours), password reset (1 hour), stock-alert verify (24 hours); only hashes are stored.
- Social sign-in fails closed. Google ID tokens are verified locally against Google’s keys and are single-use; Facebook tokens
are checked with
debug_tokenandappsecret_proof. Staff accounts are refused; linking Facebook to an existing email needs that account’s password. - Payments. Public payment config only, never cached. Order-key or Cart-Token authorisation on payment routes, amount guards, one checkout per cart, generic decline messages, and atomic rate limits keyed by cart and by the HMAC-signed Shopper address.
- Abuse controls. Cloudflare Turnstile on reviews, guest replies, contact and guest stock alerts, and per-email and per-IP throttles. Turnstile fails open when it is not configured and closed when Cloudflare cannot be reached.
Abilities and MCP
Section titled “Abilities and MCP”On WordPress 6.9 or newer the plugin registers abilities with the Abilities API. Each has show_in_rest: true and
mcp.public: true, so an MCP adapter (WooCommerce’s bundled one or the standalone mcp-adapter) exposes them to AI agents.
| Category | Abilities | Permission |
|---|---|---|
welabs-store-storefront |
welabs-store/list-storefront-settings-sections, plus get-{section}-settings and update-{section}-settings for each of the 21 catalog sections (43 in all) |
manage_options |
welabs-store-seo |
welabs-store/find-items-seo, get-item-seo, update-item-seo |
edit_post or edit_term on the item |
An update is partial: named fields merge and lists are replaced whole. It is validated against the section’s schema, image ids must
be Media Library images, and it is saved through the same Section as the settings screen. Item SEO updates write to the SEO source
in charge (Yoast SEO or Rank Math when active, else the plugin’s own copy). Contract: Architectur-docs/ABILITIES.md. Setup for store
owners: AI agents.
Extending the plugin
Section titled “Extending the plugin”Add a settings section
Section titled “Add a settings section”-
Write the failing test first, in the mirror path under
tests/php/src/. The plugin is test-driven: every class inincludes/has a mirror*Testclass. -
Add a settings class in
includes/Storefront/withdefaults(),all(),sanitize()andschema(). The schema must describe every field at every depth; use the helpers inStorefront\Sections\Schema. Sanitise per field (sanitize_text_field,wp_kses_postwhere HTML is allowed,esc_url_rawfor links). -
Register it in
SettingsSections::build()withSection::option( $key, $label, $description, $store, $option ), chainingafter_save()for a cache flush orreading()when the editor’s values differ fromall(). The settings save and aget-/update-ability pair now exist for it. Itsschema()is tested by the abilities tests intests/php/src/Abilities/, the one exception to the mirror rule. -
Add the tab to the React app: a component in
src/settings/components/, wired intoApp.jsand the tab rail. Runnpm run build. -
If the storefront must read it, add it to the public
/storefront/settingspayload, then map it in the storefront: the WooCommerce adapter’s theme mapping and the canonicalStorefrontTheme, with a contract-suite case.
Add a REST endpoint
Section titled “Add a REST endpoint”-
Write the test first: assert the route is registered, and assert the status code and response shape (
code,message) for success and every refusal. -
Add a controller in
includes/REST/extendingAbstractController, with its$rest_base. Keep request validation in the routeargs. Usecheck_authentication()as thepermission_callbackfor JWT routes,__return_trueonly for genuinely public reads, and a capability check for admin routes. -
Add the controller to the list in
RetailStoreCompaion::register_rest_route(). -
If it is a public, cacheable read, decide whether it belongs on WP REST Cache’s allow list (
StoreApi/RestCacheIntegration.php); anything per-user or payment-related never does. -
Document it in
Architectur-docs/API-REFERENCE.mdand, on the storefront side, call it from the WooCommerce adapter, never from a component.
<?php// Illustrative: the shape of a public, read-only controller.namespace WeLabs\RetailStoreCompaion\REST;
use WP_REST_Request;use WP_REST_Response;use WP_REST_Server;
class ExampleController extends AbstractController {
protected $rest_base = 'example';
public function register_routes() { register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => '__return_true', 'args' => array( 'id' => array( 'type' => 'integer', 'required' => true ), ), ), ) ); }
public function get_item( $request ) { return new WP_REST_Response( array( 'id' => (int) $request['id'] ), 200 ); }}Coding standards
Section titled “Coding standards”- PHPCS with
phpcs.xml: WordPress VIP Go standards plus PHPCompatibilityWP for PHP 8.0 to 8.3, in strict mode (warnings fail too). CI checks the changed PHP files on every pull request; runcomposer phpcsbefore pushing. - Internationalise every user-facing string with the
retail-store-compaiontext domain. - Security checklist (from the pull request template): nonces and CSRF, sanitise input, validate, escape output, capability
checks,
$wpdb->prepare()for SQL. - Match the file’s indentation; WordPress coding standards throughout.
- TDD, no exceptions: a class or method changes only with its mirror test. When a test exposes a gap, fix the code, not the assertion.