Skip to content
weLabsweLabsStorefront Playbook
Live demoQuickstart

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
  • retail-store-compaion.php defines the file constants, requires the Composer autoloader and calls welabs_retail_store_compaion(), which returns the singleton.
  • includes/RetailStoreCompaion.php is the bootstrap. On plugins_loaded it runs init_plugin() (includes and hooks); classes are instantiated on init at priority 4 (init_classes()); sub-objects live in a container reached through magic __get().
  • REST controllers register on rest_api_init in RetailStoreCompaion::register_rest_route(). A few register only when their dependency exists: brand facets and the Address Book need WooCommerce, and /contact needs Contact Form 7.
  • Most WooCommerce-dependent setup sits inside a has_woocommerce() check and no-ops without it.
  • 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/ mirrors includes/, 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.

Terminal window
composer install # development, with PHPCS and PHPUnit
composer install --optimize-autoloader --no-dev -q # production dependencies only
npm install && npm run build # the admin React apps, Node 22 (.nvmrc)
bin/build.sh # build/retail-store-compaion-<version>.zip

The 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.

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.

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

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.

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_user it sets the user from a valid Bearer JWT for requests under wc/v3, wc/store or any namespace configured as protected.
  • On rest_authentication_errors it hard-gates the protected namespaces (default wc/v3): a request needs a valid JWT, or a logged-in cookie plus a valid wp_rest nonce. WooCommerce consumer keys and nonce-less cookies get 401 rsc_unauthorized. A public allowlist, checked first, can open specific routes.
  • WooCommercePermissions only 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).

  • Secrets off the database. RSC_JWT_SECRET and RSC_STOREFRONT_SHARED_SECRET live in wp-config.php only. Social and Turnstile credentials may be set in the admin or pinned with RSC_* 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_token and appsecret_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.

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.

  1. Write the failing test first, in the mirror path under tests/php/src/. The plugin is test-driven: every class in includes/ has a mirror *Test class.

  2. Add a settings class in includes/Storefront/ with defaults(), all(), sanitize() and schema(). The schema must describe every field at every depth; use the helpers in Storefront\Sections\Schema. Sanitise per field (sanitize_text_field, wp_kses_post where HTML is allowed, esc_url_raw for links).

  3. Register it in SettingsSections::build() with Section::option( $key, $label, $description, $store, $option ), chaining after_save() for a cache flush or reading() when the editor’s values differ from all(). The settings save and a get-/update- ability pair now exist for it. Its schema() is tested by the abilities tests in tests/php/src/Abilities/, the one exception to the mirror rule.

  4. Add the tab to the React app: a component in src/settings/components/, wired into App.js and the tab rail. Run npm run build.

  5. If the storefront must read it, add it to the public /storefront/settings payload, then map it in the storefront: the WooCommerce adapter’s theme mapping and the canonical StorefrontTheme, with a contract-suite case.

  1. Write the test first: assert the route is registered, and assert the status code and response shape (code, message) for success and every refusal.

  2. Add a controller in includes/REST/ extending AbstractController, with its $rest_base. Keep request validation in the route args. Use check_authentication() as the permission_callback for JWT routes, __return_true only for genuinely public reads, and a capability check for admin routes.

  3. Add the controller to the list in RetailStoreCompaion::register_rest_route().

  4. 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.

  5. Document it in Architectur-docs/API-REFERENCE.md and, 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 );
}
}
  • 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; run composer phpcs before pushing.
  • Internationalise every user-facing string with the retail-store-compaion text 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.
Storefront Playbook · Built by weLabsFeaturesFAQTalk to us