Competitive intel · 2026-05-22

Data Model Inferred

So what: WeCourts's domain model is bigger and more padel-specific than OpenRally's v0.5 schema, and centers around a canonical WPPR Player identity that links a person to every match in their history regardless of which organizer ran it. The model is similar to OpenRally's at the Tournament/Match/Team layer but adds Organizer tier, Club facilities catalogue, Community, Rating cycle + projection, Magic link, Incident log, and WPPR submission queue as first-class entities.

This is inferred from the rendered public surface, Featurebase changelog notes, network observations, and event-page URL patterns — not from a database dump. Each table notes its evidence source.


Core entities

Player

Inferred from: 02-wppr.text.txt (Players Hub), 13/29-event-*.text.txt (event cards), 43-may-grand-clash.text.txt (LFP cards), 34-featurebase-changelog.text.txt Mar 13 entry.

Field Type Notes / Evidence
wppr_id string (P + 5 hex chars, e.g. P67152) Public ID, shareable, primary key for cross-tournament linkage
display_name string Composed of first + last; canonicalized at WPPR submission
first_name string Stored separately (used in match cards)
last_name string
nationality string "Russian", "Jamaican", "Argentine" etc — flag-icon emitted from this
home_city string "Dubai, UAE" — appears under nationality
home_country string ISO-ish (e.g., "ESP", "IRN", "JOR")
gender enum "Male", "Female", "Mixed" not visible per-player but exists at tournament level
playing_hand enum "Right Hand", "Left Hand" — observed 02-wppr.text.txt
preferred_side enum "Right Side", "Left Side", "Both Sides" — padel-specific
experience_years int e.g. "EXP 5 years" — observed 43-may-grand-clash.text.txt
wppr_rating decimal 0.000-8.000, 3 decimal places observed (e.g. 3.954, 4.391)
wppr_category enum D-, D, D+, C-, C, C+, B-, B, B+, A-, A, A+, AS (13 tiers)
wppr_status enum WPPR (full) / WPPR IR (intermediate/initial) / unrated — observed 02-wppr.text.txt
is_wppr_verified boolean "WPPR Verified Badge" per 34-featurebase-changelog.text.txt Mar 13
mobile_e164 string Stored but masked in public views (last 4 digits shown) per Apr 30 changelog
auth_method enum "mobile_otp" (player path) / "email_password" (organizer path) / "google_oauth"
created_at timestamp Implicit
country_rating_scheme string "country-aware letter bands" per May 13 changelog — implies per-country band mapping
confidence_level enum/decimal Tap-to-explain on player profile — exists but spec not observed
matches_played int "238 matches" on dev-API sample player
win_rate decimal "67%" on dev-API sample player
group_stage_win_rate decimal Career stat per Mar 13 changelog
knockout_win_rate decimal Career stat per Mar 13 changelog
gold_medals int Career stat per Mar 13 changelog
silver_medals int Career stat per Mar 13 changelog
global_position int / string "Where you stand globally" — sortable position
share_url_slug string Shareable rich-preview URL — likely /p/{wppr_id} or /player/{slug}

Open questions: Are there merge / canonical-pointer rows for duplicate profiles? Featurebase complaints suggest no clean merge — duplicates exist as separate rows and users can't resolve.

Tournament

Inferred from: 13-sample-event.text.txt (/e/ZE7A), 29-event-5GLP.text.txt (Groups + KO), 45-americano-event.text.txt (/t/33F8), 43-may-grand-clash.text.txt (drawer).

Field Type Notes / Evidence
short_code string (4 chars, base62 — observed: ZE7A, 5GLP, N3UE, ZYZM, T6A6, 83SU, WNEH, NZ2E, 33F8) Used in URL path; presumably has random / Hashids generation
route_type enum e (scored, WPPR-eligible, bracket UI) vs t (listed-only, external-register) — observed in URL patterns
name string "TURBO 3.0
slug string Not directly observed — may not exist (short_code substitutes)
description_md text Free-text with Markdown (emoji, \r\n, asterisks for bold) — 43-may-grand-clash.text.txt
start_date date "Mon, May 18"
start_time time "9:00 PM"
end_date date Optional (multi-day)
timezone string "Dubai local time" displayed; almost certainly tied to club's country
entry_fee_amount decimal e.g. 250, 30, 35
entry_fee_currency enum AED, JOD confirmed; supports multi-currency
entry_fee_basis enum "per player" (vs per-team) — observed in drawer
venue_id (FK) UUID Links to Club entity
organizer_id (FK) UUID Links to Organizer entity
submitted_by string Free-text or FK to user — "Submitted by Padel Clash"
assistant_organizer_name string e.g. "Mousa darwish", "Bati Varfi", "Mulham Hmidan" — appears next to "BY {organizer}"
tournament_format enum single_elim (KO) / groups_plus_ko / round_robin / koc (King of Court) / americano (testing)
team_count_capacity int "teams / 32" in card
team_count_registered int "21 teams / 32" — denormalized for card display
levels array enum ["B-", "C+", "C", "C-"] for May Grand Clash; ["D-", "D", "D+", "C-", "C"] for Jordan tournament — multiple WPPR categories per tournament allowed
level_summary string "Team Level 7.7" / "Open D" / "C +2 more" — denormalized display label
team_level_sum decimal Inferred: "Team Level 7.7" likely means sum of both partners' WPPR scores ≤ 7.7 — gating
gender_category enum open / mixed (with 👫 icon) / inferred mens / womens
kids_category enum/string "DSG U14 Boys", "U16 Boys", "U18-Mother's Day" — youth tournaments are first-class
prize_pool_total_amount decimal "AED 7500 Total Prize" in title
prize_tiers json/array [{rank: 1, amount: 5000}, {rank: 2, amount: 2000}, {rank: '3-4', amount: 500}] — structured display in drawer
prize_pool_conditional_clause text "Full prize granted only when reaching at least 30 out of 32 teams" — free-text in description
wppr_verification_status enum not_submitted / submitted_awaiting_review / published_to_wppr / waived
status enum draft / registration_open / registration_closed / live / ended / results_submitted / published / cancelled
is_team_list_hidden boolean "Team list hidden by organizer" — explicit toggle
is_private boolean "Private Tournaments" planned — implies false default
is_broadcast_live boolean Free-text "BROADCASTED LIVE" in name today; may become structured later
mgg_enabled boolean Minimum Games Guarantee toggle (gated to 12/14/18-team formats) per Apr 17 changelog
direct_advance_enabled boolean Direct Advance toggle (gated to 10/11/20/21/22/23/26/27-team formats) per Apr 22 changelog
registration_closes_at timestamp "Registration closes by Thursday 21st May at 9:00 PM" in description — likely also a structured field
created_at timestamp Implicit
updated_at timestamp Implicit

Team (Registration)

Inferred from: event detail pages with team rows, "Looking-for-Partner" cards, 34-featurebase-changelog.text.txt Apr 30.

Field Type Notes
id UUID
tournament_id (FK) UUID
player_1_wppr_id (FK) string
player_2_wppr_id (FK, nullable) string NULL while player is in LFP (solo, looking for partner)
team_status enum pending_partner / confirmed / waitlisted / withdrawn / disqualified / cancelled (cancelled = tournament voided)
payment_status enum unpaid / pending_confirmation / paid / refunded (mostly offline-tracked today)
seed_position int (nullable) Set when bracket draws
group_id (FK, nullable) UUID NULL until group draw
lfp_visibility enum public (shows in LFP listing) / private (hidden from LFP feed)
disqualification_outcome enum (nullable) forfeit_wins / zero_unplayed / void_all — set when SOS disqualifies
created_at timestamp

Match

Inferred from: bracket pages and "Recent Results" cards.

Field Type Notes
id UUID
tournament_id (FK) UUID
team_a_id (FK) UUID
team_b_id (FK) UUID
bracket_round enum group_stage / r32 / r16 / qf / sf / final + Pre-rounds for MGG (pre_r16 / pre_qf / pre_sf)
group_label (nullable) string "Group A" / "B" / "C" / "D"
match_position int Position within round (for bracket UI)
team_a_score int (per set, array) e.g. [6, 4, 7] — best-of-3 sets, third set may be 7-6 super-tiebreak
team_b_score int (per set, array)
winner_team_id (FK, nullable) UUID
status enum scheduled / in_progress / completed / walkover / voided
played_at timestamp
is_walkover boolean Distinct from regular complete (e.g., forfeit-wins after DQ)
court_id (FK, nullable) UUID Specific court assignment — exists in Operator Console "advanced" tab, not observed in public surfaces
referee_id (FK, nullable) UUID Inferred — there are "magic links" likely for referees
rating_impact_team_a_player_1 decimal (nullable) Stored AFTER WPPR publishes; "every match clearly marked as counted or unrated" per May 13 changelog
rating_impact_team_a_player_2 decimal (nullable)
rating_impact_team_b_player_1 decimal (nullable)
rating_impact_team_b_player_2 decimal (nullable)
eligibility_for_wppr enum eligible / ineligible / pending

Club / Venue

Inferred from: 20-club-detail.text.txt (ISD Padel), 47-padel-art-club.text.txt.

Field Type Notes
id UUID (observed in URLs 9d3fffbf-2e00-...) Public UUID, not slug
name string "ISD Padel", "Padel Art"
city string "Dubai"
country string "United Arab Emirates"
address string "ISD Dubai Sports City - Dubai" — free-text
latitude, longitude decimal Google Maps embed implies geocoding
whatsapp_e164 string "971526712890"
phone_e164 string (same as WhatsApp in observed sample)
website_url string (nullable)
instagram_handle string (nullable) Instagram embed observed
business_hours json Day-by-day open/close — 7 rows in observed samples
total_courts int "10" / "9"
indoor_courts int / boolean "Indoor / Available" — possibly count or flag
outdoor_courts int / boolean Same
facilities array enum ["changing_rooms", "court_lighting", "equipment_rental", "mobile_app", "padel_courts", "parking_available", "professional_coaching", "shower_facilities", "snack_bar", "sports_courts", "wheelchair_accessible", "locker_rooms", "online_booking", "membership_programs", "public_transport_access", "tournament_hosting", "equipment_available"] — explicit tag set observed
tags_auto array ["excellent_customer_reviews", "popular_destination", "easy_to_locate", "online_presence", "open_7_days", "extended_hours", "established_business", "very_popular_destination"] — auto-tagged scoring (likely from Google reviews + tournament count)
tournaments_hosted_count int "1660 Tournaments" / "762 Tournaments"
verified_contacts array ["whatsapp"]
listed_since date "July 23, 2025"
description text Free-text with emoji — "🎾 8 Indoor Padel Courts 📍Al Quoz 1 – 18th Street"
photos array of urls "6 images"
is_wppr_verified_club boolean Top-tier badge in organizers directory

Schema observation: Same UUID-keyed Club entity serves both clubs AND federations AND academies — Jordan Tennis Federation has a club page at /clubs/2b0e3630-1d4b-.... There is likely a club_type enum: padel_court / academy / federation / event_organizer_only_no_courts.

Organizer

Inferred from: 04-organizers.text.txt, footers on event pages ("BY Padel Sphere — Bati Varfi"), Featurebase changelog ("Trusted organizers skip approval...").

Field Type Notes
id UUID
display_name string "Padel Sphere", "The Majors", "GOAT Games", "WPA Dubai"
slug string Not directly observed but inferred from name appearances
tier enum wppr_verified_club (top — 15 listed) / wppr_verified_organizer (15 listed) / other_organizer (~62 listed) — 3-tier ladder
trust_state enum probation / standard / trusted — inferred from "Trusted organizers skip approval"
is_partnership_active boolean Implied by WPPR Partners count (31 of 92)
primary_contact_name string "Mousa darwish", "Bati Varfi" — appears under organizer brand on event
whatsapp_e164 string Implied — every event has WhatsApp contact
default_club_id (FK, nullable) UUID Federation organizers may not have a default club
country_primary string "Jordan" for Jordan Tennis Federation; "United Arab Emirates" for UAE-based
total_tournaments_hosted int Implied — used to compute the "Most helpful" leaderboard on Featurebase
has_overdue_results boolean Public flag — "Organizers with Overdue Results" section

OrganizerMember (RBAC for Collaboration Accounts)

Inferred from: Featurebase "Collaboration Accounts" (Completed).

Field Type Notes
id UUID
organizer_id (FK) UUID
user_id (FK to canonical User) UUID
role enum owner / admin / scorer / referee (roles not directly observed; minimum 2 inferred)
permissions_json json Specific permission flags (e.g., can_create_tournament, can_edit_draw, can_disqualify)
invited_by_user_id (FK) UUID
invited_at timestamp
accepted_at timestamp (nullable)

Community (WhatsApp-curated)

Inferred from: 09-communities.text.txt.

Field Type Notes
id UUID
name string "Padel One Community"
city string "Dubai"
country string "United Arab Emirates"
parent_organizer_id (FK) UUID "Padel One" (a known club)
whatsapp_group_invite_url string The community is hosted on WhatsApp; this is the link to join
level_match_min decimal "level-matched matchmaking" — implies a level range filter
level_match_max decimal Same
member_count int Not directly observed in public listing
membership_approval_policy enum open / approval_required / invite_only — implied by "trusted membership"

LFP_Listing (Looking-for-Partner)

Inferred from: 43-may-grand-clash.text.txt LFP block.

Field Type Notes
id UUID
tournament_id (FK) UUID LFP is tournament-scoped
player_wppr_id (FK) string "P67152", "P20934", "P40D36"
status enum open (showing in feed) / connected (paired with partner) / withdrawn / expired
created_at timestamp
is_visible_in_public_feed boolean "Public LFP listings now show WPPR profiles" per Apr 30 — toggle exists

WPPRSubmission (drop-in / linked-tournament)

Inferred from: 07-drop-in.text.txt, 34-featurebase-changelog.text.txt Apr 30 ("Resubmit Results", waiver mechanism).

Field Type Notes
id UUID
submission_type enum linked_to_wecourts_tournament / direct_external
linked_tournament_id (FK, nullable) UUID
external_tournament_name string (nullable) When type=direct_external
external_tournament_date date (nullable)
external_venue_name string (nullable)
submitted_by_user_id (FK) UUID
submitted_at timestamp
attachments array of file URLs "Upload files, links, or paste results"
paste_results_text text Free-text alternative to attachments
status enum submitted / in_review / pending / ready_for_wppr / published / waived / overdue
wecourts_reviewer_id (FK, nullable) UUID Manual review queue
waiver_reason text (nullable)
published_at timestamp (nullable)
sla_target_at timestamp start + 7 business days per /drop-in

RatingCycle / RatingProjection

Inferred from: 34-featurebase-changelog.text.txt May 13 entry "Rating Progression — your WPPR journey, fully visible".

Field Type Notes
id UUID
player_wppr_id (FK) string
cycle_started_at timestamp
cycle_match_count_target int 20 (per /rating/levels "updates every 20 matches")
cycle_matches_eligible int Counted toward cycle
cycle_matches_unrated int Counted but not eligible (e.g., self-rated, exhibition)
projected_next_rating decimal "Projected next rating — see where your WPPR is heading before the cycle closes"
current_rating_band enum Country-aware letter band
is_published boolean False during cycle, true at cycle close

Inferred from: 34-featurebase-changelog.text.txt May 4 Operator Console "magic links" tab.

Field Type Notes
id UUID
token string The shareable token
tournament_id (FK) UUID
purpose enum scorer / referee / player_self_register / tv_pairing / public_view_unlisted (inferred subset)
granted_permissions json Specific endpoints/actions enabled
expires_at timestamp (nullable)
created_by_user_id (FK) UUID
created_at timestamp
last_used_at timestamp (nullable)

Incident

Inferred from: Operator Console "incidents" tab + SOS Console actions.

Field Type Notes
id UUID
tournament_id (FK) UUID
match_id (FK, nullable) UUID
team_id (FK, nullable) UUID
incident_type enum no_show / injury / dispute / code_of_conduct / disqualification / withdrawal / equipment_failure (inferred)
outcome_action enum forfeit_wins / zero_unplayed / void_all / replay / no_action
logged_by_user_id (FK) UUID
logged_at timestamp
notes_md text Free-text

TournamentTVScreen (pairing)

Inferred from: 10-tv.text.txt, "OPEN PAIRING SCREEN" button.

Field Type Notes
id UUID
pairing_code string 6-8 char code entered on the TV browser
tournament_id (FK, nullable) UUID NULL when in "poster mode"
screen_mode enum poster / live_results / fullscreen_bracket / tournament_list
paired_at timestamp
auto_refresh_seconds int 30 (default)

Entity relationships

User (auth) ───── 1:N ──── OrganizerMember ──── N:1 ──── Organizer
                                                          │
                                                          1:N
                                                          ▼
                                                       Tournament ───── N:1 ──── Club
                                                          │                       │
                                                          │                      1:N
                                                          │                       ▼
                                                          │                    Court (inferred, not directly observed)
                                                          │
                                                          1:N
                                                          ▼
                                                       Team
                                                          │
                                                          1:N
                                                          ▼
                                                       Match
                                                          │
                                                          1:N (via team)
                                                          ▼
                                                 RatingImpactPerMatchPerPlayer
                                                          │
                                                          N:1
                                                          ▼
                                                       Player
                                                          │
                                                          1:N
                                                          ▼
                                                       RatingCycle

Tournament ───── 1:N ──── LFP_Listing ──── N:1 ──── Player
Tournament ───── 1:N ──── MagicLink
Tournament ───── 1:N ──── Incident
Tournament ───── 1:N ──── TournamentTVScreen
Tournament ───── 1:1 ──── WPPRSubmission (when scored)

Community ───── N:1 ──── Organizer (parent)
Community ───── M:N ──── Player (via membership)

Key schema decisions inferred (lessons for OpenRally)

  1. Player is a globally canonical entity (wppr_id) — exists independent of any single tournament or organizer. This is the identity-graph moat. Players exist in the schema BEFORE they sign up — they're inserted by organizers submitting results.
  2. Tournament has two route types (/e/ scored, /t/ listed) — implies a route_type field controlling whether bracket UI renders. OpenRally should consider an early "listed-only" mode to onboard organizers who don't want full scoring (smooth migration path off spreadsheets).
  3. Multi-currency at the Tournament level (AED + JOD observed) — entry_fee_currency is a real column, not assumption. OpenRally's "Thailand-first" should still ship multi-currency from v0.5+.
  4. Levels are an array on Tournament — ["B-", "C+", "C", "C-"] — a tournament can accept multiple WPPR categories. OpenRally's current category model is unclear (DECISIONS.md doesn't specify per-tournament level array).
  5. Team Level Sum field gates entry by combined skill — "Team Level 7.7" means the two partners' WPPR scores sum ≤ 7.7. This is an anti-sandbagging mechanism baked into registration. OpenRally should design for this when v3 rating lands.
  6. MGG and Direct Advance are first-class booleans tied to team counts — they're not just config in description, they're enforced at draw-time. OpenRally will need this when Groups+KO lands.
  7. Disqualification has 3 outcome modes — forfeit_wins / zero_unplayed / void_all — stored on the Team or Incident, drives recompute of standings. OpenRally's current single-elim model doesn't need this yet but Groups+KO does.
  8. Magic links carry granular granted_permissions JSON — auth-less shareable URLs with limited scope. OpenRally could implement this with Supabase Edge Functions + JWT signed by a per-tournament secret.
  9. Rating Cycle is separate from Player — projected ratings live in a transient cycle table, published ratings live on Player. Updating-every-20-matches is the cycle close trigger.
  10. The Identity graph is leaky — duplicates are a current pain. The fact that WPPR ratings can be applied to players who never signed up means duplicates compound when organizers spell names differently. OpenRally's wedge is to require canonical player identity at registration time (mobile OTP confirm) before granting ratings — keeps the graph clean from day 1.

Post-onboarding-walk additions (2026-05-22)

The Phase 3 partial walk (signup → /onboarding → /onboarding/pending-review) confirmed some inferences and added new entities. Sections below upgrade specific entities from inferred → observed.

Organizer entity — upgraded fields

The Organizer table inferred earlier now has observed columns from the onboarding /onboarding flow (screenshots/64-create-company-form.png):

Field Now confirmed via Notes
display_name #cname text input "Company name *" (required) — placeholder "Your company or brand name"
description #cdesc textarea optional — placeholder mentions "Tell players about your organization, events, and what makes your tournaments special..."
logo_url dual-mode upload (file OR url field) file upload PNG/JPG ≤10MB, OR direct URL input
whatsapp_e164 unnamed tel input Confirmed as system-of-record contact channel — UI literally says "This number will be pre-filled for new tournaments"
email #cemail email optional
website_url #cweb url optional
instagram_url #cinsta url optional
country "Select country" dropdown full ISO-3166; optional on Company (was inferred required)
city "Select city" dropdown country-coupled; optional on Company
approval_state not in form but inferred pending_review after submission; transitions to approved after manual review
organizer_type new column based on path choice enum: padel_club (club-anchored) / organizer_company (entity-anchored)

NEW entity: ClubProfile (distinct from Organizer)

The onboarding split (Padel Club vs Organizer Company) confirms two separate entity types branching from the same approval queue. The Padel Club path creates a Club row directly (linking to Organizer.default_club_id), while the Organizer Company path creates a standalone Organizer record.

Confirmed from screenshots/60-onboarding-create-club.png:

Field Type Required Notes
name text Yes "Club name *"
country enum/text Yes full ISO-3166
city text Yes country-coupled
about textarea No (interestingly labeled "About" not "Description" here vs Company form)
whatsapp_e164 tel No same "pre-filled for new tournaments" copy
email email No
website_url url No
instagram_url url No

Notable absence: No Logo upload in this MVP variant. Club logos come from the wider Clubs directory or the auto-tagged photos field on Club detail page (20-club-detail.text.txt shows "6 images" gallery).

NEW entity: OnboardingState

A state machine that didn't exist in my earlier data-model spec.

Field Type Notes
user_id (FK) UUID
state enum email_unverified / email_verified_no_path / path_chosen_no_entity / entity_created_pending_review / approved / rejected
chosen_path enum (nullable) padel_club / organizer_company (set at step 8 in journey)
whatsapp_e164_resolved string (nullable) Pre-resolved from identity graph if available (observed: Nestor's +66 was pre-filled)
whatsapp_verified_at timestamp (nullable) Auto-set if identity-graph lookup succeeded
entity_created_id (FK, nullable) UUID Points to either Club.id or Organizer.id based on chosen_path
submitted_for_review_at timestamp (nullable)
approved_at timestamp (nullable)
approved_by_user_id (FK, nullable) UUID WeCourts staff who approved
rejection_reason text (nullable) If rejected
approval_notification_channel enum whatsapp (observed) — could also be email as backup

NEW entity: RatingCycle — upgraded with Initial Rating ("IR") status

Earlier inferred RatingCycle.is_published boolean. Confirmed from raw-html/67-rating-wppr.text.txt: - The featured player "Gonzalo Rubio" has WPPR INITIAL RATING 7.587 IR A+ Provisional with 10 matches and 90% win rate, 2 tournaments - "Provisional" is a distinct rating status before the full 20-match cycle closes - "IR" prefix on the badge ("IR A+") differentiates Initial from Published

Updated enum on Player.wppr_status: - unrated — no matches yet - ir_provisional — at least 1 match but cycle not closed (< 20 matches) - wppr — at least one full 20-match cycle published - wppr_verified — verified player (had matches verified by organizers)

NEW: BookingSystemIntegration (inferred from marketing)

Per raw-html/67-rating-wppr.text.txt: "Booking Systems — Full APIs ready. Live with booking systems like PadelOS, Epic Padel, and more."

Field Type Notes
id UUID
vendor enum padelos / epic_padel / future others
club_id (FK) UUID links the wecourts Club to the external booking system
vendor_account_id string external system's account ID
last_sync_at timestamp
sync_status enum connected / disconnected / error
permissions json what wecourts is allowed to read/write

NEW: ExternalRatingSource (inferred from "WPPR Watch")

Per raw-html/67-rating-wppr.text.txt: "WPPR Watch — Aggregating global data from FIP, Premier Padel, RankedIn, Winner App, and Tournify."

Field Type Notes
id UUID
source_name enum fip / premier_padel / rankedin / winner_app / tournify / future others
player_wppr_id (FK) string linked WPPR record
external_player_id string source-system ID
external_rating decimal their rating
external_rank_position int (nullable) global ranking on that source
last_synced_at timestamp

NEW: SmartCourtAICameraSource (inferred)

Per raw-html/67-rating-wppr.text.txt: "AI Camera Feeds — Tracking match results and analytics seamlessly from smart courts powered by AI-powered smart courts."

Field Type Notes
id UUID
court_id (FK) UUID
camera_vendor string (vendor name unspecified in marketing)
auto_score_enabled boolean
feed_endpoint string webhook target

Stack confirmations (post-walk)

Layer Wecourts (confirmed) Notes
Database / API Supabase project URL uptheqepffmurgysqzal.supabase.co visible in HTML preconnect
Storage / CDN DigitalOcean Spaces (Bangalore region) wecourts.blr1.cdn.digitaloceanspaces.com
Edge / CDN Cloudflare global (Bangkok PoP serves SEA) CF-Ray header 9ff6513b591e7b41-BKK
Frontend Next.js Pages Router /_next/static/chunks/pages/_app-*.js paths
In-app feedback / changelog Featurebase SSO via JWT observed cookie featurebase-access-* is HS256 JWT issued by production:backend, audience sess, src: "sso"
Analytics Google Analytics (gtag) confirmed earlier
Email / Auth not yet observed direct vendor; likely Supabase Auth inference
Fonts Google Fonts: Lexend + Lexend Exa preconnect + stylesheet links
PWA configured with manifest + apple-touch-icon /images/pwa/hub/wecourts-hub-pwa-*.png naming suggests "Hub" is the internal organizer app's brand

Updated lesson set

  1. Two-persona organizer model — Club (venue-anchored) vs Organizer Company (entity-anchored, multi-venue) is a meaningful schema choice. OpenRally's current single Organizer model may need a type discriminator early.
  2. Onboarding state machine deserves explicit modeling — wecourts has email_unverified → email_verified → path_chosen → entity_created → pending_review → approved/rejected as discrete states with associated UIs. OpenRally's v0.3 admin organizer-applications has Approve/Reject but the player-facing intermediate states aren't fully spec'd in DECISIONS.md.
  3. Identity-graph pre-fill is a UX feature wecourts already ships — if you sign up with an email that already has a WPPR player record, your phone, name, country are pre-filled. OpenRally could mirror this once OpenRally's player graph exists (probably v3+).
  4. Dedupe-search-before-create on entity creation — wecourts gets this right on Org/Club paths, gets it wrong on Player path. OpenRally should apply this pattern uniformly from day 1.
  5. Two route types per tournament: /e/{code} for scored/WPPR-eligible, /t/{code} for listed-only — confirmed by walking the Americano test event (raw-html/45-americano-event.text.txt). Schema column route_type is real.
  6. Approval / trust ladder is a distinct lifecycle from feature gates — wecourts pings organizers via WhatsApp on state change. OpenRally's email-only notification needs a multi-channel rewrite if we want to match this DX in SEA (where LINE / WhatsApp dominate).