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 |
MagicLink
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)
- 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. - Tournament has two route types (
/e/scored,/t/listed) — implies aroute_typefield 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). - Multi-currency at the Tournament level (AED + JOD observed) —
entry_fee_currencyis a real column, not assumption. OpenRally's "Thailand-first" should still ship multi-currency from v0.5+. - 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). - 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.
- 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.
- 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. - Magic links carry granular
granted_permissionsJSON — auth-less shareable URLs with limited scope. OpenRally could implement this with Supabase Edge Functions + JWT signed by a per-tournament secret. - 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.
- 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 |
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
- Two-persona organizer model — Club (venue-anchored) vs Organizer Company (entity-anchored, multi-venue) is a meaningful schema choice. OpenRally's current single
Organizermodel may need atypediscriminator early. - Onboarding state machine deserves explicit modeling — wecourts has
email_unverified → email_verified → path_chosen → entity_created → pending_review → approved/rejectedas 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. - 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+).
- 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.
- 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 columnroute_typeis real. - 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).