WP Manifestindependent plugin directory
manifest / events / fuse-2026-wordpress-plugin

Fuse 2026 Registration

by AIME Group · github.com/aime-creative/fuse-2026-wordpress-plugin

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/aime-creative/fuse-2026-wordpress-plugin/archive/refs/heads/main.zip

1. What it is, in one paragraph

Conexsys is the badge-printing vendor for AIME Fuse 2026. They do not have database access — instead, this plugin exposes a single read-only REST endpoint that returns the full attendee list as a flat JSON array, one row per human being who needs a badge. Conexsys (or anyone holding the API key) pulls that endpoint on demand. Nothing is pushed to Conexsys: there is no outbound call, no webhook, no queue. The endpoint is a view over the Supabase tables, computed fresh on every request.


2. The endpoint at a glance

Item Value
Route GET /wp-json/fuse/v1/conexsys
Auth Shared secret, any one of: X-Fuse-API-Key header, ?api_key= query param, or Authorization: Bearer <key>
Where the key lives WordPress option fuse_api_key, set in Fuse 2026 › Settings › Conexsys API Key
Optional param modified_since — ISO 8601 datetime, e.g. 2026-05-20T00:00:00Z. Returns only records changed on or after that time (incremental sync).
Response { "success": true, "count": N, "data": [ …rows… ] } — plus modified_since echoed back when it was supplied
Rate limiting / caching None. Every call re-queries Supabase and re-computes.

Auth fallback worth knowing: if fuse_api_key is empty the endpoint does not become public — it falls back to requiring a logged-in WordPress admin (manage_options). The comparison uses hash_equals(), so it is timing-safe.


3. The row shape

Every row — registrant or guest — carries the same keys, so Conexsys parses one schema:

{
  "badge_type":       "registrant",
  "first_name":       "Jane",
  "last_name":        "Doe",
  "preferred_name":   "",
  "email":            "jane@example.com",
  "phone":            "",
  "company":          "Acme Co",
  "ticket_type":      "vip",
  "tier":             "premium",
  "purchase_type":    "purchased",
  "has_hall_of_aime": true,
  "has_wmn_at_fuse":  false,
  "has_vip_luncheon": false,
  "has_vetted_va":    false,
  "guest_of":         "",
  "registration_id":  "uuid",
  "created_at":       "2026-05-01T12:00:00Z",
  "updated_at":       "2026-05-02T09:14:00Z"
}

Field notes:

  • badge_type — one of registrant, guest, guest_hoa
  • preferred_name / phone — registrants only; always "" on guest rows
  • company — guests inherit the registrant's company
  • guest_of — on a guest row, the registrant's full name; "" on a registrant
  • registration_id — shared by a registrant and all of their guests

badge_type is the only reliable registrant-vs-guest signal. It used to be inferable from ticket_type, but a VIP guest and a VIP registrant now both carry ticket_type: "vip", so that no longer works. The _hoa suffix on guest_hoa flags a Hall of AIME guest.


4. How a request is assembled

  1. Query Supabase for all fuse_registrations for the configured event, with their fuse_registration_guests nested in one PostgREST call.
  2. Page through the results via Fuse_Supabase_API::request_all() — 1,000 rows per page, ordered created_at.desc, id.asc so paging stays stable when timestamps tie. If any page errors it returns the error rather than a partial set; a short read that looks complete is the exact bug this method exists to prevent.
  3. If modified_since was passed, run a second query against fuse_registration_guests for guests changed since the cutoff and pull in their parent registrations. A guest can be edited without touching the parent row, so without this step guest-only changes would be missed.
  4. Scrub add-on flags against the waitlist index (see §5).
  5. Flatten — emit one registrant row, then one row per guest.

5. Three rules not to break

Waitlisted people must never reach Conexsys as session attendees

has_<addon> = true means a confirmed seat in the room — nothing weaker. Waitlist interest lives in a completely separate table, fuse_addon_waitlist. Never join the waitlist table into the Conexsys payload. The export reads the has_* columns directly, which is precisely what makes a waitlister structurally unable to be misread as having a seat.

On top of that there is a defensive scrub: if a has_* flag is somehow true while a waiting queue row still exists (bad manual edit, half-finished promotion, direct SQL), the flag is forced to false and the discrepancy is written to the error log as [Fuse conexsys] scrubbed …. Grep the PHP error log for that string if an attendee insists they were signed up for a session.

Important nuance: the scrub strips add-on flags only. A waitlisted person is still a real event attendee — they keep their ticket, their badge, and their registrant/guest row in the export.

full_name is the badge name and wins

fuse_registrations carries first_name, last_name and full_name, and the three can disagree — rows created through the Supabase admin UI populate full_name only, and full_name is also edited directly in Supabase to fix badge spellings. fuse_reg_split_name() therefore prefers full_name, splitting it into first token + remainder, and only falls back to first_name / last_name when full_name is blank. This matches how the CSV export splits names, so a person reads identically either way.

A guest with no name is still exported

Nameless guest rows used to be skipped, which made the export total come in under the dashboard's. They are now included — they are paid attendees who need a badge — with empty name fields. Conexsys cannot print a usable badge until the name is filled in, so an unnamed-guest count is surfaced in the admin test tool rather than silently dropped.


6. Where to look in the code

Everything is in one file: fuse-registration-plugin/fuse-registration/fuse-registration.php (~5,600 lines). Line numbers are for v2.34.1 and will drift — the function names are the stable handle.

What Where
Route registration (all REST routes) fuse_reg_register_routes() — line 4650; the /conexsys route at 4659
API-key check fuse_reg_conexsys_auth() — line 4697
The export handler — start here fuse_reg_handle_conexsys_export() — line 4744
Waitlist scrub lines 4800–4823 ($scrub_addons closure; Fuse_Waitlist::waiting_index() at 4812)
Row flattening loop at line 4825; registrant row 4837; guest row / badge_type 4882
Name resolution fuse_reg_split_name() — line 51
Supabase client + pagination class Fuse_Supabase_API — line 483; request() 500, request_all() 563
Waitlist registry / queue index class Fuse_Waitlist — line 1015; addons() 1028, column() 1082, waiting_index() 1592
Server-side connection test Fuse_Registration_Ajax::admin_test_conexsys() — line 3182
"Copy URL with key" handler Fuse_Registration_Ajax::admin_get_api_url() — line 3146
Admin page markup templates/admin-export.php
API key settings field templates/admin-settings.php — lines 212–240
Admin page JS assets/js/admin.jstestConexsysApi() 1437, exportConexsys() 1633

There is also a longer internal reference in the repo: Fuse 2026 Registration Plugin — Developer Handoff.md, which covers the whole plugin (schema, flows, all endpoints, waitlist design).


7. How to test it safely

Preferred — from the WordPress admin: Fuse 2026 › Export / Conexsys › Test API Connection. This dispatches the REST request server-side via rest_do_request(), so the API key never reaches the browser. It renders a side-by-side comparison of the export's own tallies (total / registrants / guests / GA / VIP / each add-on) against the dashboard's independent stats query, so any divergence between the two code paths is visible rather than guessed at. That comparison table is the fastest triage tool when the numbers look wrong.

From a terminal:

curl -s -H "X-Fuse-API-Key: $KEY" \
  "https://<site>/wp-json/fuse/v1/conexsys" | jq '.count'

# incremental
curl -s -H "X-Fuse-API-Key: $KEY" \
  "https://<site>/wp-json/fuse/v1/conexsys?modified_since=2026-05-20T00:00:00Z" | jq '.count'

The key is write-only in the admin UI — it is never rendered into the settings page HTML. Use the Copy with key button on the Settings or Export page to get the full URL onto the clipboard.


8. Gotchas

  • The on-page "Export for Conexsys" button is not the same code path as the API. It hits Fuse_Registration_Ajax::admin_export() (line 3775), which returns the raw nested Supabase rows via the single-page request() — not the flattened badge format, and not paginated. Judge the real payload by the REST endpoint or the Test API Connection tool, not by that download.
  • The event scope comes from the fuse_event_id option. If it is unset or wrong, the query filters to nothing and you get a valid-looking empty export.
  • VIP Luncheon seats are largely written by the AIME member portal, not this plugin — but the portal writes has_vip_luncheon on the same rows the export reads, so capacity and queueing are enforced here regardless of which system did the writing.
  • request_all() has a 100-page (100k row) backstop. If it is ever hit, it logs request_all() hit the page cap and returns what it has.
  • The API key also authorizes /post-totals (and is accepted as a legacy fallback on /dashboard-stats). It unlocks full attendee data — treat it as a production secret and never put it in a public page.

Route Auth Purpose
GET /wp-json/fuse/v1/conexsys X-Fuse-API-Key Attendee list for badge printing
GET /wp-json/fuse/v1/dashboard-stats Scoped dashboard token (Conexsys key also accepted for legacy embeds) Aggregate counts only — powers the public GHL dashboard iframe
GET/POST /wp-json/fuse/v1/post-totals X-Fuse-API-Key Posts the weekly ticket totals to Roam; fired from an external cron
POST /wp-json/fuse/v1/stripe-webhook Stripe HMAC signature Handles checkout.session.completed and invoice.paid

10. One process note for whoever edits the plugin

The plugin is installed on the live site by uploading a zip by hand. A source edit is not a delivered change until a new versioned zip is built. Any change requires bumping the version in both places in fuse-registration.php — the Version: header and the FUSE_REG_VERSION constant — because that constant is the cache-buster on the enqueued JS and CSS. Skip the bump and admins keep running the old JS against the new PHP, which looks exactly like "the fix didn't work."