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):

  1. Starts from WordPress get_site_url() (the Site Address from Settings → General, including subdirectory path if WordPress is not installed at the domain root)
  2. Strips the http:// or https:// prefix
  3. Passes the result through the bright_site_url filter

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 (updateBrightUserMetaupdateRealmUserCustom) 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/portal vs example.com)
  • bright_site_url overrides 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: hostdata
  • email: 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:

  1. Sync profile/org fields from WordPress → Bright hostdata
  2. Define a stored query on the Bright backend that:
    • Joins realm_users to registrations (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)
  3. Call the query from WordPress via stored_query/run and 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)

\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:

  1. Calls updateBrightUserMeta() (same as single-user sync)
  2. Writes progress to the page (Updated Bright User MetaData for {email})
  3. Sets bright-last-fullsync usermeta 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:

  1. Add the usermeta key to bright_usermeta_export on WordPress
  2. Ensure users are synced (per-user hooks or bulk sync page)
  3. Author SQL in a Rails rake task or Bright admin stored-query editor
  4. Extract hostdata fields with json_extract_path and params[:site_url]
  5. Join to registrations / subscriptions as needed
  6. Version query names (my_report_1my_report_2) when changing column layouts
  7. Call stored_query/run from WordPress with matching name and 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

  1. Bright → Settings → Connection Settings — API URL, realm GUID, secret key configured
  2. Bright → Settings → Plugin Function → UserMeta Sync — comma-separated meta keys set
  3. Per-user or bulk sync run at least once so hostdata exists in Bright
  4. Stored query deployed on Bright backend referencing hostdata → meta → {your_key}
  5. WordPress report/shortcode calls stored_query/run with correct site_url param

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.


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_usermeta keys sync via bright_usermeta_export
  • Sync pushes a hostdata snapshot to realm_users.custom via realm_user/gcustom
  • Report by extracting hostdata in Bright stored queries and joining to registration data
  • Automate with profile_update / user_register, plus updated_user_meta for direct meta writes
  • Backfill with /wp-admin/admin.php?page=bright_options_sync when deploying new fields to existing users