Havenlytics stores its two primary content entities as WordPress custom post types: hvnly_property (listings) and hvnly_agent (real-estate agents). This reference documents how each CPT is registered, the exact meta-key conventions the plugin uses to persist listing and agent data, and the seven custom database tables that back the inquiry, notification, and enterprise subsystems. Everything here is grounded in the Havenlytics 3.3.1 source so you can query, extend, and integrate with confidence.
Note: Both CPTs are registered on the standard WordPress
inithook. All Havenlytics post types, meta keys, and hooks use thehvnly_prefix. If an identifier is not listed here, it does not exist in the plugin — do not rely on it.
The hvnly_property post type
The Property CPT is registered by HvnlyNab\Database\Custom_Posts\HavenlyticsNab (file includes/Database/Custom_Posts/HavenlyticsNab.php), which extends the shared base class HvnlyNab\Database\Base\Custom_Posts. The base constructor hooks registration onto init at priority 0 so the post type exists before any other module runs:
<?php
// includes/Database/Base/Custom_Posts.php (constructor)
add_action( 'init', array( $this, 'register_custom_post_type' ), 0 );
Registration arguments
The Property CPT is fully public and archive-enabled. Several arguments are computed dynamically at registration time rather than hard-coded — most importantly show_in_rest, which is gated on whether the Gutenberg editor is enabled for Havenlytics.
| Argument | Value | Notes |
|---|---|---|
public | true | Front-end queryable and visible. |
show_in_rest | dynamic | PluginGutenbergSupport::is_enabled(). Off by default (see note below). |
has_archive | dynamic | PermalinkSettings::get_property_archive_slug(), fallback true. |
rewrite | array | slug = property single slug (fallback 'property'), with_front false, pages true, feeds true. |
supports | array | title, editor, author, thumbnail, excerpt, comments. |
publicly_queryable | true | |
query_var | true | |
show_in_menu | true | Menu position 3. |
menu_icon | SVG | assets/admin/img/havenlytics-icon18x18.svg. |
capability_type | 'post' | Default post capabilities. |
labels['menu_name'] | "Havenlytics" | The top-level admin menu label. |
Note:
show_in_restis not enabled by default. It resolves toPluginGutenbergSupport::is_enabled(), which reads the setting keyhvnly_EnabledGutenbergEditorin thegeneralgroup of thehvnly_plugin_settingsoption. That setting defaults tofalse, so the Property CPT is excluded from the REST API and the block editor unless an admin turns Gutenberg support on. If you build a headless or block-editor integration, verify this setting first.
Automatic Property ID meta
On insert and publish, the Property CPT automatically assigns a unique, human-readable identifier to the _hvnly_unique_property_id meta key via the plugin helper’s generate_property_id(). A matching cleanup routine runs on before_delete_post. The class also registers custom, sortable admin columns and taxonomy filters for the listing table.
Tip: Treat
_hvnly_unique_property_idas read-only. It is generated by the plugin and used as a stable reference in exports, inquiries, and integrations. Do not overwrite it.
The hvnly_agent post type
The Agent CPT is registered by HvnlyNab\Agent\AgentPostType (file includes/Agent/AgentPostType.php), which also extends Custom_Posts. Its slug is defined as the constant AgentConstants::POST_TYPE = 'hvnly_agent'. Unlike the Property CPT, the Agent post type does not add its own top-level menu — that is handled separately by AgentAdminMenu.
Registration arguments
| Argument | Value | Notes |
|---|---|---|
public | true | |
publicly_queryable | true | |
show_ui | true | |
show_in_menu | false | Menu provided by AgentAdminMenu, not the CPT. |
show_in_nav_menus | true | |
show_in_admin_bar | true | |
show_in_rest | dynamic | PluginGutenbergSupport::is_enabled(), same gate as Property. |
has_archive | dynamic | Agent archive slug. |
hierarchical | false | |
capability_type | 'post' | |
map_meta_cap | true | Maps meta capabilities to the primitive post caps. |
supports | array | title, editor, thumbnail, excerpt, revisions. |
rewrite['slug'] | dynamic | sanitize_title() of the agent single slug; falls back through filter hvnly_agent_rewrite_slug to 'agent'. with_front false. |
menu_icon | dashicons-businessman |
After registration, the Agent CPT fires an action you can hook to run setup once the post type is guaranteed to exist:
<?php
// Fired by AgentPostType after register_post_type()
add_action( 'hvnly_agent_post_type_registered', function ( $post_type ) {
// $post_type === 'hvnly_agent'
// Safe point to register agent-related integrations.
}, 10, 1 );
The Agent listing table adds columns for photo, title, position, company, email, phone, and availability.
Note: The shared
Custom_Posts::init()also callsregister_post_status( 'expired', … )once per CPT. This is redundant across post types but harmless — theexpiredstatus is registered globally.
Property meta-key conventions
Havenlytics uses a disciplined, prefix-based convention for post meta so keys stay namespaced, hidden from the default custom-fields UI (leading underscore), and protected from accidental deletion. There are two families: fixed scalar keys and dynamic builder-group keys.
Scalar property meta (_hvnly_property_*)
Core listing attributes are stored under the private prefix _hvnly_property_. Real keys in the plugin include:
_hvnly_property_price,_hvnly_property_bedrooms,_hvnly_property_bathrooms,_hvnly_property_reception_rooms_hvnly_property_sqft,_hvnly_property_garage_sqft,_hvnly_property_street,_hvnly_property_year_built_hvnly_property_amenities,_hvnly_property_floor_plan,_hvnly_property_virtual_tour,_hvnly_property_brochure_hvnly_property_epc,_hvnly_property_map,_hvnly_property_inspection_report_hvnly_property_featuredand_hvnly_property_action_tool_is_featured(featured flags)_hvnly_property_views(array containing a running total) and the generated_hvnly_unique_property_id
Builder group meta: {group_base_id}_{metaKey}
Fields created through the drag-and-drop DnD Property Builder are not stored under the fixed _hvnly_property_ prefix. Instead each builder group has a stable master base ID, and every field within it is saved as {group_base_id}_{metaKey}. The master base IDs are resolved once by UnifiedFieldGenerator::get_or_create_master_base_ids() (fallback option hvnly_master_base_ids) and cached, so the same field always maps to the same key across saves.
Common group suffixes:
| Field type | Meta key pattern | Stored value |
|---|---|---|
| gallery | {base}_images | CSV / array of attachment IDs |
| video | {base}_url | Video URL |
| map | {base}_preview plus {map_*}_address / _latitude / _longitude | Map preview + coordinates |
| agents | {base}_agents | Assigned agent IDs |
| faq | {base}_faqs | FAQ entries |
| repeater | {base}_items | Repeater rows |
Gallery is the canonical SSOT
The property gallery is the single source of truth for a listing’s images. It is stored as the {group_base_id}_images value (a CSV/array of attachment IDs) and must always be read through the one canonical loader, hvnly_get_property_gallery_ids(). Never parse the raw meta yourself — the loader normalizes CSV vs. array storage and applies the plugin’s expected ordering.
<?php
// Read a property's gallery attachment IDs the SSOT way.
$property_id = get_the_ID();
$gallery_ids = hvnly_get_property_gallery_ids( $property_id );
foreach ( $gallery_ids as $attachment_id ) {
echo wp_get_attachment_image( $attachment_id, 'large' );
}
Common mistake: Reading gallery images by calling
get_post_meta()on a guessed key. The base ID is dynamic and the value may be CSV or array. Always callhvnly_get_property_gallery_ids()so you get the normalized, correctly ordered list.
Per-image titles and captions are stored on the attachment posts themselves; the metabox save routine reads POST keys of the form hvnly_gallery_title_{id}, hvnly_gallery_ids_{id}, and hvnly_gallery_caption_{id}. A JSON field-map bookkeeping value is kept under _hvnly_field_map.
Agent meta-key conventions (_hvnly_agent_*)
Agent contact and profile data is stored under the _hvnly_agent_ prefix. Real keys include _email, _phone, _mobile, _whatsapp, _fax, _office, _position, _company, _website, _address, _license, and the social keys _facebook, _linkedin, _instagram, _twitter, _pinterest, _youtube, _tiktok, _vimeo. Identity linkage lives in _hvnly_agent_linked_user_id, _hvnly_agent_registered_at, and the array _hvnly_agent_extensions. Availability is one of the slugs available, busy, away, or offline.
Property ↔ agent link meta
A listing references its agents through post meta rather than a taxonomy:
_hvnly_property_agents— ordered array of Agent CPT post IDs (the current, canonical link)._hvnly_property_agent— legacy single WordPress user ID (kept for backward compatibility)._hvnly_hide_sidebar_agent_widget— per-listing flag to suppress the sidebar agent widget.
Note: Meta prefixes
_hvnly_,_havenlytics_,gallery_,video_,map_,property_docs_, andpreset_hvnly_property_field_are protected against the plugin’s deletion-safety routine. Protected options includehvnly_plugin_settings,hvnly_property_builder.sections,hvnly_master_base_ids,hvnly_db_version, andhvnly_migrations_completed.
Querying properties and reading meta
Because hvnly_property is a standard CPT, you query it with WP_Query and read values with get_post_meta(). This example fetches featured properties under a price ceiling and reads a scalar meta value:
<?php
$query = new WP_Query( array(
'post_type' => 'hvnly_property',
'post_status' => 'publish',
'posts_per_page' => 12,
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_hvnly_property_featured',
'value' => '1',
),
array(
'key' => '_hvnly_property_price',
'value' => 750000,
'type' => 'NUMERIC',
'compare' => '<=',
),
),
) );
if ( $query->have_posts() ) {
while ( $query->have_posts() ) {
$query->the_post();
$price = get_post_meta( get_the_ID(), '_hvnly_property_price', true );
$bedrooms = get_post_meta( get_the_ID(), '_hvnly_property_bedrooms', true );
$ref = get_post_meta( get_the_ID(), '_hvnly_unique_property_id', true );
printf(
'<article><h3>%s (%s)</h3><p>%s bed · %s</p></article>',
esc_html( get_the_title() ),
esc_html( $ref ),
esc_html( $bedrooms ),
esc_html( number_format_i18n( (float) $price ) )
);
}
wp_reset_postdata();
}
Performance:
meta_querycomparisons on unindexed post meta do not scale to very large catalogs. For heavy filtering, prefer the plugin’s search engine, and cast numeric comparisons with'type' => 'NUMERIC'so the database compares values, not strings.
Custom database tables
Beyond post meta, Havenlytics provisions seven custom tables, all prefixed with $wpdb->prefix. The ContactAgent and Notification tables are created with dbDelta(); the enterprise tables are created with raw CREATE TABLE IF NOT EXISTS statements during a migration.
Inquiry & notification tables
| Table | Creating class | Migration |
|---|---|---|
{prefix}hvnly_inquiries | ContactAgent\Database\InquirySchema::create_table() | Version302InquiriesHandler (3.0.2) |
{prefix}hvnly_inquiry_replies | ContactAgent\Database\InquiryReplySchema::create_table() | Version302InquiryRepliesHandler (3.0.2) |
{prefix}hvnly_portal_notifications | Workspace\Notifications\NotificationSchema::create_table() | Version320PortalNotificationsHandler (3.2.0) |
hvnly_inquiries stores Contact-Agent submissions. It is created lazily on the first submit, during ContactAgentBootstrap, and via its 3.0.2 migration. Creation fires the action hvnly_contact_agent_inquiries_table_created. Columns:
| Column | Type | Notes |
|---|---|---|
id | bigint UNSIGNED AI | Primary key |
property_id | bigint | Indexed (idx_property_id) |
property_title | varchar(255) | |
agent_id | bigint | Indexed (idx_agent_id) |
agent_name | varchar(255) | |
sender_name | varchar(100) | |
sender_email | varchar(100) | |
sender_phone | varchar(30) | |
message | longtext | |
source | varchar(50) | Default 'contact_agent_modal' |
status | varchar(20) | Default 'new'; indexed (idx_status) |
ip_address | varchar(45) | |
user_agent | text | |
created_at | datetime | Default CURRENT_TIMESTAMP; indexed (idx_created_at) |
hvnly_inquiry_replies threads replies against an inquiry. Columns: id, inquiry_id (indexed), author_user_id (indexed), direction varchar(20) default 'outbound', message longtext, email_sent tinyint default 0, and created_at (indexed).
hvnly_portal_notifications powers Agent Workspace notifications. Columns: id, user_id, agent_id, type varchar(50), category varchar(30) default 'account', title varchar(255), body text, icon varchar(50) default 'bell', action_url varchar(255), payload longtext, read_at datetime, created_at. Composite keys include user_read(user_id, read_at) and user_category(user_id, category).
Enterprise tables (migration 2.3.0)
Four additional tables are created by Version230Handler::up() using raw CREATE TABLE IF NOT EXISTS statements (not dbDelta). Their id columns are bigint but not unsigned, unlike the ContactAgent and Notification tables.
| Table | Purpose | Key columns |
|---|---|---|
{prefix}hvnly_section_cache | Cached rendered sections (the only enterprise table actively written; key 'all_sections') | section_key varchar(100) UNIQUE, cache_data longtext, cache_hash varchar(64), expires_at, created_at |
{prefix}hvnly_field_index | Field-value index for fast querying (scaffolding) | property_id, field_key varchar(100), field_value_index varchar(255), numeric_value decimal(20,6), date_value |
{prefix}hvnly_audit_log | Change audit trail (scaffolding) | user_id, action varchar(100), entity_type varchar(50), entity_id, old_value/new_value longtext, ip_address, user_agent, created_at |
{prefix}hvnly_webhook_queue | Outbound webhook queue (scaffolding) | webhook_url varchar(500), payload longtext, attempts, max_attempts default 3, last_error, status varchar(20) default 'pending', scheduled_at, processed_at |
Warning: Only
hvnly_section_cachecurrently has writers in the plugin.hvnly_field_index,hvnly_audit_log, andhvnly_webhook_queueare enterprise scaffolding created by the migration but not yet populated by any runtime code. Do not assume they contain data.
Reading a custom table
<?php
global $wpdb;
$table = $wpdb->prefix . 'hvnly_inquiries';
$recent = $wpdb->get_results(
$wpdb->prepare(
"SELECT id, sender_name, sender_email, status, created_at
FROM {$table}
WHERE agent_id = %d
ORDER BY created_at DESC
LIMIT %d",
$agent_id,
20
)
);
Security: Always build custom-table queries with
$wpdb->prepare()and never interpolate untrusted input directly into SQL. Interpolate only the table name (which you control) into the query string, and pass every value as a placeholder argument.
Best practices
- Query listings and agents with
WP_Queryandpost_type => 'hvnly_property'/'hvnly_agent'— they are ordinary CPTs. - Read gallery images only through
hvnly_get_property_gallery_ids(); never parse the raw{base}_imagesmeta yourself. - Treat
_hvnly_unique_property_idand the linked-user identity keys as plugin-managed; do not write to them directly. - Check
PluginGutenbergSupport::is_enabled()before assuming the CPTs are exposed over REST or the block editor. - Respect the protected meta prefixes and options so your integrations survive the deletion-safety routine.
- Do not depend on the enterprise scaffolding tables for data until a release documents active writers for them.
Common mistakes
- Assuming
show_in_restistrue— it is gated on the Gutenberg setting and defaults to off. - Guessing builder-group meta keys — the group base IDs are dynamic and cached; resolve them via the plugin, don’t hard-code.
- Adding an
hvnly_agentmenu item manually — the CPT setsshow_in_menu = falseon purpose;AgentAdminMenuowns the menu. - Expecting the enterprise tables to be unsigned or populated — their
idcolumns are signed bigint and most are unwritten scaffolding.