← Back to index | ← 08b Platform analytics | 10 Deployment →

How MMC's WordPress site and the CoachPilot gateway talk to each other. Two directions, two secret patterns, one shared protocol.


The two bridge directions

                                                                  
   ┌──────────────────────────┐         ┌──────────────────────────┐
   │   MMC WordPress          │         │   CoachPilot Gateway     │
   │   mindfulmoneycoaching   │         │   api.coachpilot.ch      │
   │   .online                │         │                          │
   │                          │         │                          │
   │                          │ ──→ A   │  (1) Calibrate scores    │
   │                          │ ──→ B   │  (2) Sync MMC data IN    │
   │                          │ ←── C   │  (3) Webhook OUT to MMC  │
   │                          │         │                          │
   └──────────────────────────┘         └──────────────────────────┘
DirectionEndpoint patternSecret usedCaller
A: WP → Gateway (Likert calibration)POST /api/v1/assess/framework/.../calibrate— (public-readable)WP archetype-scores.php
B: Gateway → WP (bio + Money Quiz pulls)GET /wp-json/coachpilot/v1/<endpoint>X-MMC-Bridge-SecretGateway _mmc_bridge_get
C: Quiz app → WP (archetype-scores webhook)POST /wp-json/sg-course/v1/archetype-scoresX-Deal-Secretmoneyquiz-app fireArchetypeWebhook

Direction A — Likert calibration (C5, 2026-05-19)

When MMC's WordPress Money Quiz (Likert) completes, the WP receiver POSTs raw scores to the gateway to apply Tier-2 calibration before storing in mq_* tables.

WP-side caller

archetype-scores.php::mmc_archetype_scores_calibrate_via_gateway:

function mmc_archetype_scores_calibrate_via_gateway( array $scores, string $tenant = 'mmc' ): ?array {
    $base      = get_option( 'mmc_calibrate_gateway_url', 'https://api.coachpilot.ch' );
    $framework = get_option( 'mmc_calibrate_framework', 'money_archetypes' );
    $tenant    = get_option( 'mmc_calibrate_tenant', $tenant );

    // 0..1 → 0..100
    $scaled = array_map( fn($v) => round($v * 100, 4), $scores );

    $url = rtrim($base, '/') . '/api/v1/assess/framework/' . rawurlencode($framework) . '/calibrate?client=' . rawurlencode($tenant);
    $response = wp_remote_post( $url, [...] );

    // Failure: return null → caller keeps raw scores
    // Success: return calibrated 0..1 scores
}

Scale conversion: WP stores 0..1; gateway uses 0..100. Convert at the boundary.

Failure-safe: 5 s timeout, returns null on any non-200, caller falls through to raw scores. The quiz always completes.

Gateway-side endpoint

POST /api/v1/assess/framework/{framework_id}/calibrate?client=<tenant> — defined in routers/assess_framework.py. Public (no auth).

Implementation:

  1. Load resolved framework for the tenant (cached by Postgres view materialisation TBD; today every call hits DB).
  2. Call scoring_calibration.py::apply_archetype_calibration.
  3. Return {scores, applied_rules, framework}.

See 06 Scoring + calibration for the rule engine.

Verification

curl -X POST "https://api.coachpilot.ch/api/v1/assess/framework/money_archetypes/calibrate?client=mmc" \
  -H "Content-Type: application/json" \
  -d '{"scores":{"victim":65,"martyr":22}}'
# → {"scores":{"victim":65,"martyr":32},"applied_rules":["victim_martyr_cooccurrence"],"framework":"money_archetypes"}

Production smoke 2026-05-19: raw martyr 0.22 → calibrated 0.32 stored in WP transient. End-to-end verified.


Direction B — Bio + Money Quiz data IN (gateway ← WP)

The CoachPilot dashboard's Personas system needs to read MMC's existing data:

  • Student bio from the SG Course workbook (topic 20080 responses).
  • Historical Money Quiz scores by email.

Gateway calls MMC WP via _mmc_bridge_get(path, params).

Auth

Pre-shared secret stored in three places:

  • WP option mmc_bridge_integration_secret
  • k8s coachpilot-secrets.MMC_BRIDGE_INTEGRATION_SECRET
  • Gateway env var MMC_BRIDGE_INTEGRATION_SECRET

Header on every gateway → WP call: X-MMC-Bridge-Secret: <secret>.

WP gates via mmc_bridge_integration_auth permission_callback:

function mmc_bridge_integration_auth($request) {
    $expected = get_option('mmc_bridge_integration_secret');
    $provided = $request->get_header('X-MMC-Bridge-Secret');
    return $expected && $provided && hash_equals($expected, $provided);
}

hash_equals is constant-time — protects against timing attacks. Don't replace with ==.

Endpoints exposed by MMC

PathSourcePurpose
/wp-json/coachpilot/v1/sg-course-bio?email=MMC mu-plugin integrations-coachpilot.phpReturns bio data from _sg_workbook_responses user meta for topic 20080
/wp-json/coachpilot/v1/money-quiz?email=SameReturns historical Money Quiz scores from mq_taken + mq_results

See reference_mmc_sg_course_workbook.md for the bio source structure.

Gateway-side helper

_mmc_bridge_get(path, params) in api-gateway/registry/framework_config.py (or wherever it now lives). Returns parsed JSON or raises on 503/502/404. Adding a new integration:

  1. WP: register_rest_route with mmc_bridge_integration_auth permission_callback.
  2. Gateway: helper using _mmc_bridge_get.
  3. Dashboard: assess.fetchXFromMmc(...) API client helper.

Secret rotation

24 h dual-validate window:

  1. Update WP option mmc_bridge_integration_secret to new value.
  2. Update k8s secret MMC_BRIDGE_INTEGRATION_SECRET to new value.
  3. Rolling restart gateway pods (kubectl rollout restart deployment/coachpilot-gateway).
  4. After 24 h with no errors, remove old secret from any temporary dual-accept code (if added).

Direction C — Archetype-scores webhook (WP ← quiz app)

The original direction this all started from. Every quiz completion (any format, any tenant with archetype_scores_webhook set) POSTs to the tenant's CRM receiver. For MMC, that receiver is WP.

Caller

moneyquiz-app/src/lib/tenant-config.ts::fireArchetypeWebhook:

export async function fireArchetypeWebhook(
  brand: TenantBrand | null,
  payload: ArchetypeWebhookPayload,
): Promise<void> {
  if (!brand?.archetype_scores_webhook) return;
  if (!payload.userEmail) return;
  // ... normalise scores 0..100 → 0..1
  const headers: Record<string, string> = { "Content-Type": "application/json" };
  if (brand.archetype_scores_secret) {
    headers["X-Deal-Secret"] = brand.archetype_scores_secret;
  }
  await fetch(brand.archetype_scores_webhook, { method: "POST", headers, body: ... });
}

Fire-and-forget: errors logged, never thrown. The quiz always completes.

Called from /api/quiz/results (Mirror + Game) and /api/deal/results (Deal + Realm).

Receiver

MMC archetype-scores.php::sg_rest_receive_archetype_scores:

Receives {email, name, scores, dominant, secondary, source, response_format, played_at, ...}. Validates X-Deal-Secret. Sanitizes scores (legacy → production key map). C5 calibration. Looks up WP user. Writes to legacy mq_* tables. Schedules emails. Stamps invite_token completion. Returns {stored, user_id, snapshot}.

Payload shape (contract)

{
  "email": "player@example.com",
  "name": "Jane Doe",
  "scores": {
    "hero":     0.40, "artist": 0.35, "ruler":  0.20, "innocent": 0.25,
    "maverick": 0.30, "victim": 0.65, "martyr": 0.32, "magician": 0.15
  },
  "dominant":      "victim",
  "secondary":     "hero",
  "source":        "money-mirror",
  "response_format": "qa",
  "quiz_length":   "",
  "urgency":       "guide",
  "combos":        [],
  "played_at":     "2026-05-19T08:00:00Z"
}

This contract is stable. Any new tenant CRM receiver MUST accept this shape (or document a different one in their tenant_brand metadata).


The auto-pages mu-plugin

MMC/mu-plugins/mmc-moneyquiz-bridge/auto-pages.php creates and renders the four iframe pages on MMC's WordPress:

const MMC_BRIDGE_AUTO_PAGES = [
    'money-mirror' => 'Money Mirror',
    'money-game'   => 'Money Game',
    'money-deal'   => 'Money Deal',
    'money-realm'  => 'Money Realm',
];

// On admin_init (idempotent via option flag):
foreach (MMC_BRIDGE_AUTO_PAGES as $slug => $title) {
    if (! get_page_by_path($slug)) {
        wp_insert_post([
            'post_title'  => $title,
            'post_name'   => $slug,
            'post_status' => 'publish',
            'post_type'   => 'page',
        ]);
    }
}

// On the_content filter for those slugs: render iframe
add_filter('the_content', function($content) {
    if (is_page($auto_slugs)) {
        $tenant = 'mmc';
        $bare = str_replace('money-', '', get_post()->post_name);
        $src = "https://quiz.thesynergygroup.ch/{$bare}?tenant={$tenant}";
        return '<iframe src="' . esc_url($src) . '" style="..."></iframe>';
    }
    return $content;
});

Page IDs created on first deploy: 21692–21695.

This is the canonical pattern for adding white-label iframe tenants. Future tenants get their own mu-plugin (or shared with namespacing).


File deploy pattern for MMC

MMC WP files deploy via SFTP to Hostinger:

  • Host: coachpilot.ch:65002
  • User: u146818668
  • Theme path: /home/u146818668/domains/mindfulmoneycoaching.online/public_html/wp-content/themes/coachpilot-theme-v2/
  • Mu-plugins: /home/u146818668/domains/mindfulmoneycoaching.online/public_html/wp-content/mu-plugins/
  • SG Course Engine: /home/u146818668/domains/mindfulmoneycoaching.online/public_html/wp-content/mu-plugins/sg-course-engine/

After SFTP upload:

  1. Touch the file (force timestamp update).
  2. Invalidate OPcache: opcache_invalidate('/path/to/file.php', true) via wp eval.
  3. wp cache flush + purge LiteSpeed + transients.

See 11 Operations §MMC deploy for the full recipe.


Secret store

SecretWhere storedPurpose
MMC_BRIDGE_INTEGRATION_SECRETk8s + WP optionGateway → WP integration auth
archetype_scores_secret (per tenant)tenant_brand column + WP sg_deal_webhook_secret optionQuiz app → CRM webhook auth
BEARER_MMCk8s coachpilot-secretsMMC tenant static bearer for gateway-internal calls
Hostinger SSH passwordVault secret/passwords/hostinger_sshSFTP for MMC + TSG + HSP sites (shared)

Never hardcode secrets in source. The mmc-deal-webhook-2026 literal that lingers in some test fixtures is the DEFAULT VALUE — production values are in the secret store.


Common pitfalls

  1. Sending raw scores at 0..1 to the calibrate endpoint — calibration rules use 0..100 thresholds. Always × 100 before sending; ÷ 100 after.
  2. Sending ?client= without a corresponding tenant row — gateway returns 404. Add the tenant first.
  3. Forgetting to bump WP cache after deploying archetype-scores.php. OPcache will serve stale bytecode.
  4. Hardcoded secret in webhook secret field — TSG/coachpilot use NULL (no webhook). Don't fill these in just because there's a field.

Next

10 Deployment — image taxonomy, kubectl recipes, rollout patterns.