ukyo Posted 17 hours ago Posted 17 hours ago Hi everyone, I wanted to open a discussion on an architectural concept I've been working on and testing extensively in real-world projects: the need for a native Entity Layer in ProcessWire. The Missing Layer in "Everything is a Page" One structural limitation I've repeatedly encountered in ProcessWire is the idea that everything must be a Page. While the Page tree is brilliant for routable, hierarchical documents, relying on it for every piece of structured data creates unnecessary overhead. I strongly believe ProcessWire needs a dedicated Entity Layer for data management. Specifically, just two fundamental entity types—Single and Collection (alongside reusable blocks/fieldsets)—provide the foundational infrastructure to handle almost any data modeling task in ProcessWire without needing extra modules. Once this layer exists, it can be used both by site-specific templates and by other modules for their own internal data. I have been actively building and dogfooding this architecture, and my experience so far has been eye-opening: once a native Entity structure is in place, most specialized fields (even many use cases covered by ProFields) and dozens of single-purpose modules in the modules directory simply become unnecessary. Why? Because in Entity management: You don't need a Page—it works completely independently. Global settings (Single) or structured datasets like rates, variants, testimonials, and logs (Collection) live in their own clean space without polluting the Page tree or creating dozens of field_* SQL tables. When you do want a Page connection, it works just like InputfieldPage. You can connect entities to ProcessWire Pages using an Entity field (FieldtypeWireEntries / InputfieldWireEntries), establishing the Page–Entity relationship in two ways: By Embedding (embedded mode): The entity (and its nested blocks) lives directly inside the Page editor as a structured composite field, replacing complex custom fieldtypes. By Selecting Existing Data (reference mode): Just like FieldtypePage / InputfieldPage, you select, autocomplete, or inline-create entities from a Collection and attach them to the Page. 1. The Core Architecture: Single and Collection (WireEntries) In this implementation ($wire->entries / wire('entries')), entities are defined declaratively via YAML or PHP-array Blueprints (site/entries/{singles,collections,blocks,fieldsets}/). Under the hood, everything runs on just two database tables (wire_entries_items for canonical JSON + metadata and wire_entries_index for flattened relational/search indexing), while still using 100% native ProcessWire Inputfield modules. Single (Deterministic UUIDv5): Ideal for site settings, API configurations, or singleton state ($entries->single('general_settings')). Because its UUIDv5 is derived deterministically from the blueprint name, it always has the exact same identity across local, staging, and production—no dummy "Settings" page or database ID drift. Collection (Time-ordered UUIDv7): Ideal for structured records ($entries->collection('rates')), complete with bitmask statuses (published, unpublished, hidden, trash, locked), composite unique constraints, exclusive default toggles, and optional hierarchy. Native ProcessWire Superpowers Built-In: Zero-Field Media: type: image and type: file store assets in site/assets/files/entries/{uuid}/without needing a ProcessWire database Field. Standard Pageimage methods ($img->size(800, 600)->webp()->url) work natively. Multi-Language (inheritLanguage😞 Native multi-language values with automatic default-language fallback, pre-indexed per language for fast SQL queries. Custom Entity Classes: Just like $config->usePageClasses in site/classes/, blueprints map to strongly-typed classes (RatesCollection extends EntryCollection, RateItem extends EntryItem, SettingsSingle extends EntrySingle). Example Blueprint (site/entries/collections/rates.yaml) title: Yacht Charter Rates kind: collection icon: money labelField: title columns: [title, season, price, status] fields: title: type: text label: Season Title required: true useLanguages: true inheritLanguage: true columnWidth: 50 season: type: select label: Season Period index: true options: low: Low Season mid: Mid Season high: High Season columnWidth: 50 price: type: float label: Weekly Rate (EUR) required: true index: true brochure: type: file label: Rate Sheet PDF extensions: pdf maxFiles: 1 highlights: type: blocks label: Rate Highlights allowedBlocks: [feature_item, note_box] 2. The Page ↔ Entity Bridge: Embedding vs. Referencing Because Entities and Pages complement each other, connecting them doesn't require learning a new paradigm: Embedded Mode (bindingMode = 'embedded'😞 Attach the field to a template, and each Page owns its own root entity (page_id = $page->id). A single field on your template can now hold a complete multi-field schema or repeatable block tree—without creating global ProcessWire fields for every subfield. Reference Mode (bindingMode = 'reference'😞 Works just like FieldtypePage. Editors pick existing collection items using AsmSelect, Select, Radios, Checkboxes, or TextTags (with optional inline creation), and you can look up reverse relations via $item->referencingPages(). Best of all, ProcessWire's Selector Engine works across both layers seamlessly: // Query standalone entities directly (including nested block subfields) $rates = $entries->find('type=rates, season=high, price>=10000, highlights.title*=Crew, sort=-price'); // Or query ProcessWire Pages by their attached Entity subfields (works for BOTH Embedded and Reference modes!) $yachts = $pages->find('template=yacht, charter_rates.season=high, charter_rates.price<=15000'); 3. Modules Using the Entity Layer: How WireHeadless Uses It Earlier I mentioned that an Entity layer isn't just for frontend templates—it provides the data infrastructure for modulestoo. A great example of this is WireHeadless, a hybrid/full-headless REST & RPC gateway module I built for ProcessWire. WireHeadless needs to manage: Global gateway configuration, Scoped Personal Access Tokens (whk_...) linked to ProcessWire users, Rotating JWT Refresh Tokens (whr_...), Outbound Webhook subscriptions with signing secrets (whsec_...) and event filters. Without an Entity layer, WireHeadless would have needed ConfigurableModule boilerplate plus 3 custom SQL tables, custom CRUD repositories, and hand-built admin forms/tables. Instead, WireHeadless creates zero custom SQL tables and doesn't even implement ConfigurableModule. It simply registers four module-private (hidden: true) blueprints on the Entity layer: wire-headless (kind: single, hidden: true😞 Gateway settings (API prefix, CORS origins, rate limits, full headless toggle, signing secret). wire-headless-keys (kind: collection, hidden: true😞 API keys using a native type: page(pageAutocomplete, template=user) field to bind keys to ProcessWire users, plus showIf-controlled scope checkboxes. wire-headless-tokens (kind: collection, hidden: true😞 Refresh tokens with expiration and revocation timestamps. wire-headless-webhooks (kind: collection, hidden: true😞 Outbound webhooks with auto-generated whsec_secrets (type: secret) and lifecycle event checkboxes (page.saved, entry.saved, etc.). // How WireHeadless registers its own internal storage as a hidden Entity blueprint: $entries->registry()->register('wire-headless-keys', [ 'title' => __('API Keys'), 'kind' => 'collection', 'hidden' => true, // Hidden from the general Content > Entries workspace; managed inside Setup > Headless 'columns' => ['title', 'user_id', 'key_prefix', 'rate_limit', 'last_used_at', 'status'], 'fields' => [ 'title' => ['type' => 'text', 'required' => true, 'index' => true, 'columnWidth' => 50], 'user_id' => [ 'type' => 'page', 'inputfield' => 'pageAutocomplete', 'findPagesSelector' => 'template=user, include=all', 'index' => true, 'columnWidth' => 25, ], 'rate_limit' => ['type' => 'integer', 'default' => 120, 'columnWidth' => 25], 'full_access' => ['type' => 'checkbox', 'default' => 0, 'columnWidth' => 100], 'scopes' => [ 'type' => 'checkboxes', 'options' => $availableScopes, 'showIf' => 'full_access=0', 'columnWidth' => 100, ], 'key_hash' => ['type' => 'text', 'index' => true, 'unique' => true], ], ], 'collection', true); Because WireEntries already provides indexed selector queries ($entries->findOne("type=wire-headless-keys, key_hash={$hash}, status=published")), native Inputfield form rendering, and HTMX v4 admin table components (EntriesTable), ProcessWireHeadless gets a complete, consistent admin UI and storage engine out of the box. What Are Your Thoughts? Having Single and Collection entities as a first-class citizen alongside Pages has completely changed how I architect ProcessWire sites and modules: Pages stay focused on what they do best: routable content and site hierarchy. Entities handle standalone collections, global singletons, embedded composite data, and module-owned records—eliminating the need for dozens of extra fieldtypes and helper modules. I'd love to hear from the community: Have you also felt the need for a standalone Entity layer (Single / Collection) alongside ProcessWire's Page tree? How are you currently handling non-page data and page-to-entity relationships (embedding vs. referencing) in your projects? 6
Krlos Posted 16 hours ago Posted 16 hours ago Hi @ukyo, Very interesting work! I'm currently working on a tourism platform, built with ProcessWire, and coincidentally we've been exploring almost exactly the same architectural challenge. We're rethinking our data model and looking for a way to manage structured entities independently of the page tree, while keeping Pages for public-facing content. We've been evaluating ProFields Custom Fields and other approaches, but your WireEntries concept seems particularly aligned with what we're trying to achieve. I was wondering if you have any plans to release WireEntries as a standalone module, either publicly or as a commercial module. Thanks for sharing your work! 1
ukyo Posted 16 hours ago Author Posted 16 hours ago Hi @Krlos Thanks! I live in Marmaris, Turkey—a city that lives on tourism—so I mostly work in the tourism sector, particularly with yacht charter companies. WireEntries grew directly out of the needs I kept running into in these projects. Since building it, I've completely stopped using CustomFields and Combo fields in my projects. Regarding your question: yes, I do plan to release WireEntries as a standalone module (with my Htmx module as a requirement), and it will most likely be a commercial module. Right now, I'm focused on testing and refining it across my active projects before releasing it. Once it's ready, I'll share an update here! 4
Recommended Posts