Architecture & engineering

SPEC: Agend WordPress Integration Plugin (thin, no-IP)

The missing glue between @agend/elementor-widgets and a client WordPress site. Promised as milestone BOA-08 on the AHRI partner page (ahri-build-on-agend-overview.html). Status: DRAFT for pickup · 2026-07-07 · Cross-refs: .docs/reports/lms-embed-...-wordpress-contract.md, .docs/reports/wordpress-idp-integration-profile.md

Problem

The React widget library is built and compiled, but nothing installs it on a WordPress site. Today a client would have to hand-place <div data-widget=...> markup and author window.agendConfig themselves; api-client.ts throws "Ensure the Agend WordPress plugin is configured correctly" referencing a plugin that does not exist (packages/wordpress-integration/ is an empty stub).

What already exists (verified 2026-07-07)

PieceLocationState
Widget sourcepackages/@agend/elementor-widgets/ (v0.1.0, Vite/React)✅ built
Widget entriessrc/entries/{directory,events,jobs,lms,loop,member}.ts — job board/search, course catalogue, learning paths, CPD balance, directory search, featured listings, category grid, event list/calendar/featured, community feed, announcements, member portal, membership renewal
Compiled bundlepackages/wordpress/agend-integration/assets/js/widgets/agend-widgets.js (+ CSS) exposing the AgendWidgets global✅ staged
Runtime contractsrc/lib/api-client.ts reads window.agendConfig { apiUrl, apiKey, accountId, ssoToken? }; sends X-API-Key + X-Account-Id (+ Authorization: Bearer <ssoToken>)
Mountingsrc/mount.ts mounts React onto [data-widget] nodes with per-instance data-config JSON
WP pluginpackages/wordpress-integration/❌ empty stub — this spec

Goals / non-goals

Goals: a freely distributable WordPress plugin containing zero proprietary logic — configuration, asset loading, widget registration, SSO handoff. Nothing in it is worth protecting; that is the point (see the Build on Agend strategy: IP stays on Agend infrastructure).

Non-goals: repointing the legacy Membership Kiosk PHP widgets (they retire per the write-back phases); any direct Upbeat access; bundling API keys with manage scopes.

Requirements

R1 · Settings page

Admin screen: API base URL (default https://api.agend.com.au), API key, Account ID, environment badge (from ag_test_/ag_live_ prefix). "Test connection" button calling /v1/health (note: currently returns 401 — hardening finding F5; test via a scoped endpoint until fixed).

R2 · Asset delivery

Enqueue the compiled bundle + CSS, versioned; only on pages that contain an Agend widget (render-time detection), not site-wide.

R3 · Config injection — with the browser-key security model

Inject window.agendConfig before the bundle. The API key in this config is visible to every site visitor. The plugin MUST therefore:

  • accept only browse-scoped keys (reject *.manage/.create/.delete scopes at save time via a key-introspection call);
  • surface the per-key allowed_origins CORS allowlist (already supported by api_keys.allowed_origins[]) and instruct the admin to lock the key to the site origin;
  • never inject config into feeds/REST responses — page render only.

R4 · Elementor registration

One Elementor widget per entry (category "Agend"), each rendering the data-widget div and exposing that widget's data-config options as Elementor controls. Detect Elementor's absence and degrade to R5.

R5 · Shortcode + block fallback

[agend_widget type="event-list" config="..."] and a matching Gutenberg block, so non-Elementor sites (or non-Elementor pages) work identically.

R6 · Logged-in member context (SSO token)

For member-scoped widgets (member portal, CPD balance, membership renewal): if the WP user is linked to an Agend identity, mint/fetch a short-lived ssoToken server-side (per wordpress-idp-integration-profile.md) and include it in agendConfig. Anonymous visitors get the config without a token; member widgets render their logged-out state.

R7 · Distribution

Installable zip built in CI from packages/wordpress-integration/; semantic version pinned to the widget bundle version it ships; update path via standard WP plugin update mechanics (own update endpoint, no wordpress.org dependency).

Acceptance criteria

  1. Fresh WordPress + this plugin + a browse-scoped ag_test_ key → the event-list widget renders live data with no code edits.
  2. A key with write scopes is rejected at settings save with an explanatory message.
  3. Same page works with Elementor widget, shortcode and block variants.
  4. Logged-in linked member sees the member-portal widget authenticated (R6); anonymous visitor sees the logged-out state.
  5. No request from the plugin ever goes anywhere but the configured apiUrl.

Open questions

  • Key introspection endpoint for R3 scope-checking — exists? If not, smallest addition to apps/api.
  • Where the WP-user ↔ Agend-contact link is mastered for R6 (SSO profile doc leans account-level SAML; widgets need per-user).
  • Whether the AHRI pilot ships R4 (Elementor) first and defers R5, or both.

Future scope (do not preclude)

  • Generic object widgets: once custom objects ship (ADR-001), a generic agend-object-list / agend-object-detail widget pair should render any registry-defined type. The data-widget + data-config contract already accommodates dynamic types — no action now, but avoid design choices that hard-code the widget catalogue.