Eventonomy

Architecture

Eventonomy is built in seven well-separated layers. Understanding them tells you exactly where to put code, how to resolve dependencies, and why certain patterns are required.

Full reference: docs/ARCHITECTURE.md in the plugin root. This page is a navigable summary; the source file is the authority.

What You Will Learn

  • The seven layers and what belongs in each one
  • How auto-discovery keeps parallel development conflict-free
  • The data model: tables, indexes, and JSON columns
  • How the Free / Pro seam works

The Seven Layers

Layer Location Rule
Bootstrap eventonomy.php, Core/Plugin Wiring only, no logic
Container Core/ServiceContainer bind() (swap one) / tag() (add to collection) / get() / tagged()
Repository Repository/* + Contracts/*Interface The only SQL layer; prepared statements; query-args filters; cache incrementor
Services Services/* Business logic; returns arrays or WP_Error; fires before/after hooks; never echo
Surface REST/Controller/*, CLI/*, Blocks/*, src/blocks/* Thin: validate → service → format
Templates templates/* Presentation only; zero DB; $view_data via evnm_get_view_data()
Admin Admin/* Custom UI; REST/Settings-API backed; no alert/confirm

The container accessor: evnm( ContractClass::class ) is the public API for resolving any service. Always resolve a contract, never new a concrete class.

Auto-Discovery (The Backbone)

Auto-discovery keeps parallel feature branches conflict-free: you never edit a shared registrar to add something new.

What you want to add How to add it
A new service or repository Add one *Provider.php to includes/Providers/
A new REST endpoint Add one *Controller.php to includes/REST/Controller/
A new Gutenberg block Add a src/blocks/*/block.json directory
A new admin page Add one *Page.php implementing PageInterface to includes/Admin/

The registrars (RestRegistrar, BlockRegistrar, AdminMenu, ServiceContainer::load_providers()) scan those paths automatically.

Data Model

Free creates ten custom tables, all InnoDB / utf8mb4:

Table Contents
evnm_events Core event rows. Hot-path filter/sort fields (status, author_id, featured, space_id, city) are real indexed columns, and "upcoming" sorts on the denormalized next_occurrence_utc pointer.
evnm_occurrences One row per event instance. Recurring events generate many rows here via OccurrenceMaterializer. Keyset-paginated on (id).
evnm_rsvps Attendee registrations. Cursor-paginated (the largest table on busy sites).
evnm_tickets Ticket types per event.
evnm_orders Registration orders: $0 (Free) and paid (Pro).
evnm_meta Key/value metadata for any resource type. One row per key, not many.
evnm_venues Shared venue catalog with optional lat/lng.
evnm_organizers Shared organizer catalog.
evnm_webhook_events Gateway webhook ids already seen. The unique (gateway, event_id) is the replay-idempotency mechanism.
evnm_notifications The in-app notification feed, also served to the mobile app.

Pro adds five: evnm_follows (following), evnm_earnings + evnm_payouts + evnm_payout_debts (the commission ledger) and evnm_pro_feeds (calendar-sync subscriptions).

Note that there is no start_local and no category column on evnm_events. Start and end times live on evnm_occurrences (start_utc/start_local), and categories/tags are native WordPress taxonomies. Full column and index detail: Database Schema.

JSON columns (cold data only, never queried in WHERE): organizer, venue (snapshot on the event), recurrence, settings, response, meta, billing, early_bird, config. Do not add WHERE column->'$.key' queries; keep indexes on real columns.

Categories and tags use native WordPress taxonomies bound to the evnm_event object-type string, not a CPT, just the taxonomy registration.

Event Status Values

Eventonomy uses its own status strings, not WordPress's publish:

draft | pending | published | cancelled | private

Important: Use 'published', not 'publish'. WordPress pages that Eventonomy creates use 'publish' because they are standard WP pages, but the event rows in evnm_events use 'published'.

Scale Design

  • Cursor (keyset) pagination on evnm_occurrences and evnm_rsvps, which avoids OFFSET scans on large tables.
  • Cache incrementor invalidation - wp_cache_set_last_changed() keeps shared data fresh without a per-row flush strategy.
  • Atomic ticket increment - TicketRepository::increment_sold() uses an atomic SQL increment to prevent oversell under concurrent load.
  • Background fan-out - notifications and occurrence expansion run via cron / Action Scheduler, not inline in the request.

The Free / Pro Seam

Pro extends Free only through the public extension surface:

  • evnm_* hooks (actions and filters)
  • Eventonomy\Contracts\* interfaces resolved through evnm()
  • The service container's tag() and bind() methods

Pro never imports Free concrete classes. The payment seam is apply_filters('evnm_process_payment', null, $order, $request). Free returns WP_Error(402) for paid orders; Pro hooks in to capture payment and returns a result.

This means your own add-ons can extend Eventonomy with the same depth that Pro does.

What's Next?

Browse the full REST API contract.

REST API Reference →