SPEC: Agend WordPress Integration Plugin (thin, no-IP)
The missing glue between
@agend/elementor-widgetsand 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)
| Piece | Location | State |
|---|---|---|
| Widget source | packages/@agend/elementor-widgets/ (v0.1.0, Vite/React) | ✅ built |
| Widget entries | src/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 bundle | packages/wordpress/agend-integration/assets/js/widgets/agend-widgets.js (+ CSS) exposing the AgendWidgets global | ✅ staged |
| Runtime contract | src/lib/api-client.ts reads window.agendConfig { apiUrl, apiKey, accountId, ssoToken? }; sends X-API-Key + X-Account-Id (+ Authorization: Bearer <ssoToken>) | ✅ |
| Mounting | src/mount.ts mounts React onto [data-widget] nodes with per-instance data-config JSON | ✅ |
| WP plugin | packages/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/.deletescopes at save time via a key-introspection call); - surface the per-key
allowed_originsCORS allowlist (already supported byapi_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
- Fresh WordPress + this plugin + a browse-scoped
ag_test_key → the event-list widget renders live data with no code edits. - A key with write scopes is rejected at settings save with an explanatory message.
- Same page works with Elementor widget, shortcode and block variants.
- Logged-in linked member sees the member-portal widget authenticated (R6); anonymous visitor sees the logged-out state.
- 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-detailwidget pair should render any registry-defined type. Thedata-widget+data-configcontract already accommodates dynamic types — no action now, but avoid design choices that hard-code the widget catalogue.