Bright User Meta Sync – Developer’s Guide
Bright User Meta Sync — Developer Guide
This guide explains how the Bright WordPress plugin synchronizes WordPress user metadata to the Bright realm user record, how that data is stored, and how it can be levereged in custom reports via Bright stored queries, which is available in Bright Enterprise plans.
Overview
Bright maintains a realm user record for each learner, keyed by email. WordPress user profile data that is relevant to reporting, segmentation, or business logic can be pushed from the site into Bright as hostdata on that realm user.
Some key things to note about this
- the same user, as referrred to by a unique email address, can be in multiple Bright projects [or Realms as they were historically called]
- a user's custom data is also site specific, this means it is stored in Bright by the originating site host FQDN. This allows test and development sites to function without "overwriting" a production dataset.
The sync pipeline is:
WordPress wp_usermeta → getUserAttributes() → realm_user/gcustom API → realm_users.custom.hostdata
Once hostdata is in Bright, stored queries on the Bright/Rails backend can extract those fields and join them to registration, subscription, and course-completion data for custom reports served back to WordPress (or other clients) via stored_query/run.
What Are the Bright User Meta Fields?
There are two layers to understand: configured export keys and the full hostdata payload.
Configured export keys (bright_usermeta_export)
The list of WordPress wp_usermeta keys to sync is stored in the WordPress option bright_usermeta_export. It is a comma-separated list of meta key names, for example:
first_name,last_name,mepr_department,corporate_user_id
Configure this in the WordPress admin:
- Bright → Settings → Plugin Function → UserMeta Sync
- URL:
/wp-admin/admin.php?page=bright_options_settings(Plugin Function tab)
Only keys listed here are read from wp_usermeta and placed into the synced payload under meta. If a key is not in this list, WordPress may store the value locally, but Bright will never receive it.
Common keys:
| Meta key | Typical source | Notes |
|---|---|---|
first_name |
WordPress core user meta | Standard profile field |
last_name |
WordPress core user meta | Standard profile field |
mepr_* |
MemberPress custom fields | Stored as usermeta when MemberPress saves custom field values |
| Custom keys | Your plugin | Any wp_usermeta key name works if listed in bright_usermeta_export |
There is no fixed schema beyond what you configure. The Bright plugin does not ship a canonical list of business fields — each integration chooses which usermeta keys matter for that site.
Full hostdata payload
When sync runs, getUserAttributes() builds a richer structure than the export list alone. The payload sent to Bright includes:
| Field | Source | Description |
|---|---|---|
meta |
bright_usermeta_export keys via get_user_meta() |
Your configured usermeta values |
id |
$user->ID |
WordPress user ID |
email |
User email | Realm user lookup key |
display_name |
WordPress display name | |
site_roles |
$user->roles |
Map of role slug → true |
avatar |
Gravatar/avatar URL | |
attributes_derived_at |
Current timestamp | When the snapshot was built |
groups |
BuddyPress (if installed) | Optional group membership |
Example hostdata shape as stored in Bright (realm_users.custom):
{
"hostdata": {
"example.com": {
"id": 42,
"email": "learner@example.com",
"display_name": "Jane Learner",
"site_roles": { "subscriber": true },
"attributes_derived_at": "Wed, 10 Jun 2026 12:00:00 +0000",
"meta": {
"first_name": "Jane",
"last_name": "Learner",
"mepr_department": "Operations",
"corporate_user_id": "17"
}
}
}
}
The site URL key (e.g. example.com) comes from Bright\Wordpress::getSiteUrl(). See Site URL derivation and override below.
Site URL derivation and override
The site URL is the key under hostdata in Bright. It must be consistent everywhere: sync writes, stored-query json_extract_path calls, and stored_query/run params must all use the same value.
How it is derived
Bright\Wordpress::getSiteUrl() in php-connect/wordpress.php:
public function getSiteUrl($without_protocol = true)
{
$url = get_site_url();
if ($without_protocol) {
$find = array('http://', 'https://');
$replace = '';
$url = str_replace($find, $replace, $url);
}
return apply_filters('bright_site_url', $url);
}
By default ($without_protocol = true):
- Starts from WordPress
get_site_url()(the Site Address from Settings → General, including subdirectory path if WordPress is not installed at the domain root) - Strips the
http://orhttps://prefix - Passes the result through the
bright_site_urlfilter
Examples:
| WordPress Site Address | getSiteUrl() result (default) |
|---|---|
https://training.example.com |
training.example.com |
https://example.com/portal |
example.com/portal |
http://localhost:8080 |
localhost:8080 |
Pass getSiteUrl(false) if you need the full URL including protocol (uncommon for hostdata keys).
Server-side sync (updateBrightUserMeta → updateRealmUserCustom) uses this value as the JSON key when writing hostdata to Bright.
Overriding with bright_site_url
Use the bright_site_url filter to force a canonical hostdata key when the WordPress site URL does not match what Bright stored queries expect — for example, local/staging environments that should share production hostdata:
add_filter('bright_site_url', function ($url) {
// Only override on local dev; production keeps the default.
if (isset($_SERVER['HTTP_HOST']) && preg_match('/\.local$/', $_SERVER['HTTP_HOST'])) {
return 'training.example.com';
}
return $url;
});
The filter receives the protocol-stripped URL and must return the string that should be used as the hostdata key.
Important: After changing this filter, existing hostdata under the old key is not migrated automatically. Users synced under staging.example.com will not appear in queries that read training.example.com until you re-sync (bulk UserMeta Sync page or per-user sync).
Using the same value in stored queries
Always pass the filtered site URL when calling stored queries from PHP:
$bright = \Bright\Wordpress::getInstance();
$site_url = $bright->getSiteUrl();
$results = $bright->callApi('stored_query/run', array(
'accessMode' => 'realm',
'params' => array(
'name' => 'my_report_1',
'site_url' => $site_url,
),
));
Stored-query SQL should reference the same value via params[:site_url] in json_extract_path (see examples in Where Data Is Stored in Bright).
Do not hard-code a hostname in SQL if the site uses bright_site_url — hard-coding bypasses the filter and breaks non-production environments.
Client-side sync uses a different key
The browser path in bright-js/bright.js pushes hostdata using window.location.hostname (hostname only, no path, no filter):
data[window.location.hostname] = eval("(" + bright_user_attributes + ")");
That can produce a different hostdata key than server-side getSiteUrl() when:
- WordPress lives in a subdirectory (
example.com/portalvsexample.com) bright_site_urloverrides the server value
For reporting, treat server-side sync as the source of truth. Rely on updateRealmUserMeta() / updateBrightUserMeta() for fields that stored queries read. Client-side sync is a convenience on page load, not a substitute for consistent server-side keys.
Where Data Is Stored in Bright
Sync writes to the Bright API endpoint realm_user/gcustom with:
key:hostdataemail: the learner's email- Site URL as the top-level key inside the custom JSON blob
On the Bright/Rails side, this is persisted on realm_users.custom (JSON column). Stored queries access nested paths such as:
json_extract_path(
realm_users.custom::json,
'hostdata',
'<%= params[:site_url] %>',
'meta',
'first_name'
)
The meta sub-object contains only keys from bright_usermeta_export. Top-level hostdata fields (id, email, site_roles, etc.) are siblings of meta, not inside it.
How Synced Meta Is Used
1. Per-user snapshot in Bright
Hostdata gives Bright a site-specific profile snapshot for each realm user. This is useful when:
- The WordPress site is the system of record for profile or organizational attributes
- You need those attributes available in Bright Admin or API contexts without re-querying WordPress
2. Cross-reference with registration data in stored queries
The primary reporting pattern is:
- Sync profile/org fields from WordPress → Bright hostdata
- Define a stored query on the Bright backend that:
- Joins
realm_userstoregistrations(and related tables) on learner email or ID - Extracts hostdata fields with
json_extract_path - Accepts parameters (e.g.
site_url,corporate_user_id, date ranges)
- Joins
- Call the query from WordPress via
stored_query/runand render results in a shortcode, admin page, or DataTable template
Example PHP call pattern:
$bright = \Bright\Wordpress::getInstance();
$results = $bright->callApi('stored_query/run', array(
'accessMode' => 'realm',
'params' => array(
'name' => 'my_org_subscription_report_1',
'site_url' => $bright->getSiteUrl(),
'corporate_user_id' => $current_user->id,
),
));
Example stored-query extraction (Rails/ERB SQL fragment):
replace(
json_extract_path(
realm_users.custom::json,
'hostdata',
'<%= params[:site_url] %>',
'meta',
'department'
)::varchar,
'"', ''
) as department
Join to registrations on realm_users.id = registrations.realm_user_id (or equivalent join path for your schema) to produce rows that combine who the learner is (hostdata) with what they completed (registration scores, dates, course titles).
3. Bright templates and client-side context
getUserAttributes() is also used to populate userAttributes in Bright template context (e.g. <code class="kb-btn">{user.meta.first_name}</code> in Handlebars templates). That is separate from the persisted hostdata sync but uses the same attribute builder.
Use Cases (Abstract)
These patterns appear frequently across integrations. None require a specific customer or plugin — they illustrate why teams add usermeta keys to the export list.
Organizational segmentation
A membership plugin stores attributes such as department, region, or cost center in usermeta. After sync, stored queries let managers filter completion reports to learners in their organization unit.
Parent / child account relationships
Sub-accounts carry meta such as corporate_user_id or corporate_user_login (written by site-specific login hooks). Reports scoped to a parent account filter on those hostdata fields so each manager sees only their learners.
Subscription or cohort identification
Meta such as subscr_id or cohort year can be synced so Bright reports group results by billing subscription or training cohort without joining back to WordPress at report time.
Custom profile dimensions for compliance
Industry-specific identifiers (license number, job title, facility code) live in usermeta, sync to hostdata, and appear as columns in audit or roster reports alongside course completion status.
Enriching registration rows for DataTables / CSV export
A stored query returns a flat array of rows (learner name, org field, course title, score, completed_at). WordPress renders this via bright_generic_report or a custom shortcode with DataTables filters on each column.
Automatic Sync (WordPress Hooks)
Built-in Bright hooks
The plugin registers these in bright-wordpress.php:
add_action('profile_update', 'Bright\Wordpress::updateRealmUserMeta', PHP_INT_MAX, 2);
add_action('user_register', 'Bright\Wordpress::updateRealmUserMeta', PHP_INT_MAX, 1);
profile_update fires when a user is saved through WordPress user APIs (e.g. admin profile edit, wp_update_user()). It does not fire for direct update_user_meta() calls.
user_register fires when a new user is created.
Both call updateRealmUserMeta($user_id), which loads the user and delegates to updateBrightUserMeta().
Extension hook before sync
Immediately before the API call, Bright fires:
do_action('bright_before_update_bright_user_meta', $user);
Use this to log, validate, or perform side effects. The user object is the WordPress WP_User; hostdata has not been sent yet.
Client-side sync on page load
bright-js/bright.js also pushes hostdata from the browser when bright_user_attributes is present in the page (unless bright_block_usermeta_sync is in the URL). This is a secondary path for logged-in learners visiting Bright-enabled pages, not a substitute for server-side sync after programmatic meta changes.
Custom plugins: sync on update_user_meta
Because many plugins (MemberPress sub-account forms, CSV import, bulk updates) write usermeta via update_user_meta() rather than profile_update, site-specific plugins often add:
add_action('updated_user_meta', 'my_sync_bright_meta', PHP_INT_MAX, 4);
add_action('added_user_meta', 'my_sync_bright_meta', PHP_INT_MAX, 4);
add_action('deleted_user_meta', 'my_sync_bright_meta', PHP_INT_MAX, 4);
function my_sync_bright_meta($meta_id, $user_id, $meta_key, $meta_value) {
$export = array_filter(array_map('trim', explode(',', get_option('bright_usermeta_export', ''))));
if (!in_array($meta_key, $export, true)) {
return;
}
\Bright\Wordpress::updateRealmUserMeta((int) $user_id);
}
Only sync keys that appear in bright_usermeta_export to avoid unnecessary API traffic.
Calling Sync Directly (PHP)
By user ID (recommended for hook callbacks)
\Bright\Wordpress::updateRealmUserMeta($user_id);
Static wrapper; matches what profile_update uses.
By user object
$bright = \Bright\Wordpress::getInstance();
$user = get_user_by('email', 'learner@example.com');
$bright->updateBrightUserMeta($user);
Legacy helper
bright_update_user_meta('email', 'learner@example.com');
// or
bright_update_user_meta('ID', 42);
// or pass WP_User as second arg with first arg 'bright_user'
Defined in php-connect/wp-deprecation.php; delegates to updateBrightUserMeta().
Bulk sync all users (programmatic)
$bright = \Bright\Wordpress::getInstance();
$result = $bright->updateAllUsers(array(
'skip-today' => true, // skip users already synced today
'max-records' => 100, // limit batch size (optional)
));
// Returns: array('records' => N, 'success' => M)
Same method the admin UserMeta Sync page uses.
Inspect the payload without syncing
$bright = \Bright\Wordpress::getInstance();
$user = get_user_by('ID', $user_id);
$attrs = $bright->getUserAttributes($user, array('raw' => true));
Returns the array that would be sent to Bright (not JSON-encoded).
Bulk User Meta Sync (Admin UI)
Use the admin page to push hostdata for all WordPress users after initial setup or when adding new keys to bright_usermeta_export.
Location: Bright → UserMeta Sync
URL: /wp-admin/admin.php?page=bright_options_sync
Form options
| Option | Purpose |
|---|---|
| Sync User Metadata (submit) | Runs updateAllUsers() for each WP user |
| Skip Records Already Updated Via Full Sync Today | Skips users with bright-last-fullsync usermeta equal to today's date |
| Max records to process | Limits batch size to avoid PHP timeouts on large user bases |
Behavior
For each user, the page:
- Calls
updateBrightUserMeta()(same as single-user sync) - Writes progress to the page (
Updated Bright User MetaData for {email}) - Sets
bright-last-fullsyncusermeta to today's date (d-m-Y) on success
When to run a full sync
- After adding new keys to UserMeta Sync settings
- After deploying a feature that backfills usermeta for existing users
- After restoring from backup or migrating users
- When stored-query reports show blank columns for users who pre-date the feature
For ongoing changes, prefer automatic hooks or direct updateRealmUserMeta() calls so individual updates stay current without nightly bulk jobs.
Developing Stored Queries That Use Hostdata
Stored queries live on the Bright/Rails backend, not in the WordPress plugin. General workflow:
- Add the usermeta key to
bright_usermeta_exporton WordPress - Ensure users are synced (per-user hooks or bulk sync page)
- Author SQL in a Rails rake task or Bright admin stored-query editor
- Extract hostdata fields with
json_extract_pathandparams[:site_url] - Join to
registrations/ subscriptions as needed - Version query names (
my_report_1→my_report_2) when changing column layouts - Call
stored_query/runfrom WordPress with matchingnameand params
Parameter conventions
| Param | Typical use |
|---|---|
site_url |
Must match the hostdata key written by getSiteUrl() |
corporate_user_id |
Filter learners belonging to a parent account |
corporate_user_login |
Alternate filter when login is the stable identifier |
| Date / cohort params | Scope registration rows |
Column indexing in templates
stored_query/run often returns a JSON array of row arrays. WordPress report templates reference columns by index (<code class="kb-btn">{this.[0]}</code>, <code class="kb-btn">{this.[1]}</code>, …). Document column order in your query version when building DataTable or Handlebars templates.
See also: bright-php-api-reference.md (stored query section) and the Bright v2 API controller reference for realm_user/gcustom.
Configuration Checklist
- Bright → Settings → Connection Settings — API URL, realm GUID, secret key configured
- Bright → Settings → Plugin Function → UserMeta Sync — comma-separated meta keys set
- Per-user or bulk sync run at least once so hostdata exists in Bright
- Stored query deployed on Bright backend referencing
hostdata → meta → {your_key} - WordPress report/shortcode calls
stored_query/runwith correctsite_urlparam
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Field empty in Bright report | Key missing from bright_usermeta_export, or user never synced after key was added |
| Field correct in WP but not Bright | Meta updated via update_user_meta() without a sync hook; run bulk sync or call updateRealmUserMeta() |
| Field in hostdata but not in report | Stored query does not extract that meta key, or wrong site_url in query params |
| Bulk sync times out | Set Max records to process and run in batches; enable Skip Records Already Updated on subsequent runs |
profile_update did not sync department |
Expected — use updated_user_meta hook or direct sync call |
Verify hostdata in Bright Admin
Use Bright → Admin (realm user editor) to inspect a learner's custom JSON and confirm the hostdata entry for your site URL contains the expected meta keys.
Related Files in This Plugin
| File | Role |
|---|---|
bright-wordpress.php |
Registers profile_update / user_register hooks |
php-connect/wordpress.php |
updateRealmUserMeta(), getUserAttributes(), syncMenu(), updateAllUsers() |
php-connect/base.php |
updateBrightUserMeta(), updateRealmUserCustom() API call |
settings/tabbed-sections.php |
bright_usermeta_export settings field |
php-connect/wp-deprecation.php |
bright_update_user_meta() legacy helper |
bright-js/bright.js |
Client-side Bright.updateRealmUserMeta() |
Summary
- Configure which
wp_usermetakeys sync viabright_usermeta_export - Sync pushes a hostdata snapshot to
realm_users.customviarealm_user/gcustom - Report by extracting hostdata in Bright stored queries and joining to registration data
- Automate with
profile_update/user_register, plusupdated_user_metafor direct meta writes - Backfill with
/wp-admin/admin.php?page=bright_options_syncwhen deploying new fields to existing users
