REFORMATION BRIEF R-01 — THE DOOR¶
RTO Onboarding — Search Recognition + Claim Flow + L3 Dashboard Shell¶
UCCA Platform · 18 March 2026¶
R-00 built the rails. R-01 opens the door. This brief is deliberately functional, not beautiful. Wire it in. Make it work. Beauty comes later.
SURFACE: rtopacks-site (rtopacks.com.au) + ucca-api (engine/ucca-engine/backend/)
DO NOT TOUCH: ucca-site, ucca-ir, ucca-keys, ucca-ops, ucca-track
Cloudflare Account: e5a9830215a8d88961dc6c80a8c7442a
engine-db: 0efa8970-0053-4623-8436-4e877af10887
rtopacks-db: 334ac8fb
Auth0 Domain: dev-0jm2d1q8khz1eiqu.au.auth0.com
Auth0 Client ID: w5CGFlNFZQvq05LZemGpeTx8IcK3mu66
RTOpacks Org ID: org_giDMY4hPkEKTiMen
→ ALEX¶
1. Search Bar — RTO Recognition Hook¶
The search bar on rtopacks.com.au already works. Do not redesign it. Add one behaviour:
When a search result is an RTO (not a qualification or unit), check if the user's email domain matches the RTO's registered domain in rtopacks-db.
If the user has typed an email anywhere on the page (or is logged in), and the email domain matches an RTO in the results:
- Add a subtle "Claim this RTO" prompt below that result
- Label: "Is this your RTO? Set up your free account →"
- This is a plain text link or simple button — no modal, no animation yet
- Clicking it starts the claim flow (Section 2)
If no email is known yet:
- Show the prompt anyway: "Are you from [RTO Name]? Set up your account →"
- Clicking it pre-fills the RTO in the claim flow
Search improvement — also do this:
Current fuzzy logic returns RTOs, qualifications, and units. Add a result type indicator to each result so users can visually distinguish:
- RTO badge — green
- Qualification badge — blue
- Unit badge — grey
No redesign. Just a small coloured badge prefix on each result row. One line of CSS.
2. Claim Flow — Three Funnels, One Destination¶
All three funnels land at the same endpoint: a provisioned L3 tenant in engine-db, seeded with TGA data, ready to log in.
Funnel 1 — Organic (via search "Claim this RTO")
User clicks claim → lands on /claim page on rtopacks.com.au
Funnel 2 — Direct (via rtopacks.com.au/claim) Same page, no pre-fill
Funnel 3 — Outbound (via email link)
Link includes ?rto_id=XXXXX param → pre-fills RTO on claim page
The /claim Page¶
Scaffold this page on rtopacks.com.au. Deliberately simple — no theatre, just function.
Step 1 — Identity
[ Your email address ]
[ Your mobile number ] ← include country code selector, default +61
[ Your RTO name or number ] ← autocomplete from rtopacks-db, pre-filled if known
[ Continue ]
On submit:
1. Look up email domain against rtos table in rtopacks-db → find TGA match
2. If matched → pre-fill RTO name/number, show: "We found [RTO Name] (RTO #XXXXX) — is that right?"
3. If no TGA match → show corporate trainer branch (Step 1B below)
4. Send email OTP via Auth0 passwordless email connection
5. Send SMS OTP via Twilio Verify to the mobile number provided
Step 1B — Corporate Trainer Branch (no TGA match)
We couldn't find your organisation in the national training register.
Are you a corporate trainer or enterprise L&D team?
[ Yes, set up as a corporate trainer ]
[ Search again ]
Corporate trainer path → same dual auth → provision tenant with tenant_type = 'corporate', tga_rto_id = NULL → land in same dashboard shell with manual scope setup wizard (stub the wizard for now — just a placeholder card).
Step 2 — Verify
We've sent a code to [email] and a message to [mobile].
Enter both to continue.
Email code: [ _ _ _ _ _ _ ]
SMS code: [ _ _ _ _ _ _ ]
[ Verify & Continue ]
Both must verify before provisioning. If either fails → resend option.
Step 3 — Provision
On successful dual verification:
- Check if tenant already exists in engine-db for this
tga_rto_id: - If yes → log them in, skip to dashboard
-
If no → provision new tenant
-
Provision new tenant:
# In ucca-api — POST /v1/provision-tenant tenant = { "id": uuid4(), "slug": slugify(rto_name), "display_name": rto_name, "tenant_type": "rto", # or "corporate" "tga_rto_id": rto_id, # NULL for corporate "config_json": json.dumps({ "rto_name": rto_name, "rto_number": rto_id, "training_packages": [], # populated from TGA data "onboarding_complete": False }) } db.insert("tenants", tenant) # Create L3 user user = { "id": uuid4(), "email": email, "display_name": display_name, "auth0_sub": None # set on first Auth0 login } db.insert("users", user) # Grant L3 role db.insert("user_tenant_roles", { "id": uuid4(), "user_id": user["id"], "tenant_id": tenant["id"], "role": "L3", "granted_by": "00000000-0000-0000-0000-000000000001" # Tim as L1 granter }) # Write audit log write_audit(...) -
Seed TGA data into tenant config:
- Pull RTO's training packages from rtopacks-db using
tga_rto_id -
Store as
training_packagesarray inconfig_json -
Write to Auth0 — create user in RTOpacks organization:
- Auth0 Management API → create user → add to
org_giDMY4hPkEKTiMen -
Set
app_metadata.tenant_idandapp_metadata.role = "L3" -
Redirect to dashboard with session token
Write platform_audit_log entry: tenant.provision with full payload.
3. L3 Dashboard Shell — rtopacks.com.au/dashboard¶
Scaffold this page. Deliberately a skeleton. Real data, rough layout.
Header:
Four cards — all real data from engine-db + rtopacks-db:
Card 1 — Your RTO
[RTO Name]
RTO Number: XXXXX
Registration Status: [from TGA data]
Registration Expiry: [from TGA data]
Card 2 — Your Scope
Training Packages: [count]
Qualifications: [count]
Units of Competency: [count]
[ View Scope → ] ← stub, links nowhere yet
Card 3 — Course Packs
You haven't purchased any course packs yet.
[ Browse Catalogue → ] ← links back to rtopacks.com.au search
Card 4 — Compliance
⚠ Email domain mismatch detected.
Your RTO is registered with [tga_email_domain] but you signed up
with [user_email_domain]. Update your TGA contact details to match.
[ Learn more → ] ← stub
"Your contact details match your TGA registration."
No nav, no sidebar, no mega menu. Just four cards. This is day one.
4. Twilio Verify — SMS OTP¶
Add Twilio Verify to ucca-api for the dual auth SMS step.
Secrets to add to ucca-api Worker:
TWILIO_ACCOUNT_SID ← Tim to provide from Twilio console
TWILIO_AUTH_TOKEN ← Tim to provide from Twilio console
TWILIO_VERIFY_SID ← Tim to create a Verify Service in Twilio console, provide SID
Endpoints to add to ucca-api:
# POST /v1/verify/send
# Body: { "phone": "+61412345678" }
# Sends OTP via Twilio Verify
# POST /v1/verify/check
# Body: { "phone": "+61412345678", "code": "123456" }
# Returns: { "valid": true/false }
5. Email OTP — Auth0 Passwordless¶
Auth0 already has the email passwordless connection configured. Use it for the email OTP step.
Auth0 Management API call to send email OTP:
POST https://dev-0jm2d1q8khz1eiqu.au.auth0.com/passwordless/start
{
"client_id": "w5CGFlNFZQvq05LZemGpeTx8IcK3mu66",
"connection": "email",
"email": "[user email]",
"send": "code"
}
Verify OTP:
POST https://dev-0jm2d1q8khz1eiqu.au.auth0.com/oauth/token
{
"grant_type": "http://auth0.com/oauth/grant-type/passwordless/otp",
"client_id": "w5CGFlNFZQvq05LZemGpeTx8IcK3mu66",
"client_secret": "[from 1Password]",
"username": "[user email]",
"otp": "[code entered]",
"realm": "email",
"scope": "openid profile email"
}
Note: Auth0 default email provider active for now — dev only. SendGrid wired in R-02.
6. Schema Addition — rtopacks-db¶
Add one column to the RTO table in rtopacks-db to support domain matching:
Populate from existing email data:
UPDATE rtos
SET email_domain = LOWER(SUBSTR(email, INSTR(email, '@') + 1))
WHERE email IS NOT NULL AND email != '';
Create index:
7. New API Endpoints Summary¶
All in ucca-api (engine/ucca-engine/backend/):
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/v1/rto/lookup |
GET | None | Look up RTO by email domain or RTO ID |
/v1/verify/send |
POST | None | Send SMS OTP via Twilio |
/v1/verify/check |
POST | None | Verify SMS OTP |
/v1/provision-tenant |
POST | None* | Provision new L3 tenant |
/v1/dashboard |
GET | L3 JWT | Return dashboard data for tenant |
*/v1/provision-tenant — protected by dual-auth token from the claim flow, not a public endpoint.
OPS STUB TASK¶
Add to ops console task queue under IDENTITY & TENANCY world:
R-01 · THE DOOR
Search recognition hook live ✓
/claim page live ✓
Dual auth flow live (email OTP + SMS OTP) ✓
Tenant provisioning endpoint live ✓
L3 dashboard shell live ✓
TGA data seeded on provision ✓
email_domain index added to rtopacks-db ✓
→ TIM¶
What this brief does:
R-01 builds the front door of the platform. Three things happen:
-
The search bar gets smarter — when an RTO appears in search results, it shows a "claim this RTO" prompt. The search also gets result type badges (RTO / Qualification / Unit) so users can orient themselves faster. No redesign — just two small additions to what's already there.
-
The claim flow goes live — an RTO admin types their email and mobile, we match them to TGA data, send a code to both email and SMS, they verify both, and their tenant is automatically provisioned with their TGA data pre-loaded. Corporate trainers get the same flow with a manual setup wizard (stubbed for now). Three funnels (organic search, direct URL, outbound email link) all land at the same place.
-
The L3 dashboard shell goes live — after claiming, the RTO lands on a simple four-card dashboard: their RTO details, their scope of registration, their course packs (empty for now with a "browse catalogue" link), and a compliance flag if their email domain doesn't match their TGA registration. Rough but real. Everything on it is live data.
What you need to do before Alex can execute:
- Provide Twilio credentials: Account SID, Auth Token, and create a Verify Service in the Twilio console (console.twilio.com → Verify → Services → Create). Give Alex the three values.
- Auth0 Client Secret — Alex already has this from R-00.
What this is NOT: - Not beautiful — four plain cards, basic form, no animation - Not the search redesign — that's a later brief - Not Stripe — payments come in R-02 - Not the full scope explorer — that's R-03
The goal is one RTO claiming their account and landing in their dashboard. That's the coin in the jukebox.
REFORMATION BRIEF R-01 · UCCA Inc · 18 March 2026 "Open the door. Make it pretty later."