Database Schema

Database Schema (User Meta Fields)

All fields are stored as user meta (wp_usermeta), under the plugin's namespace prefix lh_profile-. There is no dedicated database table.

Registered meta (validated via register_meta())

These fields have a sanitize_callback registered, so any direct update_user_meta() write – from this plugin, the REST API, or any other plugin – is validated/normalised, not just writes that go through this plugin's own save paths.

Meta keyTypeFormatNotes
lh_profile-phonestringE.164 (+61...)Validated via giggsey/libphonenumber-for-php-lite (Composer). A full backfill (this session) converted ~8,300 legacy-format rows to E.164.
lh_profile-secondary_phonestringE.164Same validator as phone.
lh_profile-genderstringMale|Female|Organisation|RobotFixed allowed-value set.
lh_profile-birthdatestringY-m-dChanged from a UNIX timestamp to a plain date string in v2.13 – see Architectural Notes for why, and the full migration history.
lh_profile-street_addressstringfree textsanitize_text_field() only – no richer format exists.
lh_profile-townstringfree textsanitize_text_field() only.
lh_profile-postcodestringfree textsanitize_text_field() only for now – real per-country format validation (AU 4-digit, US ZIP, UK alphanumeric) is a known future candidate, not yet built.
lh_profile-state_provincestringfree textsanitize_text_field() only.
lh_profile-countrystringfree textsanitize_text_field() only – a plain text field, not a country-code dropdown.

Unregistered meta

Meta keyTypeNotes
lh_profile-birthdate_readablestringCompanion field alongside lh_profile-birthdate, written by handle_birthdate_update(). Historically held the pre-parse date string; since the v2.13 format change both fields hold the same Y-m-d value, making this field largely redundant going forward, but it's kept for backward compatibility since other code (filter_query_joins() for the "Users Insights" plugin integration) reads it directly.
lh_profile-emailsarrayAdditional/secondary emails, serialized array of validated email strings. Slated for removal – superseded by lh-user-identity. Not extracted into its own field class for this reason. As of this session, 1,888 users have a non-empty value here, largely following a firstname.lastname@theaustraliatimes.com.au <-> real-email cross-reference pattern that turned out to indicate a large number of duplicate user accounts – see the exploration brief referenced in Architectural Notes.
lh_profile-updated_timeintUNIX timestamp of the last time any "non-native" field (see below) was updated. Set by track_last_meta_update_time().
lh_profile-updated_fieldstringWhich meta key was last updated, alongside updated_time.

Core WordPress fields (not this plugin's own meta)

FieldStorageNotes
First namefirst_name usermeta (WP core key)No register_meta() here deliberately – registering a sanitize_callback against a core-recognised key would affect any other code touching that exact key, not just this plugin.
Last namelast_name usermeta (WP core key)Same reasoning as first name.
Display namewp_users.display_name columnNot usermeta at all – register_meta() doesn't apply.

"Non-native fields" list

return_all_non_native_fields() returns: street_address, town, postcode, gender, phone, birthdate (all namespace-prefixed). This list drives several cross-cutting behaviours: vcard export tracking, notification token generation, the "updated_time"/"updated_field" tracking, and the prevent_bad_meta_values filter (rejects empty-string writes to any of these keys).