TL;DR: To build a block-based page builder, we refused to use the slow, rigid EAV pattern. We traded global SQL WHERE flexibility for a hybrid JSON and polymorphic architecture that keeps our query planner sane and database flat.
The EAV Nightmare
When you set out to build a modern block-based page builder, your first instinct is to look at how traditional CMS platforms handled custom fields. You quickly discover the Entity-Attribute-Value (EAV) pattern. It looks elegant on a whiteboard: one table for entities, one for attributes, one for values.
Then you put it in production, and it destroys your database.
Retrieving a single page with 20 custom fields requires a 20-way self-join. It turns your database into an unstructured, unindexed key-value store pretending to be relational. We refused to do this for StakCMS.
The JSON + Polymorphic Hybrid
Instead, we built a hybrid system. We store primitive field definitions (text, booleans) directly inside a JSON column on the content_blocks table. We then use Laravel’s polymorphic relationships (MorphMany) exclusively for heavy relational data, like media attachments and author references.
Here is what that tradeoff looks like in practice. We sacrifice some query flexibility—you can't easily do a global WHERE field_x = 'value' across all block types—in exchange for a flat schema and blazing-fast reads:
// Creating a dynamic block in HandlesContentBlockCreation.php
$data = collect($finalSchema)->mapWithKeys(function ($field) {
return [$field['key'] => match ($field['type']) {
'repeater', 'multi-select' => [],
'boolean' => false,
default => null,
}]; })->all();
$contentBlock = $blockable->contentBlocks()->create([ '
component_definition_id' => $finalDefinitionId, 'data' =>
$data, // Saved as raw JSON
'locale' => $locale,
]);



