Provision a New Registration Portal Tenant
Overview
This procedure onboards a new organisation onto the Event and Membership System so that its participants can register through the shared registration portal and pay through that organisation’s own WooCommerce site.
The registration portal is multi-tenant: a single deployment serves every organisation. Onboarding a tenant is therefore a configuration exercise (rows in databases, a WordPress site, one line in the portal’s deployment manifest) — not a new portal deployment. See Multi-Tenancy for the architecture this procedure operationalises.
|
Today this is a manual procedure executed through SQL, WP-CLI, and a Git-Ops manifest edit. It is defined by the outcome it produces, not the tools used. The future admin-portal use cases S01 Super-Tenant (create/manage tenants) and T01 Tenant Admin will automate these same steps. Keep this page current as the source of truth for those use cases. |
What a tenant is made of
A fully provisioned tenant is the following chain, spread across two databases and a WordPress site:
| Artefact | Lives in | Purpose |
|---|---|---|
|
admin-service Event DB |
The multi-tenancy root. Owns events, memberships, orders. |
|
admin-service Event DB |
Payment gateway + banking config for the organisation. One per |
|
admin-service Event DB |
Authenticates the event-payment plugin’s calls into admin-service ( |
|
admin-service Event DB |
Drives the registration form/workflow for the organisation’s events. |
Default WooCommerce product + |
WordPress site + admin-service Event DB |
A default/general product for the plugin’s Default-product setting, mirrored by an admin-service |
WordPress + WooCommerce site |
The organisation’s own hosting |
Money-of-record for online payment; |
|
registration portal Gateway DB |
Maps the public hostname → |
Deployment host + DNS |
ArgoCD manifest + DNS |
Adds the tenant’s hostname to the shared portal ingress and points DNS at it. |
The runtime resolution chain is Tenant (Gateway DB) → RegistrationSystem (Event DB) →
Organisation (Event DB): on each request TenantResolutionFilter matches the hostname to a
tenant row, carries registration_system_id to admin-service /api/authenticate, and
admin-service mints a JWT scoped to the resolved orgId.
Databases and access
Both databases sit on the idealogic-prod MySQL cluster (reached by SSH tunnel — see the
Number & Tag Runbook for the tunnel recipe), but
they are separate schemas, and this is the single most common mistake:
| Rows | Schema (ems-prod) | Notes |
|---|---|---|
|
|
The admin-service Event DB. In the 2026 H2 parallel run the ems-prod admin-service points at |
|
|
The portal’s own fresh UI-local Gateway schema. |
Confirm the live schema names against the ArgoCD manifests
(ems-admin-service-prod.yml, ems-registration-portal-prod.yml) before running any
INSERT — the parallel-run topology is described in
design-journal/2026-07/2026-h2-release.adoc and will change at cutover.
|
Prerequisites
Gather these before you start:
-
Organisation details — legal/display name.
-
Public registration hostname — e.g.
register.<org-domain>— and control of its DNS. -
WooCommerce site — a WordPress site (existing or new) you can install plugins on, and its base URL (typically
https://<site>/wp-json/payment-api/v1). -
Payment methods — which of Online (
O), EFT (E), Manual (M) the organisation will offer. EFT requires banking details. -
OIDC/identity — if the tenant uses a dedicated identity provider (e.g. a distinct Azure B2C user flow), its issuer URL, client id and secret. Otherwise the portal default applies.
-
Cluster access — SSH tunnel to
idealogic-prod, and write access to the~/dev/idl-xnl-jhb-rc01/argocd/Git-Ops repo.
Phase A — Admin-service data (Event DB, wpca_prod)
A1. Create the Organisation
INSERT INTO organisation (name)
VALUES ('<Organisation display name>');
-- note the generated id → <organisation_id>
A2. Create the RegistrationSystem
One row per payment configuration. type identifies the integration (WP2 = WordPress v2,
the current WooCommerce integration); base_url is the WooCommerce payment-API root;
auth_key is the plugin’s callback API key, which the event-payment plugin auto-generates at
start-up — you copy it in once the plugin is installed (B7); payment_methods is a
comma-separated set of O (Online/WooCommerce), E (EFT — needs banking columns),
M (Manual/pay-at-counter).
INSERT INTO registration_system
(name, type, base_url, auth_key, payment_methods, organisation_id)
VALUES (
'<Org> Website', -- descriptive name
'WP2', -- integration type
'https://<site>/wp-json/event-payment-api/v1', -- WooCommerce payment API root
NULL, -- auth_key: set from plugin epa_callback_api_key (B7)
'O', -- O / E / M (comma-separated)
<organisation_id>
);
-- note the generated id → <registration_system_id>
auth_key cannot be filled here yet — it is the value the plugin generates for
epa_callback_api_key at start-up. Insert the row now, then in B7 copy the plugin’s callback key
back into this row (UPDATE registration_system SET auth_key = '<value>' WHERE id = <registration_system_id>;).
|
For EFT (payment_methods includes E) also populate the banking columns:
UPDATE registration_system
SET bank_name = '<Bank>', account_name = '<Account holder>',
account_number = '<Account number>', branch_code = '<Branch code>'
WHERE id = <registration_system_id>;
|
The |
A3. Create the admin API key for the event-payment plugin
The event-payment plugin calls admin-service (order status sync) with an X-API-KEY header;
ApiKeyFilter validates it against the api_key table, and api_key_permission scopes the key
to the organisation so calls made with it only reach that org’s data. Generate a fresh UUID v4
for key_value; this same value goes into the plugin’s Admin API Key field (B7).
-- 1. The key itself
INSERT INTO api_key (key_value, description, active, created_date)
VALUES (
'<uuid-v4>', -- generate: uuidgen
'Event payment plugin - <Org>',
true,
NOW()
);
-- note the generated id → <api_key_id>
-- 2. Scope it to the organisation
INSERT INTO api_key_permission (api_key_id, organisation_id, role)
VALUES (
<api_key_id>,
<organisation_id>, -- MUST match the tenant's organisation
'ADMIN' -- role granted for that organisation
);
|
The |
This admin API key (plugin → admin-service) is distinct from registration_system.auth_key
(A2), which secures the reverse direction (portal → WooCommerce, the plugin’s epa_callback_api_key).
A4. Create the ProcessDefinition(s)
Each event registration flow is driven by a process_definition and its ordered
process_step rows. Definitions may be global or organisation-scoped. Follow the
"Creating a Process Definition" steps in
Process Entities and clone an existing
organisation’s definition as the starting point rather than building from scratch.
A5. Create the default admin-service product
Mirror the default WooCommerce product (B3) with a product row whose external_id holds the
WooCommerce product ID. Create the default product now; the per-event entry/fee products are
created per event — see Configure an Event, step E7.
|
Create the WooCommerce product (B3) first — you need its ID to populate |
INSERT INTO product (name, external_id, price)
VALUES (
'Default Entry', -- general/default product
'<woocommerce-product-id>', -- the ID WooCommerce assigned to the product
<price>
);
The product table has no registration_system reference (it is keyed only by
external_id), so product IDs must be kept distinct across tenants — see
Configure an Event, step E7, for detail.
|
Phase B — WordPress / WooCommerce site
Stand up (or reuse) the organisation’s WordPress site and install the stack. The dev bootstrap
in design-journal/2026-01/wp-deployment.adoc is the canonical, idempotent recipe — the same
WP-CLI steps apply to production with production values. The commands below can equally be done
through the WordPress/WooCommerce admin UI.
Two distinct payment layers are configured in this phase, and it helps to keep them separate:
-
The money processor — a WooCommerce payment gateway (Payfast or Paygate) that actually takes the customer’s money (B4–B6).
-
The EMS event-payment plugin —
event-payment-plugin-woocommerce, which creates the WooCommerce order from a registration and syncs its status back to admin-service (B1, B7). It does not process money; it hands checkout to whichever WooCommerce gateway is enabled.
B1. Install the plugins
WooCommerce itself, the EMS event-info plugin, and the EMS event-payment plugin:
wp plugin install woocommerce --activate
wp plugin install "https://${GITHUB_TOKEN}@github.com/christhonie/event-info-plugin/releases/download/${EIP_PLUGIN_VERSION}/event-info-plugin-${EIP_PLUGIN_VERSION}.zip" --activate
wp plugin install "https://${GITHUB_TOKEN}@github.com/christhonie/event-payment-plugin-woocommerce/releases/download/${EPA_PLUGIN_VERSION}/event-payment-plugin-${EPA_PLUGIN_VERSION}.zip" --activate
B2. Configure the WooCommerce store
Set the organisation’s store identity, address, currency and base pages. Via the admin UI this is WooCommerce → Settings → General (plus the setup wizard); the equivalent options are:
# Store identity & address (WooCommerce → Settings → General)
wp option update woocommerce_store_address "<Street address>"
wp option update woocommerce_store_city "<City>"
wp option update woocommerce_default_country "ZA:WC" # Country:State
wp option update woocommerce_store_postcode "<Postcode>"
wp option update blogname "<Organisation display name>"
# Currency & selling options
wp option update woocommerce_currency "ZAR"
wp option update woocommerce_enable_coupons "yes"
# Core pages (created idempotently by the bootstrap: Shop / Cart / Checkout / My Account)
The bootstrap script creates the Shop/Cart/Checkout/My-Account and legal pages and wires their
IDs into the matching woocommerce_*_page_id options — reuse it rather than hand-creating pages.
B3. Create the default WooCommerce product
Create one Simple product — a default/general product — for the plugin’s Default product setting (B7). Make it Virtual, give it a name/description, and tick Inventory → "Limit purchases to 1 item per order" (quantity is always 1 because participant metadata attaches per line item). Set its Catalog visibility to Hidden so it does not appear in the public shop.
Record its WooCommerce product ID — A5 needs it for product.external_id and B7 uses it as the
Default product.
| The per-event entry products and fees (one per distance/category, the CSA day licence, the Entries / CSA product categories, the exact product mechanics and how to link products to event categories and the day-licence field) are created per event — see Configure an Event, step E7. |
B4. Install and enable the payment provider
Install the gateway plugin for the chosen provider and activate it. The organisation uses either Payfast or Paygate — install only the one in use. (Paygate is a Payfast affiliate; its WooCommerce plugin is published by Payfast — Payfast/woocommerce-gateway.)
# Payfast (plugin by WooCommerce)
wp plugin install woocommerce-payfast-gateway --activate
# — or — Paygate (PayWeb, plugin by Payfast)
wp plugin install paygate-payweb-for-woocommerce --activate
Then enable it as a payment option and make sure the dev-only Direct Bank Transfer (BACS) stub is not enabled in production: WooCommerce → Settings → Payments, toggle the provider on. The enabled gateway is what the customer is redirected to at checkout after the event-payment plugin creates the order.
B5. Create / obtain the provider account and retrieve credentials
Each provider needs a merchant account and exposes an integration credential set with provider-specific field names. Myriad Events already holds accounts with both providers — obtain the credentials from the provider’s merchant dashboard (or from the Myriad Events account owner) rather than creating a new account unless this organisation bills separately.
| Provider | Integration key field(s) | API secret |
|---|---|---|
Payfast |
Merchant ID, Merchant Key |
Passphrase (salt passphrase) |
Paygate (PayWeb3) |
PayGate ID |
Encryption Key |
|
The field names differ per provider, but the roles are the same: one or more public merchant/identity fields, plus one secret used to sign/verify the payment handshake. Treat the secret (Payfast Passphrase / Paygate Encryption Key) as sensitive — store it in the secret manager, never in this guide or in Git. Use the provider sandbox credentials for a test run before switching to live. |
B6. Configure the payment provider in WooCommerce
Enter the B5 credentials into the gateway settings (WooCommerce → Settings → Payments → <provider>): Payfast’s Merchant ID / Merchant Key / Passphrase, or Paygate’s PayGate ID / Encryption Key. Set the return/notify URLs to this site, and leave sandbox/test mode on until the end-to-end verification in Phase E passes, then switch to live.
B7. Configure the EMS event-payment plugin
Configure the plugin (WooCommerce → EMS Event Payment, or the plugin settings screen) so it can reach admin-service and so admin-service accepts its calls. The key fields, with the option key each maps to:
| Plugin field | Option key | Value |
|---|---|---|
Portal URL |
|
This tenant’s registration portal host, e.g. |
Default product |
|
The general/default WooCommerce entry product (B3) used when no specific product maps. |
Admin API URL |
|
The admin-service public URL, e.g. |
Admin API Key |
|
The UUID v4 created in the |
The equivalent WP-CLI form (full option reference in WordPress Payment Integration):
wp option update epa_entry_portal_url "https://register.<org-domain>"
wp option update epa_default_prod_id "<default-woocommerce-product-id>"
wp option update epa_admin_api_url "https://admin-service.idealogic.co.za"
wp option update epa_admin_api_key "<uuid-v4 from A3>"
wp option update epa_enable_logging "yes"
|
Do not set
|
|
Option-key naming differs between the bootstrap script and the architecture doc
( |
The inbound/outbound contract the event-payment plugin honours (order create in, status sync out) is:
-
Inbound order create:
POST /wp-json/payment-api/v1/order/event/create/→{ redirectURL, orderNumber, orderExternalId } -
Outbound status sync:
PATCH /api/orders/participant/{orderId}(event) /POST /api/memberships/order(membership),X-API-KEYheader -
Status mapping:
processing/completed→PAID,cancelled/failed→CANCELLED,refunded→REFUNDED
See payment-stub/README.md for the dev-mock equivalent of this contract.
Phase C — Registration portal tenant row (Gateway DB, event_registration_ui_ems_prod)
Map the public hostname to the RegistrationSystem created in A2. This is what
TenantResolutionFilter looks up on every request.
INSERT INTO tenant (name, domain, registration_system_id)
VALUES (
'<Organisation display name>',
'register.<org-domain>', -- full hostname; matched by TenantResolutionFilter
<registration_system_id>
);
For a dedicated identity provider, also set the OIDC columns:
UPDATE tenant
SET issuer_url = '<issuer-url>', client_id = '<client-id>', client_secret = '<client-secret>'
WHERE domain = 'register.<org-domain>';
|
The |
Phase D — Deployment host and DNS
The tenant’s hostname is added to the shared portal ingress — no new deployment.
-
Edit
~/dev/idl-xnl-jhb-rc01/argocd/ems-registration-portal-prod.ymland add the hostname underingress.extraHosts:ingress: hostname: registration-v2.myriadevents.co.za extraHosts: - register.<org-domain> # <-- new tenant -
Commit and let ArgoCD sync (this app is manual sync during the parallel run — sync it).
-
Point DNS for
register.<org-domain>at the cluster ingress.cert-manager(letsencrypt-prod) issues TLS for the new host automatically once DNS resolves. -
If the tenant uses a shared/central identity provider (e.g. the Myriad Events Azure B2C user flow), add the new host’s redirect URI to that app registration.
Phase E — Verify
-
https://register.<org-domain>resolves, serves the portal over valid TLS, and the correct organisation’s branding/events appear (confirms Tenant → RegistrationSystem → Organisation). -
Create a test registration and reach checkout — confirm the order is created in WooCommerce and
registration_system_idis set on the admin-service order. -
Complete/cancel a sandbox payment through the provider (Payfast/Paygate) and confirm the status callback reaches admin-service through
ApiKeyFilter(order transitions toPAID/CANCELLED). -
Only once the sandbox run is clean, switch the WooCommerce provider from sandbox/test mode to live (B6) and disable any dev-only Direct Bank Transfer (BACS) gateway.
Worked example: Tour de Worcester (2026)
| Input | Value |
|---|---|
Public WooCommerce site |
|
Registration portal host |
|
Portal deployment |
|
Payment plugin |
|
Register CTA pattern |
|
Online processor |
WooCommerce → Payfast gateway (Paygate is a Payfast affiliate; the Payfast gateway is in use) |
WooCommerce products |
Default product created here; per-event entries + CSA day licence are set up per event — see Configure an Event (Rooibos-style categories/courses/races). |
|
assigned during provisioning — not recorded here by policy |
Discount-code / UTM carry-through for this tenant (landing ?discount=CODE, host-only
epa_discount_code cookie, 72h TTL) is covered separately in
design-journal/2026-07/registration-discount-code-carry.adoc; coupon values themselves are
owned by the WooCommerce coupon plugin, not by EMS.
Related
-
Configure an Event — the per-event setup (categories, races, entry/fee products) that follows tenant provisioning.
-
Multi-Tenancy — the architecture this procedure operationalises.
-
WordPress Payment Integration — plugin option reference and REST contract.
-
Process Entities — creating ProcessDefinitions.
-
S01 Super-Tenant / T01 Tenant Admin — the admin-portal use cases that will automate this procedure.