Skip to content

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)

  1. rating-region.config.tsgetRatingRegionConfig(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).
  2. 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.ts is region-keyed the same way.
  3. 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.ts SIDO tables; club.entity.ts literal Region type; regionSchema zod 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_id FKs, 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_regions JSONB keys are literally sido/sigungu; junction profile_play_regions (global) already exists.
  • coords-to-region.ts: KR bounding boxes only.

Provider integrations — need the MapServicePort treatment

Provider surfaceStatePort need
Maps (Naver/Google)✅ swap-readynone — 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 taxonomiesneutral 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 lookuplat/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)hardwiredPSP 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 paramcountry-keyed port + generic entity
Identity/auth (Kakao OAuth, 본인인증 roadmap), share (Kakao templates — safe fallback exists), address (Daum postcode), AlimtalkKR providersper-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)

  1. DST: recurrence whole-day arithmetic is only safe in DST-free zones (its own comments say so) — must be fixed before any DST market.
  2. Cron fire times: per-row iana_timezone fixed 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.
  3. Money minor units: INTEGER amounts are zero-decimal won; a cents currency breaks silently without a minor-unit convention.
  4. Hemisphere seasons: Mar–May="봄" baked into stored season names.

Porting effort ranking (both audits, merged)

  1. Region taxonomy cutover (widest; three unsynced sources + SQL CHECKs + every consumer)
  2. Weather/AQI (port redesign + schema + no guaranteed free provider elsewhere)
  3. Payments + pricing (new PSP integration + currency semantics + bank-account shapes)
  4. Venue provider identity + ingestion pipeline twins (~2,500 lines KR-provider logic)
  5. 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 externalRef fields, not named naverXxx columns.
  • User-facing strings never live in domain/data files — i18n only (the achievement catalog is the cautionary tale).
  • EliteBackground and 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.

  1. 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.
  2. Units & calendar conventions are config, not cosmetics. formatDistanceShort hardcodes km (US = miles); DAY_ORDER renders Monday-first weeks (US convention is Sunday-first); date formats and number grouping follow locale. → a MarketConfig unit/calendar block (measurement system, week start) consumed by the distance util and day-strip renderers.
  3. 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.
  4. 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.
  5. Payments/payout are a per-country integration, full stop. PSP (PortOne↔Stripe/Adyen), club payout rails (계좌이체 ↔ ACH/SEPA — club_bank_accounts needs 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.
  6. 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).
  7. 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.

Markdown remains the source of truth. Run yarn docs:check before handoff.