Data Sovereignty Principle
Every Flavor product owns all of its data in its own custom database tables. This is a fundamental architectural rule that applies to three separate data domains — the plugin, the theme, and Flavor Core — each with its own tables and its own access helpers.
Why Custom Tables?
- Performance — Optimized schema, proper indexes, no EAV overhead
- Independence — No dependency on the WordPress options table
- Scalability — Handles thousands of products/orders efficiently
- Clean separation — Plugin, theme and Core data never interfere
The three data domains
| Domain | Namespace | Options helper | Cache / store | Tables |
|---|---|---|---|---|
| Plugin | wpec_* | wpec_get_option() / wpec_set_option() | wpec_cache_*() | {prefix}ec_* |
| Theme | flavor_* | flavor_get_option() / flavor_set_option() | flavor_cache_*() | {prefix}flavor_* |
| Flavor Core | flavor_core_* | flavor_core_get_option() / flavor_core_set_option() / flavor_core_delete_option() | Flavor_Core_Data_Store (encrypted) | {prefix}flavor_core_data, {prefix}flavor_core_options |
Plugin Data Access
Configuration
// Store plugin settings
wpec_set_option('store_name', 'My Store', 'general');
wpec_get_option('store_name', 'Default Store');
// NEVER do this:
update_option('wpec_store_name', 'My Store'); // WRONG!
get_option('wpec_store_name'); // WRONG!
Cache
// Plugin cache (custom ec_cache table)
wpec_cache_set('product_count', 150, 3600); // TTL in seconds
$count = wpec_cache_get('product_count');
// NEVER do this:
set_transient('wpec_product_count', 150, 3600); // WRONG!
Theme Data Access
Configuration
// Store theme settings
flavor_set_option('header_sticky', 'true', 'header');
flavor_get_option('header_sticky', 'false');
// NEVER do this:
get_option('flavor_options'); // WRONG! The blob was deleted.
update_option('flavor_header_sticky', 'true'); // WRONG!
Cache
// Theme cache (custom flavor_cache table)
flavor_cache_set('menu_html', $html, 1800);
$html = flavor_cache_get('menu_html');
// IMPORTANT: flavor_cache_get() returns null (not false)
$val = flavor_cache_get('counter');
if ($val !== null) { /* exists */ }
// For integer casts with default:
$count = (int) flavor_cache_get('counter', 0);
// NEVER do this:
set_transient('flavor_menu_html', $html, 1800); // WRONG!
Flavor Core Data Access
Flavor Core (license client, update manager, unified debug, AI foundation) owns
its data in two custom tables — never in wp_options:
{prefix}flavor_core_data— sensitive data (license cache, fingerprint token), AES-256-GCM encrypted, accessed throughFlavor_Core_Data_Store.{prefix}flavor_core_options— non-sensitive settings (mode, release channel, MCP toggle, grace opt-in, self-update diagnostics, migration markers).
Non-sensitive settings use the flavor_core_*_option() helpers:
// Read / write / delete Flavor Core settings
flavor_core_get_option('release_channel', 'stable');
flavor_core_set_option('mcp_enabled', 'true', /* autoload */ false, 'general');
flavor_core_delete_option('some_stale_flag');
// NEVER do this:
update_option('flavor_core_release_channel', 'stable'); // WRONG!
get_option('flavor_core_mcp_enabled'); // WRONG!
The old "sanctioned third namespace in wp_options" exception is closed.
flavor_core_* data belongs in the Core tables. The only residue permitted in
wp_options is the per-store bootstrap markers (the schema/migration flags that
must exist before the Core tables do) — never business data.
Module Settings
Module settings use dedicated option keys:
// Correct — dedicated key per module
flavor_set_option('wishlist_enabled', 'true', 'flavor_module_wishlist');
flavor_get_option('wishlist_enabled', 'false', 'flavor_module_wishlist');
// Wrong — shared options blob
flavor_set_option('wishlist_enabled', 'true'); // Don't pollute the default group
WordPress Data (Read-Only)
For data that genuinely belongs to WordPress, use standard functions:
// Fine — reading WordPress-owned data
get_option('admin_email');
get_option('date_format');
get_option('permalink_structure');
Type Safety
flavor_get_option() has built-in type safety:
// If the caller passes an array default but the DB stores a scalar,
// it returns the default array (prevents crashes)
$colors = flavor_get_option('brand_colors', ['#000', '#fff']);
// DB has a scalar string → returns ['#000', '#fff']
// DB has a proper JSON array → returns the parsed array
Quick rules
- Plugin options →
wpec_get_option()/wpec_set_option()— neverget_option('wpec_*') - Theme options →
flavor_get_option()/flavor_set_option()— neverget_option('flavor_options') - Core options →
flavor_core_get_option()/flavor_core_set_option()— neverget_option('flavor_core_*') - Cache →
wpec_cache_*()/flavor_cache_*()— neverset_transient('wpec_*' | 'flavor_*') flavor_cache_get()returnsnull(notfalse) — test with!== null- Never
set_option(key, null)—option_valueis NOT NULL; usedelete_option(key)to clear