Globalization Model Review — Global vs Region-Specific
Status: Active Last reviewed: 2026-08-10
Owner-commissioned audit (2026-08-10): a thorough classification of every data model as GLOBAL (market-agnostic core) vs REGION-SPECIFIC (Korea-coupled), so a future port swaps configs and providers, not code. Two adversarial audit passes (domain models; schema + integrations) ground every claim in file:line evidence. This doc is the porting map; the canonical patterns it names are the law for new work.
The verdict in one paragraph
The spine is genuinely global: clubs/members/sessions/matches/scores/RSVP, the ELO engine and 5 earned tiers, all matchmaking strategies and rules files, the administrative_divisions ISO-3166-2 hierarchy, and the venues platform (country_code + adm FKs + per-row iana_timezone/currency_code) were built market-agnostic. What is Korea-coupled falls into three classes: (a) region-keyed data that should reference the global geography spine but still reads a hardcoded 17-시도 TEXT vocabulary, (b) provider integrations (Naver/Kakao/KMA/PortOne/data.go.kr) of which only maps sits behind a swap-ready port, and (c) display/config layers (currency, pricing, credential ladders, holiday/season/consent content) that need per-market config entries — for which three good in-repo precedents already exist.
The three canonical region-abstraction patterns (use these; don't invent a fourth)
rating-region.config.ts—getRatingRegionConfig(region)returns the whole market rating model (scales, bands, primary discipline, credential + elite ladders). Surfaces read only the getter. As of this review the KR table physically lives IN the config file (fixed 2026-08-10).consent-matrix.ts+Jurisdiction— the most mature: jurisdiction is a first-class column on every consent row; unimplemented branches (EU/US_CA/CN/BR/JP) throw explicitly, so the extension point is typed and visible.policy-versions.tsis region-keyed the same way.MapServicePort+ locale-aware registry wiring — the provider-swap template: one neutral port, Naver and Google adapters,createLocaleAwareMapService()choosing per locale. Every other provider port should be brought to this shape.
Classification inventory (condensed; audits carry full file:line detail)
Global — keep as-is
Clubs/members/roles/join-requests · profiles (shape) · sessions/RSVP/waitlist/rotation · matches/scores/tournaments/draws (all strategies) · ELO engine + 5 tiers + reliability · dues LEDGER shape · boards/DM/reactions/photos · signals/notifications infra · administrative_divisions + venues spine schema · push (Expo) · export-user-data mechanism · date-utils · feature entitlements.
Region-keyed data — global shape, must reference the geography spine
- THE #1 STRUCTURAL DEBT — region triplication + stalled FK cutover: the 17-시도 vocabulary exists in THREE unsynced sources (
config/regions.tsSIDO tables;club.entity.tsliteralRegiontype;regionSchemazod enum) and in SQL CHECK constraints (clubs.region,public_courts.region,court_venues.region, and — added days ago —club_play_regions). The country-agnostic replacement (adm1_id/adm2_idFKs, dual-written since 00280) EXISTS but no read path uses it. Porting = finish this cutover; new work must stop extending the TEXT vocabulary. profiles.play_regionsJSONB keys are literallysido/sigungu; junctionprofile_play_regions(global) already exists.coords-to-region.ts: KR bounding boxes only.
Provider integrations — need the MapServicePort treatment
| Provider surface | State | Port need |
|---|---|---|
| Maps (Naver/Google) | ✅ swap-ready | none — the template |
Venue search/ingest/corroborate (Naver, Kakao, KSPO, std-data, eshare; naverPlaceId as the spine's sole external anchor) | hardwired; port leaks naverMapUrl/naverPlaceId/kakaoPlaceId + KR gov taxonomies | neutral externalRef {provider,id} shape + the designed-but-deferred venue_external_refs table + second adapter |
| Weather/AQI (KMA + AirKorea) | deepest coupling: the PORT ITSELF takes KMA grid coords; cache schema mirrors KMA product windows; korean-grids.ts lookup | lat/lng-native port redesign + per-market cache/adapters |
Payments (PortOne edge fns; club_bank_accounts = KR bank-transfer shape; dues kakaopay/toss enum; KRW integer amounts everywhere; Toss PSP fee constant in subscription pricing) | hardwired | PSP adapter per market + currency minor-unit decision (currency_code columns already exist) + pricing-region.config + generic payment-method enum |
Holidays (data.go.kr; KoreanHoliday entity name) | hardwired, no country param | country-keyed port + generic entity |
| Identity/auth (Kakao OAuth, 본인인증 roadmap), share (Kakao templates — safe fallback exists), address (Daum postcode), Alimtalk | KR providers | per-market provider allowlists behind existing seams |
Display/config layers — per-market config entries + i18n
Currency formatting (formatKRW at 15+ sites; EN i18n literally renders ₩/ko-KR) · subscription plan names/prices (Korean strings + KRW in domain) · role titles 대표/총무 in DEFAULT_ROLE_CONFIGS · achievement catalog (~70 × 3 Korean strings in domain — largest mechanical extraction) · trust-tier + manner-tag labels · season names 봄/여름 + Northern-Hemisphere date ranges · describeRecurrence/DAY_NAMES Korean literals · Korean zod error messages across entities · ageBand "20~40대" idiom · LocalizedText {ko,en} fixed pair in matchmaking metadata · SQL-embedded Korean copy (generate_session_reminders) · iOS permission strings.
Correctness landmines for any non-KR launch (not just polish)
- DST: recurrence whole-day arithmetic is only safe in DST-free zones (its own comments say so) — must be fixed before any DST market.
- Cron fire times: per-row
iana_timezonefixed the date math (00287) but every pg_cron job still fires at KST-tuned UTC offsets — a second timezone gets reminders at the wrong local hour. - Money minor units: INTEGER amounts are zero-decimal won; a cents currency breaks silently without a minor-unit convention.
- Hemisphere seasons: Mar–May="봄" baked into stored season names.
Porting effort ranking (both audits, merged)
- Region taxonomy cutover (widest; three unsynced sources + SQL CHECKs + every consumer)
- Weather/AQI (port redesign + schema + no guaranteed free provider elsewhere)
- Payments + pricing (new PSP integration + currency semantics + bank-account shapes)
- Venue provider identity + ingestion pipeline twins (~2,500 lines KR-provider logic)
- Currency display + legal crons + content extraction (mechanically large, conceptually simple)
Standing rules going forward (owner-ratified 2026-08-10)
- New region-touching work references the geography spine (adm FKs), never the 시도 TEXT vocabulary — 00449 was the last exception.
- New market-variant behavior goes through a region/jurisdiction config (patterns above), never a hardcoded KR value with a TODO.
- New provider integrations define a neutral port first (MapServicePort shape); provider ids live in generic
externalReffields, not namednaverXxxcolumns. - User-facing strings never live in domain/data files — i18n only (the achievement catalog is the cautionary tale).
EliteBackgroundand rating models stay region-configured; ELO and earned tiers stay global.
Multi-country deployment roadblocks (owner-commissioned 2026-08-10; sample-run-verified)
Beyond the schema work above, these are the walls a real multi-country deployment hits, ranked by how much they threaten OUR current architecture. Empirical runs: Overpass (OSM) venue queries for Palo Alto + central Berlin; local-stack surface-catalog + i18n checks; pins live in globalization-sample.test.ts.
- Venue data LICENSING is the #1 roadblock — not maps rendering. Google Places dominates abroad but its ToS forbids storing/caching Places content beyond ~30 days (place IDs excepted) — a head-on collision with our store-everything venue spine + corroboration pipeline. Naver/Kakao granted us storable KR data; Google will not. The storable global source is OSM (ODbL: attribution + share-alike on the derived venue DB — legal review needed but workable). SAMPLE RUN: OSM has real supply in both test markets (tennis pitches found in Palo Alto and Berlin; Berlin rows even carry clay/tartan/artificial_turf surfaces) BUT quality inverts our KR situation: mostly UNNAMED geometry, no phone/address richness. Consequences: (a) abroad, the club-driven flows we already built — pick-through ingestion, user court-naming, host corroboration — stop being enhancements and become THE primary data source seeded by OSM geometry; (b) Google Places can only be a live-lookup UX layer (autocomplete + place_id + coords), never an ingestion source; (c)
venue_external_refs(S4) must carry per-provider retention semantics. - Units & calendar conventions are config, not cosmetics.
formatDistanceShorthardcodes km (US = miles);DAY_ORDERrenders Monday-first weeks (US convention is Sunday-first); date formats and number grouping follow locale. → aMarketConfigunit/calendar block (measurement system, week start) consumed by the distance util and day-strip renderers. - English pluralization is broken TODAY (S6 widened). KR needs no plurals so the i18n layer is bare template strings; live EN sites render "1 matches"/"1 players" (pinned). S6 must adopt ICU-MessageFormat-style plural rules, not just extract strings.
- Weather/AQI has no global provider parity. KMA/AirKorea are KR-only; the US has free NWS, EU is fragmented, and paid global APIs change the cost model. The product must support weather-feature DEGRADATION per market (signals/warnings optional), not assume parity — on top of the S-series port redesign.
- Payments/payout are a per-country integration, full stop. PSP (PortOne↔Stripe/Adyen), club payout rails (계좌이체 ↔ ACH/SEPA —
club_bank_accountsneeds per-market shapes), app-store billing rules for the subscription, and tax/receipt obligations. No abstraction removes the per-market work; the port just contains it. - Identity & trust model shifts. Kakao OAuth and 본인인증 (KR CI/DI identity assurance) have no US/EU equivalents; age gates differ (KR 14 / US COPPA 13 / EU GDPR 13–16 by member state) — the consent-matrix jurisdiction branches carry this, and the verified-identity roadmap needs per-market assurance strategies (email/phone-only markets lose the strong-identity features unless alternatives are wired).
- Ops/legal long tail. Data-residency expectations (GDPR transfers), per-jurisdiction retention crons (PIPA 30-day pattern parametrized), localized store listings/permission strings, support language coverage.
What did NOT turn out to be a roadblock (verified): surface_catalog already covers EU surfaces (clay_natural/artificial, carpet — OSM's tartan maps to hard/other); the spine took US/DE/JP rows with locale-resolved names untouched; composite FKs isolate each country's geography; consent fails loudly rather than exporting Korean law.