Cross-Site User Sync (WPRUS)
Login and user data sync between baseworks.com and practice.baseworks.com is handled by the WP Remote Users Sync plugin (installed on both sites). For the broader tag/access sync layer, see Cross-Site Integration & Automation.
How It Works
Section titled “How It Works”WPRUS matches users across sites by user_login (username) only — not by email or user ID. When a user logs into Site A:
- WPRUS enqueues an async login action and renews an auth token with Site B
- The user’s browser is redirected to Site B’s WPRUS endpoint with a signed payload
- Site B verifies the token, looks up the user by username, and logs them in
User IDs can differ completely between sites — WPRUS doesn’t care.
Active Sync Actions
Section titled “Active Sync Actions”Both directions (baseworks.com ↔ practice.baseworks.com):
| Action | Status | Notes |
|---|---|---|
| login | ✅ on | Core sync — logs user into both sites on login |
| create | ✅ on | Creates user on remote site if they don’t exist |
| update | ✅ on | Syncs profile changes |
| password | ✅ on | Syncs password changes |
| meta | ✅ on | Syncs user meta fields |
| logout | ❌ off | |
| delete | ❌ off | Disabled 2026-03-27 — see below |
| role | ❌ off |
Delete sync is permanently disabled. Account deletion is a rare, manual operation (~3 times in site history, based on Asia’s memory). Leaving delete sync active risks cascading deletions: deleting a user on one site would immediately delete their counterpart on the other, including admin accounts.
Admin Account Protection — Two-Layer Defence (updated 2026-04-17)
Section titled “Admin Account Protection — Two-Layer Defence (updated 2026-04-17)”Admin accounts on both sites are now protected by two independent mechanisms. Do not remove either without understanding both.
Layer 1 — WPRUS role filter (outgoing_roles)
Section titled “Layer 1 — WPRUS role filter (outgoing_roles)”Configured in the WPRUS plugin settings on both sites. Only users with the following roles trigger outgoing sync events:
baseworks.com outgoing_roles: author, contributor, customer, editor, subscriber, shop_manager
practice.baseworks.com outgoing_roles: editor, author, contributor, subscriber, customer, shop_manager, translator, group_leader, app_user, bbp_keymaster, bbp_moderator, bbp_participant, bbp_spectator, bbp_blocked
administrator is absent from both lists. Admin profile changes, new admin accounts, and admin logins are never pushed to the other site.
Maintenance note: If a new custom role is added to either site and should sync, add it to that site’s outgoing_roles list in WPRUS Settings → the peer site row.
Layer 2 — MU plugin: bw-protect-admin-from-wprus.php
Section titled “Layer 2 — MU plugin: bw-protect-admin-from-wprus.php”Deployed on both sites. Hooks into wp_pre_insert_user_data (fires inside wp_insert_user() before the DB write) and restores the original email, password, display name, and URL if the target user is an administrator and the caller is not a logged-in admin.
Why this is needed in addition to Layer 1: WPRUS incoming_roles only guards the role-sync action — it does NOT filter create or update actions. Those match users by username and call wp_insert_user() with no check on the local user’s role. A non-admin user on the remote site with the same username as a local admin can overwrite the admin’s data via sync. Layer 2 blocks that path at the WordPress core level, regardless of WPRUS configuration.
A logged-in administrator editing their own profile bypasses this filter (as intended).
Current admin usernames
Section titled “Current admin usernames”| Person | baseworks.com | practice.baseworks.com | Shared email |
|---|---|---|---|
| Patrick | basework (admin) | patrick (admin) | pat@baseworks.com |
| Asia | Pandasia (admin) | asia (admin) | asia@baseworks.com |
Usernames are intentionally mismatched (different between the two sites). This was the original mechanism relied on to prevent admin sync — but it created a vulnerability: any public user registering on either site with a username matching an admin on the other site could overwrite that admin’s data. This was discovered 2026-04-17 when Asia registered asia@cevaco.co on baseworks.com (username asia) and was locked out of practice.baseworks.com admin.
Username alignment (making them the same across both sites) is under discussion. With Layers 1 and 2 in place, it is now a cleanliness decision rather than a security concern — the protection is role-based and does not depend on username mismatch.
History of the admin isolation approach
Section titled “History of the admin isolation approach”The username mismatch was first accidental (Patrick), then intentional (Asia, after the 2026-03-27 investigation). That investigation found Asia’s asia account on practice was being synced to a subscriber asia mirror on baseworks.com, causing intermittent admin privilege loss. The fix at the time was to delete the subscriber mirrors and rely on the email conflict to block sync. This turned out to create the collision vulnerability above. The two-layer approach introduced 2026-04-17 supersedes it.
BuddyBoss “View As” — Sync Suppression
Section titled “BuddyBoss “View As” — Sync Suppression”BuddyBoss’s “View As” feature (on practice.baseworks.com) calls wp_set_auth_cookie() internally when switching to a viewed user. WPRUS hooks into the set_logged_in_cookie action fired by that function, treating it as a real login event and propagating the session switch to baseworks.com. Without intervention, “View As” on the practice site would log the admin into that member’s WooCommerce account on baseworks.com.
Fixed: mu-plugin bw-suppress-wprus-on-view-as.php on practice.baseworks.com hooks in just before WPRUS’s set_logged_in_cookie handler and removes it for the duration of any switch_to_user or switch_to_olduser request. “View As” works normally on practice; no session propagation to baseworks.com occurs.
Security note: this is not an exploitable vulnerability — initiating “View As” requires admin access and a valid nonce. The concern is accidental data exposure (admin seeing member billing data) and confused sessions.
Server Infrastructure Dependency
Section titled “Server Infrastructure Dependency”All three sites (baseworks.com, practice.baseworks.com, crm.baseworks.com) run on the same server (5.180.253.171). WPRUS makes server-to-server HTTPS calls for token renewal. /etc/hosts entries on the server point all three domains directly to the local IP, bypassing Cloudflare for these calls. This requires a publicly-trusted SSL certificate — a Let’s Encrypt cert was installed 2026-03-23 (replacing the Cloudflare Origin Certificate, which is only trusted by Cloudflare’s proxy and caused SSL failures on direct connections).
Let’s Encrypt certs expire every 90 days. Certbot auto-renewal must remain active.
WP Fusion Conflict — Tags Must Not Trigger WP RUS
Section titled “WP Fusion Conflict — Tags Must Not Trigger WP RUS”WP Fusion ships with a built-in WP Remote Users Sync integration. When active, it hooks into wpf_tags_applied and wpf_tags_removed and calls tags_modified(), which fires a full WP RUS cross-site sync every time a WP Fusion tag is applied or removed.
This integration is disabled on both sites via the wpf-wprus-decouple.php mu-plugin.
Why: Tag operations on practice.baseworks.com (e.g. PRIMER_COMPLETE applied when LearnDash Primer course is completed) trigger a WP RUS sync to baseworks.com mid-operation. This sync fires at the same moment as WP Fusion’s FluentCRM REST API call, disrupting the request lifecycle in a way that causes the FluentCRM call to fail silently. The tag appears in WP Fusion’s local cache (user meta) but never reaches FluentCRM — and therefore never triggers the downstream automation recipes (e.g. the 3→12 month extension).
Note on how tag sync actually works: WP RUS does not sync WP Fusion tags between sites. Tags travel via FluentCRM automations triggered by the SYNC Tags to PRACTICE SITE / SYNC Tags to BW.COM ephemeral tags. The WPF/WPRUS integration we disabled was adding WPF tag metadata to WP RUS user payloads — a side-channel that was interfering with the primary tag flow without adding value.
Related bug — WP Fusion API queue: The same mu-plugin also disables the WP Fusion API queue (wpf_use_api_queue filter set to false). WP Fusion normally buffers apply_tags, remove_tags, and update_contact calls and flushes them at PHP shutdown. When lesson completion is triggered asynchronously via Uncanny Automator, the shutdown flush does not reliably execute — the HTTP call to FluentCRM is silently lost. With the queue disabled, tag operations call FluentCRM immediately and inline.
This was discovered 2026-04-03 after Login Sync was enabled (~2026-03-23), which activated the WPF/WPRUS integration in a way that surfaced the conflict. See changelog entry 20260403-wpfusion-primer-complete-tag-not-reaching-fluentcrm.md.
mu-plugin: wpf-wprus-decouple.php — deployed on both baseworks.com and practice.baseworks.com. See custom-code-overview for the full mu-plugin inventory.
Custom Plugin vs. WP RUS — Decision Record (2026-04-03)
Section titled “Custom Plugin vs. WP RUS — Decision Record (2026-04-03)”After the Login Sync complications required two mu-plugin fixes, the question was raised: would a purpose-built custom sync plugin be more maintainable than WP RUS?
Decision: stay with WP RUS.
The four sync behaviors we need are:
| Behavior | Custom difficulty | Notes |
|---|---|---|
| Create user when account is created on other site | Easy | Hook user_register, POST to REST API |
| Sync passwords | Medium | Must transmit hashed password over HTTPS; handle timing edge cases |
| Sync login sessions | Hard | Requires signed time-limited tokens, cookie handling, cross-site redirect logic — essentially SSO from scratch |
| Sync basic profile (name, email) | Easy | Hook profile_update, POST changes |
The login session sync (#3) is where most of the engineering complexity in WP RUS lives. Getting it secure — no token replay attacks, no session hijacking via forged payloads, correct token renewal — is real work. WP RUS has already solved this with a HMAC-based token system, and it handles edge cases accumulated over years of use.
Building a custom plugin would mean rebuilding ~80% of WP RUS, without that history, and owning the security maintenance going forward.
The problems we experienced with WP RUS were not defects in WP RUS itself. They were conflicts with other plugins:
- BuddyBoss “View As” → fixed by
bw-suppress-wprus-on-view-as.php - WP Fusion tag hooks → fixed by
wpf-wprus-decouple.php
Both are targeted, well-understood fixes. The core sync (login, create, update, password, meta) has worked cleanly throughout.
When to revisit this decision: If WP RUS is abandoned by its developer, or if a third incompatibility emerges that cannot be resolved with a targeted fix. At that point, the accumulated knowledge of the edge cases would make it feasible to specify a custom replacement.
Related
Section titled “Related”- Cross-Site Integration & Automation — WP Fusion tag sync, FluentCRM, Uncanny Automator
- Practice Site Platform Infrastructure — full practice site stack