Fuse 2026 Registration
by AIME Group · github.com/aime-creative/fuse-2026-wordpress-plugin
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.zip1. 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 ofregistrant,guest,guest_hoapreferred_name/phone— registrants only; always""on guest rowscompany— guests inherit the registrant's companyguest_of— on a guest row, the registrant's full name;""on a registrantregistration_id— shared by a registrant and all of their guests
badge_typeis the only reliable registrant-vs-guest signal. It used to be inferable fromticket_type, but a VIP guest and a VIP registrant now both carryticket_type: "vip", so that no longer works. The_hoasuffix onguest_hoaflags a Hall of AIME guest.
4. How a request is assembled
- Query Supabase for all
fuse_registrationsfor the configured event, with theirfuse_registration_guestsnested in one PostgREST call. - Page through the results via
Fuse_Supabase_API::request_all()— 1,000 rows per page, orderedcreated_at.desc, id.ascso 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. - If
modified_sincewas passed, run a second query againstfuse_registration_guestsfor 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. - Scrub add-on flags against the waitlist index (see §5).
- Flatten — emit one
registrantrow, 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.js — testConexsysApi() 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-pagerequest()— 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_idoption. 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_luncheonon 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 logsrequest_all() hit the page capand 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.
9. Related endpoints
| 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."